mirror of https://github.com/portainer/portainer
feat(extensions): introduce RBAC extension (#2900)
parent
27a0188949
commit
8057aa45c4
|
@ -2,6 +2,7 @@ env:
|
||||||
browser: true
|
browser: true
|
||||||
jquery: true
|
jquery: true
|
||||||
node: true
|
node: true
|
||||||
|
es6: true
|
||||||
|
|
||||||
globals:
|
globals:
|
||||||
angular: true
|
angular: true
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"github.com/portainer/portainer/api/bolt/migrator"
|
"github.com/portainer/portainer/api/bolt/migrator"
|
||||||
"github.com/portainer/portainer/api/bolt/registry"
|
"github.com/portainer/portainer/api/bolt/registry"
|
||||||
"github.com/portainer/portainer/api/bolt/resourcecontrol"
|
"github.com/portainer/portainer/api/bolt/resourcecontrol"
|
||||||
|
"github.com/portainer/portainer/api/bolt/role"
|
||||||
"github.com/portainer/portainer/api/bolt/schedule"
|
"github.com/portainer/portainer/api/bolt/schedule"
|
||||||
"github.com/portainer/portainer/api/bolt/settings"
|
"github.com/portainer/portainer/api/bolt/settings"
|
||||||
"github.com/portainer/portainer/api/bolt/stack"
|
"github.com/portainer/portainer/api/bolt/stack"
|
||||||
|
@ -37,6 +38,7 @@ type Store struct {
|
||||||
db *bolt.DB
|
db *bolt.DB
|
||||||
checkForDataMigration bool
|
checkForDataMigration bool
|
||||||
fileService portainer.FileService
|
fileService portainer.FileService
|
||||||
|
RoleService *role.Service
|
||||||
DockerHubService *dockerhub.Service
|
DockerHubService *dockerhub.Service
|
||||||
EndpointGroupService *endpointgroup.Service
|
EndpointGroupService *endpointgroup.Service
|
||||||
EndpointService *endpoint.Service
|
EndpointService *endpoint.Service
|
||||||
|
@ -89,29 +91,6 @@ func (store *Store) Open() error {
|
||||||
return store.initServices()
|
return store.initServices()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init creates the default data set.
|
|
||||||
func (store *Store) Init() error {
|
|
||||||
groups, err := store.EndpointGroupService.EndpointGroups()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(groups) == 0 {
|
|
||||||
unassignedGroup := &portainer.EndpointGroup{
|
|
||||||
Name: "Unassigned",
|
|
||||||
Description: "Unassigned endpoints",
|
|
||||||
Labels: []portainer.Pair{},
|
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
|
||||||
Tags: []string{},
|
|
||||||
}
|
|
||||||
|
|
||||||
return store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the BoltDB database.
|
// Close closes the BoltDB database.
|
||||||
func (store *Store) Close() error {
|
func (store *Store) Close() error {
|
||||||
if store.db != nil {
|
if store.db != nil {
|
||||||
|
@ -140,6 +119,7 @@ func (store *Store) MigrateData() error {
|
||||||
EndpointGroupService: store.EndpointGroupService,
|
EndpointGroupService: store.EndpointGroupService,
|
||||||
EndpointService: store.EndpointService,
|
EndpointService: store.EndpointService,
|
||||||
ExtensionService: store.ExtensionService,
|
ExtensionService: store.ExtensionService,
|
||||||
|
RegistryService: store.RegistryService,
|
||||||
ResourceControlService: store.ResourceControlService,
|
ResourceControlService: store.ResourceControlService,
|
||||||
SettingsService: store.SettingsService,
|
SettingsService: store.SettingsService,
|
||||||
StackService: store.StackService,
|
StackService: store.StackService,
|
||||||
|
@ -162,6 +142,12 @@ func (store *Store) MigrateData() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *Store) initServices() error {
|
func (store *Store) initServices() error {
|
||||||
|
authorizationsetService, err := role.NewService(store.db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
store.RoleService = authorizationsetService
|
||||||
|
|
||||||
dockerhubService, err := dockerhub.NewService(store.db)
|
dockerhubService, err := dockerhub.NewService(store.db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -0,0 +1,431 @@
|
||||||
|
package bolt
|
||||||
|
|
||||||
|
import portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
// Init creates the default data set.
|
||||||
|
func (store *Store) Init() error {
|
||||||
|
groups, err := store.EndpointGroupService.EndpointGroups()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(groups) == 0 {
|
||||||
|
unassignedGroup := &portainer.EndpointGroup{
|
||||||
|
Name: "Unassigned",
|
||||||
|
Description: "Unassigned endpoints",
|
||||||
|
Labels: []portainer.Pair{},
|
||||||
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||||
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||||
|
Tags: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.EndpointGroupService.CreateEndpointGroup(unassignedGroup)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, err := store.RoleService.Roles()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(roles) == 0 {
|
||||||
|
environmentAdministratorRole := &portainer.Role{
|
||||||
|
Name: "Endpoint administrator",
|
||||||
|
Description: "Full control on all the resources inside an endpoint",
|
||||||
|
Authorizations: map[portainer.Authorization]bool{
|
||||||
|
portainer.OperationDockerContainerArchiveInfo: true,
|
||||||
|
portainer.OperationDockerContainerList: true,
|
||||||
|
portainer.OperationDockerContainerExport: true,
|
||||||
|
portainer.OperationDockerContainerChanges: true,
|
||||||
|
portainer.OperationDockerContainerInspect: true,
|
||||||
|
portainer.OperationDockerContainerTop: true,
|
||||||
|
portainer.OperationDockerContainerLogs: true,
|
||||||
|
portainer.OperationDockerContainerStats: true,
|
||||||
|
portainer.OperationDockerContainerAttachWebsocket: true,
|
||||||
|
portainer.OperationDockerContainerArchive: true,
|
||||||
|
portainer.OperationDockerContainerCreate: true,
|
||||||
|
portainer.OperationDockerContainerPrune: true,
|
||||||
|
portainer.OperationDockerContainerKill: true,
|
||||||
|
portainer.OperationDockerContainerPause: true,
|
||||||
|
portainer.OperationDockerContainerUnpause: true,
|
||||||
|
portainer.OperationDockerContainerRestart: true,
|
||||||
|
portainer.OperationDockerContainerStart: true,
|
||||||
|
portainer.OperationDockerContainerStop: true,
|
||||||
|
portainer.OperationDockerContainerWait: true,
|
||||||
|
portainer.OperationDockerContainerResize: true,
|
||||||
|
portainer.OperationDockerContainerAttach: true,
|
||||||
|
portainer.OperationDockerContainerExec: true,
|
||||||
|
portainer.OperationDockerContainerRename: true,
|
||||||
|
portainer.OperationDockerContainerUpdate: true,
|
||||||
|
portainer.OperationDockerContainerPutContainerArchive: true,
|
||||||
|
portainer.OperationDockerContainerDelete: true,
|
||||||
|
portainer.OperationDockerImageList: true,
|
||||||
|
portainer.OperationDockerImageSearch: true,
|
||||||
|
portainer.OperationDockerImageGetAll: true,
|
||||||
|
portainer.OperationDockerImageGet: true,
|
||||||
|
portainer.OperationDockerImageHistory: true,
|
||||||
|
portainer.OperationDockerImageInspect: true,
|
||||||
|
portainer.OperationDockerImageLoad: true,
|
||||||
|
portainer.OperationDockerImageCreate: true,
|
||||||
|
portainer.OperationDockerImagePrune: true,
|
||||||
|
portainer.OperationDockerImagePush: true,
|
||||||
|
portainer.OperationDockerImageTag: true,
|
||||||
|
portainer.OperationDockerImageDelete: true,
|
||||||
|
portainer.OperationDockerImageCommit: true,
|
||||||
|
portainer.OperationDockerImageBuild: true,
|
||||||
|
portainer.OperationDockerNetworkList: true,
|
||||||
|
portainer.OperationDockerNetworkInspect: true,
|
||||||
|
portainer.OperationDockerNetworkCreate: true,
|
||||||
|
portainer.OperationDockerNetworkConnect: true,
|
||||||
|
portainer.OperationDockerNetworkDisconnect: true,
|
||||||
|
portainer.OperationDockerNetworkPrune: true,
|
||||||
|
portainer.OperationDockerNetworkDelete: true,
|
||||||
|
portainer.OperationDockerVolumeList: true,
|
||||||
|
portainer.OperationDockerVolumeInspect: true,
|
||||||
|
portainer.OperationDockerVolumeCreate: true,
|
||||||
|
portainer.OperationDockerVolumePrune: true,
|
||||||
|
portainer.OperationDockerVolumeDelete: true,
|
||||||
|
portainer.OperationDockerExecInspect: true,
|
||||||
|
portainer.OperationDockerExecStart: true,
|
||||||
|
portainer.OperationDockerExecResize: true,
|
||||||
|
portainer.OperationDockerSwarmInspect: true,
|
||||||
|
portainer.OperationDockerSwarmUnlockKey: true,
|
||||||
|
portainer.OperationDockerSwarmInit: true,
|
||||||
|
portainer.OperationDockerSwarmJoin: true,
|
||||||
|
portainer.OperationDockerSwarmLeave: true,
|
||||||
|
portainer.OperationDockerSwarmUpdate: true,
|
||||||
|
portainer.OperationDockerSwarmUnlock: true,
|
||||||
|
portainer.OperationDockerNodeList: true,
|
||||||
|
portainer.OperationDockerNodeInspect: true,
|
||||||
|
portainer.OperationDockerNodeUpdate: true,
|
||||||
|
portainer.OperationDockerNodeDelete: true,
|
||||||
|
portainer.OperationDockerServiceList: true,
|
||||||
|
portainer.OperationDockerServiceInspect: true,
|
||||||
|
portainer.OperationDockerServiceLogs: true,
|
||||||
|
portainer.OperationDockerServiceCreate: true,
|
||||||
|
portainer.OperationDockerServiceUpdate: true,
|
||||||
|
portainer.OperationDockerServiceDelete: true,
|
||||||
|
portainer.OperationDockerSecretList: true,
|
||||||
|
portainer.OperationDockerSecretInspect: true,
|
||||||
|
portainer.OperationDockerSecretCreate: true,
|
||||||
|
portainer.OperationDockerSecretUpdate: true,
|
||||||
|
portainer.OperationDockerSecretDelete: true,
|
||||||
|
portainer.OperationDockerConfigList: true,
|
||||||
|
portainer.OperationDockerConfigInspect: true,
|
||||||
|
portainer.OperationDockerConfigCreate: true,
|
||||||
|
portainer.OperationDockerConfigUpdate: true,
|
||||||
|
portainer.OperationDockerConfigDelete: true,
|
||||||
|
portainer.OperationDockerTaskList: true,
|
||||||
|
portainer.OperationDockerTaskInspect: true,
|
||||||
|
portainer.OperationDockerTaskLogs: true,
|
||||||
|
portainer.OperationDockerPluginList: true,
|
||||||
|
portainer.OperationDockerPluginPrivileges: true,
|
||||||
|
portainer.OperationDockerPluginInspect: true,
|
||||||
|
portainer.OperationDockerPluginPull: true,
|
||||||
|
portainer.OperationDockerPluginCreate: true,
|
||||||
|
portainer.OperationDockerPluginEnable: true,
|
||||||
|
portainer.OperationDockerPluginDisable: true,
|
||||||
|
portainer.OperationDockerPluginPush: true,
|
||||||
|
portainer.OperationDockerPluginUpgrade: true,
|
||||||
|
portainer.OperationDockerPluginSet: true,
|
||||||
|
portainer.OperationDockerPluginDelete: true,
|
||||||
|
portainer.OperationDockerSessionStart: true,
|
||||||
|
portainer.OperationDockerDistributionInspect: true,
|
||||||
|
portainer.OperationDockerBuildPrune: true,
|
||||||
|
portainer.OperationDockerBuildCancel: true,
|
||||||
|
portainer.OperationDockerPing: true,
|
||||||
|
portainer.OperationDockerInfo: true,
|
||||||
|
portainer.OperationDockerVersion: true,
|
||||||
|
portainer.OperationDockerEvents: true,
|
||||||
|
portainer.OperationDockerSystem: true,
|
||||||
|
portainer.OperationDockerUndefined: true,
|
||||||
|
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,
|
||||||
|
portainer.OperationPortainerResourceControlDelete: true,
|
||||||
|
portainer.OperationPortainerStackList: true,
|
||||||
|
portainer.OperationPortainerStackInspect: true,
|
||||||
|
portainer.OperationPortainerStackFile: true,
|
||||||
|
portainer.OperationPortainerStackCreate: true,
|
||||||
|
portainer.OperationPortainerStackMigrate: true,
|
||||||
|
portainer.OperationPortainerStackUpdate: true,
|
||||||
|
portainer.OperationPortainerStackDelete: true,
|
||||||
|
portainer.OperationPortainerWebsocketExec: true,
|
||||||
|
portainer.OperationPortainerWebhookList: true,
|
||||||
|
portainer.OperationPortainerWebhookCreate: true,
|
||||||
|
portainer.OperationPortainerWebhookDelete: true,
|
||||||
|
portainer.EndpointResourcesAccess: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.RoleService.CreateRole(environmentAdministratorRole)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
environmentReadOnlyUserRole := &portainer.Role{
|
||||||
|
Name: "Helpdesk",
|
||||||
|
Description: "Read-only authorizations on all the resources inside an endpoint",
|
||||||
|
Authorizations: map[portainer.Authorization]bool{
|
||||||
|
portainer.OperationDockerContainerArchiveInfo: true,
|
||||||
|
portainer.OperationDockerContainerList: true,
|
||||||
|
portainer.OperationDockerContainerChanges: true,
|
||||||
|
portainer.OperationDockerContainerInspect: true,
|
||||||
|
portainer.OperationDockerContainerTop: true,
|
||||||
|
portainer.OperationDockerContainerLogs: true,
|
||||||
|
portainer.OperationDockerContainerStats: true,
|
||||||
|
portainer.OperationDockerImageList: true,
|
||||||
|
portainer.OperationDockerImageSearch: true,
|
||||||
|
portainer.OperationDockerImageGetAll: true,
|
||||||
|
portainer.OperationDockerImageGet: true,
|
||||||
|
portainer.OperationDockerImageHistory: true,
|
||||||
|
portainer.OperationDockerImageInspect: true,
|
||||||
|
portainer.OperationDockerNetworkList: true,
|
||||||
|
portainer.OperationDockerNetworkInspect: true,
|
||||||
|
portainer.OperationDockerVolumeList: true,
|
||||||
|
portainer.OperationDockerVolumeInspect: true,
|
||||||
|
portainer.OperationDockerSwarmInspect: true,
|
||||||
|
portainer.OperationDockerNodeList: true,
|
||||||
|
portainer.OperationDockerNodeInspect: true,
|
||||||
|
portainer.OperationDockerServiceList: true,
|
||||||
|
portainer.OperationDockerServiceInspect: true,
|
||||||
|
portainer.OperationDockerServiceLogs: true,
|
||||||
|
portainer.OperationDockerSecretList: true,
|
||||||
|
portainer.OperationDockerSecretInspect: true,
|
||||||
|
portainer.OperationDockerConfigList: true,
|
||||||
|
portainer.OperationDockerConfigInspect: true,
|
||||||
|
portainer.OperationDockerTaskList: true,
|
||||||
|
portainer.OperationDockerTaskInspect: true,
|
||||||
|
portainer.OperationDockerTaskLogs: true,
|
||||||
|
portainer.OperationDockerPluginList: true,
|
||||||
|
portainer.OperationDockerDistributionInspect: true,
|
||||||
|
portainer.OperationDockerPing: true,
|
||||||
|
portainer.OperationDockerInfo: true,
|
||||||
|
portainer.OperationDockerVersion: true,
|
||||||
|
portainer.OperationDockerEvents: true,
|
||||||
|
portainer.OperationDockerSystem: true,
|
||||||
|
portainer.OperationDockerAgentPing: true,
|
||||||
|
portainer.OperationDockerAgentList: true,
|
||||||
|
portainer.OperationDockerAgentHostInfo: true,
|
||||||
|
portainer.OperationDockerAgentBrowseGet: true,
|
||||||
|
portainer.OperationDockerAgentBrowseList: true,
|
||||||
|
portainer.OperationPortainerStackList: true,
|
||||||
|
portainer.OperationPortainerStackInspect: true,
|
||||||
|
portainer.OperationPortainerStackFile: true,
|
||||||
|
portainer.OperationPortainerWebhookList: true,
|
||||||
|
portainer.EndpointResourcesAccess: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.RoleService.CreateRole(environmentReadOnlyUserRole)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
standardUserRole := &portainer.Role{
|
||||||
|
Name: "Standard user",
|
||||||
|
Description: "Regular user account restricted to access authorized resources",
|
||||||
|
Authorizations: map[portainer.Authorization]bool{
|
||||||
|
portainer.OperationDockerContainerArchiveInfo: true,
|
||||||
|
portainer.OperationDockerContainerList: true,
|
||||||
|
portainer.OperationDockerContainerExport: true,
|
||||||
|
portainer.OperationDockerContainerChanges: true,
|
||||||
|
portainer.OperationDockerContainerInspect: true,
|
||||||
|
portainer.OperationDockerContainerTop: true,
|
||||||
|
portainer.OperationDockerContainerLogs: true,
|
||||||
|
portainer.OperationDockerContainerStats: true,
|
||||||
|
portainer.OperationDockerContainerAttachWebsocket: true,
|
||||||
|
portainer.OperationDockerContainerArchive: true,
|
||||||
|
portainer.OperationDockerContainerCreate: true,
|
||||||
|
portainer.OperationDockerContainerKill: true,
|
||||||
|
portainer.OperationDockerContainerPause: true,
|
||||||
|
portainer.OperationDockerContainerUnpause: true,
|
||||||
|
portainer.OperationDockerContainerRestart: true,
|
||||||
|
portainer.OperationDockerContainerStart: true,
|
||||||
|
portainer.OperationDockerContainerStop: true,
|
||||||
|
portainer.OperationDockerContainerWait: true,
|
||||||
|
portainer.OperationDockerContainerResize: true,
|
||||||
|
portainer.OperationDockerContainerAttach: true,
|
||||||
|
portainer.OperationDockerContainerExec: true,
|
||||||
|
portainer.OperationDockerContainerRename: true,
|
||||||
|
portainer.OperationDockerContainerUpdate: true,
|
||||||
|
portainer.OperationDockerContainerPutContainerArchive: true,
|
||||||
|
portainer.OperationDockerContainerDelete: true,
|
||||||
|
portainer.OperationDockerImageList: true,
|
||||||
|
portainer.OperationDockerImageSearch: true,
|
||||||
|
portainer.OperationDockerImageGetAll: true,
|
||||||
|
portainer.OperationDockerImageGet: true,
|
||||||
|
portainer.OperationDockerImageHistory: true,
|
||||||
|
portainer.OperationDockerImageInspect: true,
|
||||||
|
portainer.OperationDockerImageLoad: true,
|
||||||
|
portainer.OperationDockerImageCreate: true,
|
||||||
|
portainer.OperationDockerImagePush: true,
|
||||||
|
portainer.OperationDockerImageTag: true,
|
||||||
|
portainer.OperationDockerImageDelete: true,
|
||||||
|
portainer.OperationDockerImageCommit: true,
|
||||||
|
portainer.OperationDockerImageBuild: true,
|
||||||
|
portainer.OperationDockerNetworkList: true,
|
||||||
|
portainer.OperationDockerNetworkInspect: true,
|
||||||
|
portainer.OperationDockerNetworkCreate: true,
|
||||||
|
portainer.OperationDockerNetworkConnect: true,
|
||||||
|
portainer.OperationDockerNetworkDisconnect: true,
|
||||||
|
portainer.OperationDockerNetworkDelete: true,
|
||||||
|
portainer.OperationDockerVolumeList: true,
|
||||||
|
portainer.OperationDockerVolumeInspect: true,
|
||||||
|
portainer.OperationDockerVolumeCreate: true,
|
||||||
|
portainer.OperationDockerVolumeDelete: true,
|
||||||
|
portainer.OperationDockerExecInspect: true,
|
||||||
|
portainer.OperationDockerExecStart: true,
|
||||||
|
portainer.OperationDockerExecResize: true,
|
||||||
|
portainer.OperationDockerSwarmInspect: true,
|
||||||
|
portainer.OperationDockerSwarmUnlockKey: true,
|
||||||
|
portainer.OperationDockerSwarmInit: true,
|
||||||
|
portainer.OperationDockerSwarmJoin: true,
|
||||||
|
portainer.OperationDockerSwarmLeave: true,
|
||||||
|
portainer.OperationDockerSwarmUpdate: true,
|
||||||
|
portainer.OperationDockerSwarmUnlock: true,
|
||||||
|
portainer.OperationDockerNodeList: true,
|
||||||
|
portainer.OperationDockerNodeInspect: true,
|
||||||
|
portainer.OperationDockerNodeUpdate: true,
|
||||||
|
portainer.OperationDockerNodeDelete: true,
|
||||||
|
portainer.OperationDockerServiceList: true,
|
||||||
|
portainer.OperationDockerServiceInspect: true,
|
||||||
|
portainer.OperationDockerServiceLogs: true,
|
||||||
|
portainer.OperationDockerServiceCreate: true,
|
||||||
|
portainer.OperationDockerServiceUpdate: true,
|
||||||
|
portainer.OperationDockerServiceDelete: true,
|
||||||
|
portainer.OperationDockerSecretList: true,
|
||||||
|
portainer.OperationDockerSecretInspect: true,
|
||||||
|
portainer.OperationDockerSecretCreate: true,
|
||||||
|
portainer.OperationDockerSecretUpdate: true,
|
||||||
|
portainer.OperationDockerSecretDelete: true,
|
||||||
|
portainer.OperationDockerConfigList: true,
|
||||||
|
portainer.OperationDockerConfigInspect: true,
|
||||||
|
portainer.OperationDockerConfigCreate: true,
|
||||||
|
portainer.OperationDockerConfigUpdate: true,
|
||||||
|
portainer.OperationDockerConfigDelete: true,
|
||||||
|
portainer.OperationDockerTaskList: true,
|
||||||
|
portainer.OperationDockerTaskInspect: true,
|
||||||
|
portainer.OperationDockerTaskLogs: true,
|
||||||
|
portainer.OperationDockerPluginList: true,
|
||||||
|
portainer.OperationDockerPluginPrivileges: true,
|
||||||
|
portainer.OperationDockerPluginInspect: true,
|
||||||
|
portainer.OperationDockerPluginPull: true,
|
||||||
|
portainer.OperationDockerPluginCreate: true,
|
||||||
|
portainer.OperationDockerPluginEnable: true,
|
||||||
|
portainer.OperationDockerPluginDisable: true,
|
||||||
|
portainer.OperationDockerPluginPush: true,
|
||||||
|
portainer.OperationDockerPluginUpgrade: true,
|
||||||
|
portainer.OperationDockerPluginSet: true,
|
||||||
|
portainer.OperationDockerPluginDelete: true,
|
||||||
|
portainer.OperationDockerSessionStart: true,
|
||||||
|
portainer.OperationDockerDistributionInspect: true,
|
||||||
|
portainer.OperationDockerBuildPrune: true,
|
||||||
|
portainer.OperationDockerBuildCancel: true,
|
||||||
|
portainer.OperationDockerPing: true,
|
||||||
|
portainer.OperationDockerInfo: true,
|
||||||
|
portainer.OperationDockerVersion: true,
|
||||||
|
portainer.OperationDockerEvents: true,
|
||||||
|
portainer.OperationDockerSystem: true,
|
||||||
|
portainer.OperationDockerUndefined: true,
|
||||||
|
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,
|
||||||
|
portainer.OperationPortainerResourceControlDelete: true,
|
||||||
|
portainer.OperationPortainerStackList: true,
|
||||||
|
portainer.OperationPortainerStackInspect: true,
|
||||||
|
portainer.OperationPortainerStackFile: true,
|
||||||
|
portainer.OperationPortainerStackCreate: true,
|
||||||
|
portainer.OperationPortainerStackMigrate: true,
|
||||||
|
portainer.OperationPortainerStackUpdate: true,
|
||||||
|
portainer.OperationPortainerStackDelete: true,
|
||||||
|
portainer.OperationPortainerWebsocketExec: true,
|
||||||
|
portainer.OperationPortainerWebhookList: true,
|
||||||
|
portainer.OperationPortainerWebhookCreate: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.RoleService.CreateRole(standardUserRole)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
readOnlyUserRole := &portainer.Role{
|
||||||
|
Name: "Read-only user",
|
||||||
|
Description: "Read-only user account restricted to access authorized resources",
|
||||||
|
Authorizations: map[portainer.Authorization]bool{
|
||||||
|
portainer.OperationDockerContainerArchiveInfo: true,
|
||||||
|
portainer.OperationDockerContainerList: true,
|
||||||
|
portainer.OperationDockerContainerChanges: true,
|
||||||
|
portainer.OperationDockerContainerInspect: true,
|
||||||
|
portainer.OperationDockerContainerTop: true,
|
||||||
|
portainer.OperationDockerContainerLogs: true,
|
||||||
|
portainer.OperationDockerContainerStats: true,
|
||||||
|
portainer.OperationDockerImageList: true,
|
||||||
|
portainer.OperationDockerImageSearch: true,
|
||||||
|
portainer.OperationDockerImageGetAll: true,
|
||||||
|
portainer.OperationDockerImageGet: true,
|
||||||
|
portainer.OperationDockerImageHistory: true,
|
||||||
|
portainer.OperationDockerImageInspect: true,
|
||||||
|
portainer.OperationDockerNetworkList: true,
|
||||||
|
portainer.OperationDockerNetworkInspect: true,
|
||||||
|
portainer.OperationDockerVolumeList: true,
|
||||||
|
portainer.OperationDockerVolumeInspect: true,
|
||||||
|
portainer.OperationDockerSwarmInspect: true,
|
||||||
|
portainer.OperationDockerNodeList: true,
|
||||||
|
portainer.OperationDockerNodeInspect: true,
|
||||||
|
portainer.OperationDockerServiceList: true,
|
||||||
|
portainer.OperationDockerServiceInspect: true,
|
||||||
|
portainer.OperationDockerServiceLogs: true,
|
||||||
|
portainer.OperationDockerSecretList: true,
|
||||||
|
portainer.OperationDockerSecretInspect: true,
|
||||||
|
portainer.OperationDockerConfigList: true,
|
||||||
|
portainer.OperationDockerConfigInspect: true,
|
||||||
|
portainer.OperationDockerTaskList: true,
|
||||||
|
portainer.OperationDockerTaskInspect: true,
|
||||||
|
portainer.OperationDockerTaskLogs: true,
|
||||||
|
portainer.OperationDockerPluginList: true,
|
||||||
|
portainer.OperationDockerDistributionInspect: true,
|
||||||
|
portainer.OperationDockerPing: true,
|
||||||
|
portainer.OperationDockerInfo: true,
|
||||||
|
portainer.OperationDockerVersion: true,
|
||||||
|
portainer.OperationDockerEvents: true,
|
||||||
|
portainer.OperationDockerSystem: true,
|
||||||
|
portainer.OperationDockerAgentPing: true,
|
||||||
|
portainer.OperationDockerAgentList: true,
|
||||||
|
portainer.OperationDockerAgentHostInfo: true,
|
||||||
|
portainer.OperationDockerAgentBrowseGet: true,
|
||||||
|
portainer.OperationDockerAgentBrowseList: true,
|
||||||
|
portainer.OperationPortainerStackList: true,
|
||||||
|
portainer.OperationPortainerStackInspect: true,
|
||||||
|
portainer.OperationPortainerStackFile: true,
|
||||||
|
portainer.OperationPortainerWebhookList: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.RoleService.CreateRole(readOnlyUserRole)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
package migrator
|
||||||
|
|
||||||
|
import (
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Migrator) updateUsersToDBVersion18() error {
|
||||||
|
legacyUsers, err := m.userService.Users()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range legacyUsers {
|
||||||
|
user.PortainerAuthorizations = map[portainer.Authorization]bool{
|
||||||
|
portainer.OperationPortainerDockerHubInspect: true,
|
||||||
|
portainer.OperationPortainerEndpointGroupList: true,
|
||||||
|
portainer.OperationPortainerEndpointList: true,
|
||||||
|
portainer.OperationPortainerEndpointInspect: true,
|
||||||
|
portainer.OperationPortainerEndpointExtensionAdd: true,
|
||||||
|
portainer.OperationPortainerEndpointExtensionRemove: true,
|
||||||
|
portainer.OperationPortainerExtensionList: true,
|
||||||
|
portainer.OperationPortainerMOTD: true,
|
||||||
|
portainer.OperationPortainerRegistryList: true,
|
||||||
|
portainer.OperationPortainerRegistryInspect: true,
|
||||||
|
portainer.OperationPortainerTeamList: true,
|
||||||
|
portainer.OperationPortainerTemplateList: true,
|
||||||
|
portainer.OperationPortainerTemplateInspect: true,
|
||||||
|
portainer.OperationPortainerUserList: true,
|
||||||
|
portainer.OperationPortainerUserMemberships: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.userService.UpdateUser(user.ID, &user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Migrator) updateEndpointsToDBVersion18() error {
|
||||||
|
legacyEndpoints, err := m.endpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range legacyEndpoints {
|
||||||
|
endpoint.UserAccessPolicies = make(portainer.UserAccessPolicies)
|
||||||
|
for _, userID := range endpoint.AuthorizedUsers {
|
||||||
|
endpoint.UserAccessPolicies[userID] = portainer.AccessPolicy{
|
||||||
|
RoleID: 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint.TeamAccessPolicies = make(portainer.TeamAccessPolicies)
|
||||||
|
for _, teamID := range endpoint.AuthorizedTeams {
|
||||||
|
endpoint.TeamAccessPolicies[teamID] = portainer.AccessPolicy{
|
||||||
|
RoleID: 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
|
||||||
|
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpointGroup := range legacyEndpointGroups {
|
||||||
|
endpointGroup.UserAccessPolicies = make(portainer.UserAccessPolicies)
|
||||||
|
for _, userID := range endpointGroup.AuthorizedUsers {
|
||||||
|
endpointGroup.UserAccessPolicies[userID] = portainer.AccessPolicy{
|
||||||
|
RoleID: 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointGroup.TeamAccessPolicies = make(portainer.TeamAccessPolicies)
|
||||||
|
for _, teamID := range endpointGroup.AuthorizedTeams {
|
||||||
|
endpointGroup.TeamAccessPolicies[teamID] = portainer.AccessPolicy{
|
||||||
|
RoleID: 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Migrator) updateRegistriesToDBVersion18() error {
|
||||||
|
legacyRegistries, err := m.registryService.Registries()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, registry := range legacyRegistries {
|
||||||
|
registry.UserAccessPolicies = make(portainer.UserAccessPolicies)
|
||||||
|
for _, userID := range registry.AuthorizedUsers {
|
||||||
|
registry.UserAccessPolicies[userID] = portainer.AccessPolicy{}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.TeamAccessPolicies = make(portainer.TeamAccessPolicies)
|
||||||
|
for _, teamID := range registry.AuthorizedTeams {
|
||||||
|
registry.TeamAccessPolicies[teamID] = portainer.AccessPolicy{}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.registryService.UpdateRegistry(registry.ID, ®istry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/portainer/portainer/api/bolt/endpoint"
|
"github.com/portainer/portainer/api/bolt/endpoint"
|
||||||
"github.com/portainer/portainer/api/bolt/endpointgroup"
|
"github.com/portainer/portainer/api/bolt/endpointgroup"
|
||||||
"github.com/portainer/portainer/api/bolt/extension"
|
"github.com/portainer/portainer/api/bolt/extension"
|
||||||
|
"github.com/portainer/portainer/api/bolt/registry"
|
||||||
"github.com/portainer/portainer/api/bolt/resourcecontrol"
|
"github.com/portainer/portainer/api/bolt/resourcecontrol"
|
||||||
"github.com/portainer/portainer/api/bolt/settings"
|
"github.com/portainer/portainer/api/bolt/settings"
|
||||||
"github.com/portainer/portainer/api/bolt/stack"
|
"github.com/portainer/portainer/api/bolt/stack"
|
||||||
|
@ -22,6 +23,7 @@ type (
|
||||||
endpointGroupService *endpointgroup.Service
|
endpointGroupService *endpointgroup.Service
|
||||||
endpointService *endpoint.Service
|
endpointService *endpoint.Service
|
||||||
extensionService *extension.Service
|
extensionService *extension.Service
|
||||||
|
registryService *registry.Service
|
||||||
resourceControlService *resourcecontrol.Service
|
resourceControlService *resourcecontrol.Service
|
||||||
settingsService *settings.Service
|
settingsService *settings.Service
|
||||||
stackService *stack.Service
|
stackService *stack.Service
|
||||||
|
@ -38,6 +40,7 @@ type (
|
||||||
EndpointGroupService *endpointgroup.Service
|
EndpointGroupService *endpointgroup.Service
|
||||||
EndpointService *endpoint.Service
|
EndpointService *endpoint.Service
|
||||||
ExtensionService *extension.Service
|
ExtensionService *extension.Service
|
||||||
|
RegistryService *registry.Service
|
||||||
ResourceControlService *resourcecontrol.Service
|
ResourceControlService *resourcecontrol.Service
|
||||||
SettingsService *settings.Service
|
SettingsService *settings.Service
|
||||||
StackService *stack.Service
|
StackService *stack.Service
|
||||||
|
@ -56,6 +59,7 @@ func NewMigrator(parameters *Parameters) *Migrator {
|
||||||
endpointGroupService: parameters.EndpointGroupService,
|
endpointGroupService: parameters.EndpointGroupService,
|
||||||
endpointService: parameters.EndpointService,
|
endpointService: parameters.EndpointService,
|
||||||
extensionService: parameters.ExtensionService,
|
extensionService: parameters.ExtensionService,
|
||||||
|
registryService: parameters.RegistryService,
|
||||||
resourceControlService: parameters.ResourceControlService,
|
resourceControlService: parameters.ResourceControlService,
|
||||||
settingsService: parameters.SettingsService,
|
settingsService: parameters.SettingsService,
|
||||||
templateService: parameters.TemplateService,
|
templateService: parameters.TemplateService,
|
||||||
|
@ -222,5 +226,28 @@ func (m *Migrator) Migrate() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Portainer 1.20.2
|
||||||
|
if m.currentDBVersion < 18 {
|
||||||
|
err := m.updateUsersToDBVersion18()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.updateEndpointsToDBVersion18()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.updateEndpointGroupsToDBVersion18()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.updateRegistriesToDBVersion18()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
package role
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/bolt/internal"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
|
BucketName = "roles"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service represents a service for managing endpoint data.
|
||||||
|
type Service struct {
|
||||||
|
db *bolt.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new instance of a service.
|
||||||
|
func NewService(db *bolt.DB) (*Service, error) {
|
||||||
|
err := internal.CreateBucket(db, BucketName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Service{
|
||||||
|
db: db,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Role returns a Role by ID
|
||||||
|
func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) {
|
||||||
|
var set portainer.Role
|
||||||
|
identifier := internal.Itob(int(ID))
|
||||||
|
|
||||||
|
err := internal.GetObject(service.db, BucketName, identifier, &set)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &set, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roles return an array containing all the sets.
|
||||||
|
func (service *Service) Roles() ([]portainer.Role, error) {
|
||||||
|
var sets = make([]portainer.Role, 0)
|
||||||
|
|
||||||
|
err := service.db.View(func(tx *bolt.Tx) error {
|
||||||
|
bucket := tx.Bucket([]byte(BucketName))
|
||||||
|
|
||||||
|
cursor := bucket.Cursor()
|
||||||
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
|
var set portainer.Role
|
||||||
|
err := internal.UnmarshalObject(v, &set)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sets = append(sets, set)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return sets, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRole creates a new Role.
|
||||||
|
func (service *Service) CreateRole(set *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)
|
||||||
|
|
||||||
|
data, err := internal.MarshalObject(set)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return bucket.Put(internal.Itob(int(set.ID)), data)
|
||||||
|
})
|
||||||
|
}
|
|
@ -383,8 +383,8 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain
|
||||||
GroupID: portainer.EndpointGroupID(1),
|
GroupID: portainer.EndpointGroupID(1),
|
||||||
Type: portainer.DockerEnvironment,
|
Type: portainer.DockerEnvironment,
|
||||||
TLSConfig: tlsConfiguration,
|
TLSConfig: tlsConfiguration,
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
Status: portainer.EndpointStatusUp,
|
Status: portainer.EndpointStatusUp,
|
||||||
|
@ -426,8 +426,8 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo
|
||||||
GroupID: portainer.EndpointGroupID(1),
|
GroupID: portainer.EndpointGroupID(1),
|
||||||
Type: portainer.DockerEnvironment,
|
Type: portainer.DockerEnvironment,
|
||||||
TLSConfig: portainer.TLSConfiguration{},
|
TLSConfig: portainer.TLSConfiguration{},
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
Status: portainer.EndpointStatusUp,
|
Status: portainer.EndpointStatusUp,
|
||||||
|
@ -647,6 +647,7 @@ func main() {
|
||||||
AssetsPath: *flags.Assets,
|
AssetsPath: *flags.Assets,
|
||||||
AuthDisabled: *flags.NoAuth,
|
AuthDisabled: *flags.NoAuth,
|
||||||
EndpointManagement: endpointManagement,
|
EndpointManagement: endpointManagement,
|
||||||
|
RoleService: store.RoleService,
|
||||||
UserService: store.UserService,
|
UserService: store.UserService,
|
||||||
TeamService: store.TeamService,
|
TeamService: store.TeamService,
|
||||||
TeamMembershipService: store.TeamMembershipService,
|
TeamMembershipService: store.TeamMembershipService,
|
||||||
|
|
|
@ -4,6 +4,7 @@ package portainer
|
||||||
const (
|
const (
|
||||||
ErrUnauthorized = Error("Unauthorized")
|
ErrUnauthorized = Error("Unauthorized")
|
||||||
ErrResourceAccessDenied = Error("Access denied to resource")
|
ErrResourceAccessDenied = Error("Access denied to resource")
|
||||||
|
ErrAuthorizationRequired = Error("Authorization required for this operation")
|
||||||
ErrObjectNotFound = Error("Object not found inside the database")
|
ErrObjectNotFound = Error("Object not found inside the database")
|
||||||
ErrMissingSecurityContext = Error("Unable to find security details in request context")
|
ErrMissingSecurityContext = Error("Unable to find security details in request context")
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/orcaman/concurrent-map"
|
"github.com/orcaman/concurrent-map"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
|
@ -20,6 +21,7 @@ var extensionDownloadBaseURL = "https://portainer-io-assets.sfo2.digitaloceanspa
|
||||||
var extensionBinaryMap = map[portainer.ExtensionID]string{
|
var extensionBinaryMap = map[portainer.ExtensionID]string{
|
||||||
portainer.RegistryManagementExtension: "extension-registry-management",
|
portainer.RegistryManagementExtension: "extension-registry-management",
|
||||||
portainer.OAuthAuthenticationExtension: "extension-oauth-authentication",
|
portainer.OAuthAuthenticationExtension: "extension-oauth-authentication",
|
||||||
|
portainer.RBACExtension: "extension-rbac",
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtensionManager represents a service used to
|
// ExtensionManager represents a service used to
|
||||||
|
@ -206,6 +208,8 @@ func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Exte
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
manager.processes.Set(processKey(extension.ID), extensionProcess)
|
manager.processes.Set(processKey(extension.ID), extensionProcess)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,8 +120,57 @@ func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User)
|
||||||
ID: user.ID,
|
ID: user.ID,
|
||||||
Username: user.Username,
|
Username: user.Username,
|
||||||
Role: user.Role,
|
Role: user.Role,
|
||||||
|
PortainerAuthorizations: user.PortainerAuthorizations,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err := handler.ExtensionService.Extension(portainer.RBACExtension)
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
return handler.persistAndWriteToken(w, tokenData)
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointAuthorizations, err := handler.getAuthorizations(user)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve authorizations associated to the user", err}
|
||||||
|
}
|
||||||
|
tokenData.EndpointAuthorizations = endpointAuthorizations
|
||||||
|
|
||||||
|
return handler.persistAndWriteToken(w, tokenData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) getAuthorizations(user *portainer.User) (portainer.EndpointAuthorizations, error) {
|
||||||
|
endpointAuthorizations := portainer.EndpointAuthorizations{}
|
||||||
|
if user.Role == portainer.AdministratorRole {
|
||||||
|
return endpointAuthorizations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userMemberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return endpointAuthorizations, err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints, err := handler.EndpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return endpointAuthorizations, err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||||
|
if err != nil {
|
||||||
|
return endpointAuthorizations, err
|
||||||
|
}
|
||||||
|
|
||||||
|
roles, err := handler.RoleService.Roles()
|
||||||
|
if err != nil {
|
||||||
|
return endpointAuthorizations, err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointAuthorizations = getUserEndpointAuthorizations(user, endpoints, endpointGroups, roles, userMemberships)
|
||||||
|
|
||||||
|
return endpointAuthorizations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {
|
||||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
|
||||||
|
|
|
@ -3,8 +3,8 @@ package auth
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
func getUserEndpointAuthorizations(user *portainer.User, endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, roles []portainer.Role, userMemberships []portainer.TeamMembership) portainer.EndpointAuthorizations {
|
||||||
|
endpointAuthorizations := make(portainer.EndpointAuthorizations)
|
||||||
|
|
||||||
|
groupUserAccessPolicies := map[portainer.EndpointGroupID]portainer.UserAccessPolicies{}
|
||||||
|
groupTeamAccessPolicies := map[portainer.EndpointGroupID]portainer.TeamAccessPolicies{}
|
||||||
|
for _, endpointGroup := range endpointGroups {
|
||||||
|
groupUserAccessPolicies[endpointGroup.ID] = endpointGroup.UserAccessPolicies
|
||||||
|
groupTeamAccessPolicies[endpointGroup.ID] = endpointGroup.TeamAccessPolicies
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
authorizations := getAuthorizationsFromUserEndpointPolicy(user, &endpoint, roles)
|
||||||
|
if len(authorizations) > 0 {
|
||||||
|
endpointAuthorizations[endpoint.ID] = authorizations
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizations = getAuthorizationsFromUserEndpointGroupPolicy(user, &endpoint, roles, groupUserAccessPolicies)
|
||||||
|
if len(authorizations) > 0 {
|
||||||
|
endpointAuthorizations[endpoint.ID] = authorizations
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizations = getAuthorizationsFromTeamEndpointPolicies(userMemberships, &endpoint, roles)
|
||||||
|
if len(authorizations) > 0 {
|
||||||
|
endpointAuthorizations[endpoint.ID] = authorizations
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointAuthorizations[endpoint.ID] = getAuthorizationsFromTeamEndpointGroupPolicies(userMemberships, &endpoint, roles, groupTeamAccessPolicies)
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpointAuthorizations
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthorizationsFromUserEndpointPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations {
|
||||||
|
policyRoles := make([]portainer.RoleID, 0)
|
||||||
|
|
||||||
|
policy, ok := endpoint.UserAccessPolicies[user.ID]
|
||||||
|
if ok {
|
||||||
|
policyRoles = append(policyRoles, policy.RoleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthorizationsFromUserEndpointGroupPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.UserAccessPolicies) portainer.Authorizations {
|
||||||
|
policyRoles := make([]portainer.RoleID, 0)
|
||||||
|
|
||||||
|
policy, ok := groupAccessPolicies[endpoint.GroupID][user.ID]
|
||||||
|
if ok {
|
||||||
|
policyRoles = append(policyRoles, policy.RoleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthorizationsFromTeamEndpointPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations {
|
||||||
|
policyRoles := make([]portainer.RoleID, 0)
|
||||||
|
|
||||||
|
for _, membership := range memberships {
|
||||||
|
policy, ok := endpoint.TeamAccessPolicies[membership.TeamID]
|
||||||
|
if ok {
|
||||||
|
policyRoles = append(policyRoles, policy.RoleID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.TeamAccessPolicies) portainer.Authorizations {
|
||||||
|
policyRoles := make([]portainer.RoleID, 0)
|
||||||
|
|
||||||
|
for _, membership := range memberships {
|
||||||
|
policy, ok := groupAccessPolicies[endpoint.GroupID][membership.TeamID]
|
||||||
|
if ok {
|
||||||
|
policyRoles = append(policyRoles, policy.RoleID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAuthorizationsFromRoles(policyRoles, roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAuthorizationsFromRoles(roleIdentifiers []portainer.RoleID, roles []portainer.Role) portainer.Authorizations {
|
||||||
|
var roleAuthorizations []portainer.Authorizations
|
||||||
|
for _, id := range roleIdentifiers {
|
||||||
|
for _, role := range roles {
|
||||||
|
if role.ID == id {
|
||||||
|
roleAuthorizations = append(roleAuthorizations, role.Authorizations)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processedAuthorizations := make(portainer.Authorizations)
|
||||||
|
if len(roleAuthorizations) > 0 {
|
||||||
|
processedAuthorizations = roleAuthorizations[0]
|
||||||
|
for idx, authorizations := range roleAuthorizations {
|
||||||
|
if idx == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
processedAuthorizations = mergeAuthorizations(processedAuthorizations, authorizations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedAuthorizations
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeAuthorizations(a, b portainer.Authorizations) portainer.Authorizations {
|
||||||
|
c := make(map[portainer.Authorization]bool)
|
||||||
|
|
||||||
|
for k := range b {
|
||||||
|
if _, ok := a[k]; ok {
|
||||||
|
c[k] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
|
@ -30,6 +30,9 @@ type Handler struct {
|
||||||
TeamService portainer.TeamService
|
TeamService portainer.TeamService
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
ExtensionService portainer.ExtensionService
|
ExtensionService portainer.ExtensionService
|
||||||
|
EndpointService portainer.EndpointService
|
||||||
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
|
RoleService portainer.RoleService
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/dockerhub",
|
h.Handle("/dockerhub",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet)
|
||||||
h.Handle("/dockerhub",
|
h.Handle("/dockerhub",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,8 +38,8 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
|
||||||
endpointGroup := &portainer.EndpointGroup{
|
endpointGroup := &portainer.EndpointGroup{
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
Description: payload.Description,
|
Description: payload.Description,
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||||
Tags: payload.Tags,
|
Tags: payload.Tags,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ type endpointGroupUpdatePayload struct {
|
||||||
Description string
|
Description string
|
||||||
AssociatedEndpoints []portainer.EndpointID
|
AssociatedEndpoints []portainer.EndpointID
|
||||||
Tags []string
|
Tags []string
|
||||||
|
UserAccessPolicies portainer.UserAccessPolicies
|
||||||
|
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error {
|
func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error {
|
||||||
|
@ -52,11 +54,20 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
||||||
endpointGroup.Tags = payload.Tags
|
endpointGroup.Tags = payload.Tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.UserAccessPolicies != nil {
|
||||||
|
endpointGroup.UserAccessPolicies = payload.UserAccessPolicies
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.TeamAccessPolicies != nil {
|
||||||
|
endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||||
|
}
|
||||||
|
|
||||||
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.AssociatedEndpoints != nil {
|
||||||
endpoints, err := handler.EndpointService.Endpoints()
|
endpoints, err := handler.EndpointService.Endpoints()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||||
|
@ -68,6 +79,7 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response.JSON(w, endpointGroup)
|
return response.JSON(w, endpointGroup)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
package endpointgroups
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
|
||||||
"github.com/portainer/libhttp/request"
|
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type endpointGroupUpdateAccessPayload struct {
|
|
||||||
AuthorizedUsers []int
|
|
||||||
AuthorizedTeams []int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (payload *endpointGroupUpdateAccessPayload) Validate(r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUT request on /api/endpoint_groups/:id/access
|
|
||||||
func (handler *Handler) endpointGroupUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
||||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload endpointGroupUpdateAccessPayload
|
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
|
||||||
if err == portainer.ErrObjectNotFound {
|
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
|
||||||
} else if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.AuthorizedUsers != nil {
|
|
||||||
authorizedUserIDs := []portainer.UserID{}
|
|
||||||
for _, value := range payload.AuthorizedUsers {
|
|
||||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
|
||||||
}
|
|
||||||
endpointGroup.AuthorizedUsers = authorizedUserIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.AuthorizedTeams != nil {
|
|
||||||
authorizedTeamIDs := []portainer.TeamID{}
|
|
||||||
for _, value := range payload.AuthorizedTeams {
|
|
||||||
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
|
|
||||||
}
|
|
||||||
endpointGroup.AuthorizedTeams = authorizedTeamIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(w, endpointGroup)
|
|
||||||
}
|
|
|
@ -22,17 +22,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/endpoint_groups",
|
h.Handle("/endpoint_groups",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupCreate))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/endpoint_groups",
|
h.Handle("/endpoint_groups",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet)
|
||||||
h.Handle("/endpoint_groups/{id}",
|
h.Handle("/endpoint_groups/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupInspect))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupInspect))).Methods(http.MethodGet)
|
||||||
h.Handle("/endpoint_groups/{id}",
|
h.Handle("/endpoint_groups/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/endpoint_groups/{id}/access",
|
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupUpdateAccess))).Methods(http.MethodPut)
|
|
||||||
h.Handle("/endpoint_groups/{id}",
|
h.Handle("/endpoint_groups/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
requestBouncer: bouncer,
|
requestBouncer: bouncer,
|
||||||
}
|
}
|
||||||
h.PathPrefix("/{id}/azure").Handler(
|
h.PathPrefix("/{id}/azure").Handler(
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI)))
|
||||||
h.PathPrefix("/{id}/docker").Handler(
|
h.PathPrefix("/{id}/docker").Handler(
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI)))
|
||||||
h.PathPrefix("/{id}/extensions/storidge").Handler(
|
h.PathPrefix("/{id}/extensions/storidge").Handler(
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI)))
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,9 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
|
|
|
@ -28,9 +28,9 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to query endpoint", errors.New("Endpoint is down")}
|
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to query endpoint", errors.New("Endpoint is down")}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
|
|
|
@ -25,9 +25,9 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
var storidgeExtension *portainer.EndpointExtension
|
var storidgeExtension *portainer.EndpointExtension
|
||||||
|
|
|
@ -178,8 +178,8 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||||
Type: portainer.AzureEnvironment,
|
Type: portainer.AzureEnvironment,
|
||||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||||
PublicURL: payload.PublicURL,
|
PublicURL: payload.PublicURL,
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
AzureCredentials: credentials,
|
AzureCredentials: credentials,
|
||||||
Tags: payload.Tags,
|
Tags: payload.Tags,
|
||||||
|
@ -224,8 +224,8 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
||||||
TLSConfig: portainer.TLSConfiguration{
|
TLSConfig: portainer.TLSConfiguration{
|
||||||
TLS: false,
|
TLS: false,
|
||||||
},
|
},
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
Tags: payload.Tags,
|
Tags: payload.Tags,
|
||||||
Status: portainer.EndpointStatusUp,
|
Status: portainer.EndpointStatusUp,
|
||||||
|
@ -268,8 +268,8 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload)
|
||||||
TLS: payload.TLS,
|
TLS: payload.TLS,
|
||||||
TLSSkipVerify: payload.TLSSkipVerify,
|
TLSSkipVerify: payload.TLSSkipVerify,
|
||||||
},
|
},
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
Tags: payload.Tags,
|
Tags: payload.Tags,
|
||||||
Status: portainer.EndpointStatusUp,
|
Status: portainer.EndpointStatusUp,
|
||||||
|
|
|
@ -23,9 +23,9 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
hideFields(endpoint)
|
hideFields(endpoint)
|
||||||
|
|
|
@ -70,11 +70,6 @@ func (handler *Handler) endpointJob(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch method {
|
switch method {
|
||||||
case "file":
|
case "file":
|
||||||
return handler.executeJobFromFile(w, r, endpoint, nodeName)
|
return handler.executeJobFromFile(w, r, endpoint, nodeName)
|
||||||
|
|
|
@ -24,6 +24,8 @@ type endpointUpdatePayload struct {
|
||||||
AzureTenantID *string
|
AzureTenantID *string
|
||||||
AzureAuthenticationKey *string
|
AzureAuthenticationKey *string
|
||||||
Tags []string
|
Tags []string
|
||||||
|
UserAccessPolicies portainer.UserAccessPolicies
|
||||||
|
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
|
func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
|
||||||
|
@ -74,6 +76,14 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
endpoint.Tags = payload.Tags
|
endpoint.Tags = payload.Tags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.UserAccessPolicies != nil {
|
||||||
|
endpoint.UserAccessPolicies = payload.UserAccessPolicies
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.TeamAccessPolicies != nil {
|
||||||
|
endpoint.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||||
|
}
|
||||||
|
|
||||||
if payload.Status != nil {
|
if payload.Status != nil {
|
||||||
switch *payload.Status {
|
switch *payload.Status {
|
||||||
case 1:
|
case 1:
|
||||||
|
|
|
@ -1,67 +0,0 @@
|
||||||
package endpoints
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
|
||||||
"github.com/portainer/libhttp/request"
|
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type endpointUpdateAccessPayload struct {
|
|
||||||
AuthorizedUsers []int
|
|
||||||
AuthorizedTeams []int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (payload *endpointUpdateAccessPayload) Validate(r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUT request on /api/endpoints/:id/access
|
|
||||||
func (handler *Handler) endpointUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
||||||
if !handler.authorizeEndpointManagement {
|
|
||||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload endpointUpdateAccessPayload
|
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
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}
|
|
||||||
} else if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.AuthorizedUsers != nil {
|
|
||||||
authorizedUserIDs := []portainer.UserID{}
|
|
||||||
for _, value := range payload.AuthorizedUsers {
|
|
||||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
|
||||||
}
|
|
||||||
endpoint.AuthorizedUsers = authorizedUserIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.AuthorizedTeams != nil {
|
|
||||||
authorizedTeamIDs := []portainer.TeamID{}
|
|
||||||
for _, value := range payload.AuthorizedTeams {
|
|
||||||
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
|
|
||||||
}
|
|
||||||
endpoint.AuthorizedTeams = authorizedTeamIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(w, endpoint)
|
|
||||||
}
|
|
|
@ -43,26 +43,24 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Handle("/endpoints",
|
h.Handle("/endpoints",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/endpoints/snapshot",
|
h.Handle("/endpoints/snapshot",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
|
||||||
h.Handle("/endpoints",
|
h.Handle("/endpoints",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
|
||||||
h.Handle("/endpoints/{id}",
|
h.Handle("/endpoints/{id}",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
|
||||||
h.Handle("/endpoints/{id}",
|
h.Handle("/endpoints/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/endpoints/{id}/access",
|
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointUpdateAccess))).Methods(http.MethodPut)
|
|
||||||
h.Handle("/endpoints/{id}",
|
h.Handle("/endpoints/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
|
||||||
h.Handle("/endpoints/{id}/extensions",
|
h.Handle("/endpoints/{id}/extensions",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
|
||||||
h.Handle("/endpoints/{id}/extensions/{extensionType}",
|
h.Handle("/endpoints/{id}/extensions/{extensionType}",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
|
||||||
h.Handle("/endpoints/{id}/job",
|
h.Handle("/endpoints/{id}/job",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
|
||||||
h.Handle("/endpoints/{id}/snapshot",
|
h.Handle("/endpoints/{id}/snapshot",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,6 +70,13 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request)
|
||||||
|
|
||||||
extension.Enabled = true
|
extension.Enabled = true
|
||||||
|
|
||||||
|
if extension.ID == portainer.RBACExtension {
|
||||||
|
err = handler.upgradeRBACData()
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = handler.ExtensionService.Persist(extension)
|
err = handler.ExtensionService.Persist(extension)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
||||||
|
|
|
@ -14,6 +14,9 @@ type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
ExtensionService portainer.ExtensionService
|
ExtensionService portainer.ExtensionService
|
||||||
ExtensionManager portainer.ExtensionManager
|
ExtensionManager portainer.ExtensionManager
|
||||||
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
|
EndpointService portainer.EndpointService
|
||||||
|
RegistryService portainer.RegistryService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage extension operations.
|
// NewHandler creates a handler to manage extension operations.
|
||||||
|
@ -23,15 +26,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Handle("/extensions",
|
h.Handle("/extensions",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
||||||
h.Handle("/extensions",
|
h.Handle("/extensions",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/extensions/{id}",
|
h.Handle("/extensions/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet)
|
||||||
h.Handle("/extensions/{id}",
|
h.Handle("/extensions/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionDelete))).Methods(http.MethodDelete)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionDelete))).Methods(http.MethodDelete)
|
||||||
h.Handle("/extensions/{id}/update",
|
h.Handle("/extensions/{id}/update",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package extensions
|
||||||
|
|
||||||
|
import portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
func updateUserAccessPolicyToReadOnlyRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
|
||||||
|
tmp := policies[key]
|
||||||
|
tmp.RoleID = 4
|
||||||
|
policies[key] = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateTeamAccessPolicyToReadOnlyRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) {
|
||||||
|
tmp := policies[key]
|
||||||
|
tmp.RoleID = 4
|
||||||
|
policies[key] = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) upgradeRBACData() error {
|
||||||
|
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpointGroup := range endpointGroups {
|
||||||
|
for key := range endpointGroup.UserAccessPolicies {
|
||||||
|
updateUserAccessPolicyToReadOnlyRole(endpointGroup.UserAccessPolicies, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range endpointGroup.TeamAccessPolicies {
|
||||||
|
updateTeamAccessPolicyToReadOnlyRole(endpointGroup.TeamAccessPolicies, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints, err := handler.EndpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
for key := range endpoint.UserAccessPolicies {
|
||||||
|
updateUserAccessPolicyToReadOnlyRole(endpoint.UserAccessPolicies, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range endpoint.TeamAccessPolicies {
|
||||||
|
updateTeamAccessPolicyToReadOnlyRole(endpoint.TeamAccessPolicies, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -4,6 +4,10 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/handler/schedules"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/handler/roles"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/http/handler/auth"
|
"github.com/portainer/portainer/api/http/handler/auth"
|
||||||
"github.com/portainer/portainer/api/http/handler/dockerhub"
|
"github.com/portainer/portainer/api/http/handler/dockerhub"
|
||||||
"github.com/portainer/portainer/api/http/handler/endpointgroups"
|
"github.com/portainer/portainer/api/http/handler/endpointgroups"
|
||||||
|
@ -14,7 +18,6 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/handler/motd"
|
"github.com/portainer/portainer/api/http/handler/motd"
|
||||||
"github.com/portainer/portainer/api/http/handler/registries"
|
"github.com/portainer/portainer/api/http/handler/registries"
|
||||||
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
|
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
|
||||||
"github.com/portainer/portainer/api/http/handler/schedules"
|
|
||||||
"github.com/portainer/portainer/api/http/handler/settings"
|
"github.com/portainer/portainer/api/http/handler/settings"
|
||||||
"github.com/portainer/portainer/api/http/handler/stacks"
|
"github.com/portainer/portainer/api/http/handler/stacks"
|
||||||
"github.com/portainer/portainer/api/http/handler/status"
|
"github.com/portainer/portainer/api/http/handler/status"
|
||||||
|
@ -31,7 +34,6 @@ import (
|
||||||
// Handler is a collection of all the service handlers.
|
// Handler is a collection of all the service handlers.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
AuthHandler *auth.Handler
|
AuthHandler *auth.Handler
|
||||||
|
|
||||||
DockerHubHandler *dockerhub.Handler
|
DockerHubHandler *dockerhub.Handler
|
||||||
EndpointGroupHandler *endpointgroups.Handler
|
EndpointGroupHandler *endpointgroups.Handler
|
||||||
EndpointHandler *endpoints.Handler
|
EndpointHandler *endpoints.Handler
|
||||||
|
@ -41,6 +43,8 @@ type Handler struct {
|
||||||
ExtensionHandler *extensions.Handler
|
ExtensionHandler *extensions.Handler
|
||||||
RegistryHandler *registries.Handler
|
RegistryHandler *registries.Handler
|
||||||
ResourceControlHandler *resourcecontrols.Handler
|
ResourceControlHandler *resourcecontrols.Handler
|
||||||
|
RoleHandler *roles.Handler
|
||||||
|
SchedulesHanlder *schedules.Handler
|
||||||
SettingsHandler *settings.Handler
|
SettingsHandler *settings.Handler
|
||||||
StackHandler *stacks.Handler
|
StackHandler *stacks.Handler
|
||||||
StatusHandler *status.Handler
|
StatusHandler *status.Handler
|
||||||
|
@ -52,7 +56,6 @@ type Handler struct {
|
||||||
UserHandler *users.Handler
|
UserHandler *users.Handler
|
||||||
WebSocketHandler *websocket.Handler
|
WebSocketHandler *websocket.Handler
|
||||||
WebhookHandler *webhooks.Handler
|
WebhookHandler *webhooks.Handler
|
||||||
SchedulesHanlder *schedules.Handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||||
|
@ -75,14 +78,18 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
default:
|
default:
|
||||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/motd"):
|
|
||||||
http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r)
|
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/extensions"):
|
case strings.HasPrefix(r.URL.Path, "/api/extensions"):
|
||||||
http.StripPrefix("/api", h.ExtensionHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.ExtensionHandler).ServeHTTP(w, r)
|
||||||
|
case strings.HasPrefix(r.URL.Path, "/api/motd"):
|
||||||
|
http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
||||||
http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/resource_controls"):
|
case strings.HasPrefix(r.URL.Path, "/api/resource_controls"):
|
||||||
http.StripPrefix("/api", h.ResourceControlHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.ResourceControlHandler).ServeHTTP(w, r)
|
||||||
|
case strings.HasPrefix(r.URL.Path, "/api/roles"):
|
||||||
|
http.StripPrefix("/api", h.RoleHandler).ServeHTTP(w, r)
|
||||||
|
case strings.HasPrefix(r.URL.Path, "/api/schedules"):
|
||||||
|
http.StripPrefix("/api", h.SchedulesHanlder).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/settings"):
|
case strings.HasPrefix(r.URL.Path, "/api/settings"):
|
||||||
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/stacks"):
|
case strings.HasPrefix(r.URL.Path, "/api/stacks"):
|
||||||
|
@ -105,8 +112,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/webhooks"):
|
case strings.HasPrefix(r.URL.Path, "/api/webhooks"):
|
||||||
http.StripPrefix("/api", h.WebhookHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.WebhookHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/schedules"):
|
|
||||||
http.StripPrefix("/api", h.SchedulesHanlder).ServeHTTP(w, r)
|
|
||||||
case strings.HasPrefix(r.URL.Path, "/"):
|
case strings.HasPrefix(r.URL.Path, "/"):
|
||||||
h.FileHandler.ServeHTTP(w, r)
|
h.FileHandler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/motd",
|
h.Handle("/motd",
|
||||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,19 +33,17 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Handle("/registries",
|
h.Handle("/registries",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/registries",
|
h.Handle("/registries",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet)
|
||||||
h.Handle("/registries/{id}",
|
h.Handle("/registries/{id}",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
|
||||||
h.Handle("/registries/{id}",
|
h.Handle("/registries/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/registries/{id}/access",
|
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdateAccess))).Methods(http.MethodPut)
|
|
||||||
h.Handle("/registries/{id}/configure",
|
h.Handle("/registries/{id}/configure",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost)
|
||||||
h.Handle("/registries/{id}",
|
h.Handle("/registries/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
||||||
h.PathPrefix("/registries/{id}/v2").Handler(
|
h.PathPrefix("/registries/{id}/v2").Handler(
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
|
@ -24,6 +26,15 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !securityContext.IsAdmin {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||||
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.RegistryAccess(r, registry)
|
err = handler.requestBouncer.RegistryAccess(r, registry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied}
|
||||||
|
|
|
@ -59,8 +59,8 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
|
||||||
Authentication: payload.Authentication,
|
Authentication: payload.Authentication,
|
||||||
Username: payload.Username,
|
Username: payload.Username,
|
||||||
Password: payload.Password,
|
Password: payload.Password,
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.RegistryService.CreateRegistry(registry)
|
err = handler.RegistryService.CreateRegistry(registry)
|
||||||
|
|
|
@ -16,6 +16,8 @@ type registryUpdatePayload struct {
|
||||||
Authentication bool
|
Authentication bool
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
UserAccessPolicies portainer.UserAccessPolicies
|
||||||
|
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *registryUpdatePayload) Validate(r *http.Request) error {
|
func (payload *registryUpdatePayload) Validate(r *http.Request) error {
|
||||||
|
@ -73,6 +75,14 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
registry.Password = ""
|
registry.Password = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.UserAccessPolicies != nil {
|
||||||
|
registry.UserAccessPolicies = payload.UserAccessPolicies
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.TeamAccessPolicies != nil {
|
||||||
|
registry.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||||
|
}
|
||||||
|
|
||||||
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
|
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err}
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
package registries
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
|
||||||
"github.com/portainer/libhttp/request"
|
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type registryUpdateAccessPayload struct {
|
|
||||||
AuthorizedUsers []int
|
|
||||||
AuthorizedTeams []int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (payload *registryUpdateAccessPayload) Validate(r *http.Request) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUT request on /api/registries/:id/access
|
|
||||||
func (handler *Handler) registryUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
|
||||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload registryUpdateAccessPayload
|
|
||||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
|
||||||
if err == portainer.ErrObjectNotFound {
|
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
|
|
||||||
} else if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.AuthorizedUsers != nil {
|
|
||||||
authorizedUserIDs := []portainer.UserID{}
|
|
||||||
for _, value := range payload.AuthorizedUsers {
|
|
||||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
|
||||||
}
|
|
||||||
registry.AuthorizedUsers = authorizedUserIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.AuthorizedTeams != nil {
|
|
||||||
authorizedTeamIDs := []portainer.TeamID{}
|
|
||||||
for _, value := range payload.AuthorizedTeams {
|
|
||||||
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
|
|
||||||
}
|
|
||||||
registry.AuthorizedTeams = authorizedTeamIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(w, registry)
|
|
||||||
}
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package roles
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler is the HTTP handler used to handle role operations.
|
||||||
|
type Handler struct {
|
||||||
|
*mux.Router
|
||||||
|
RoleService portainer.RoleService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler creates a handler to manage role operations.
|
||||||
|
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
|
h := &Handler{
|
||||||
|
Router: mux.NewRouter(),
|
||||||
|
}
|
||||||
|
h.Handle("/roles",
|
||||||
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.roleList))).Methods(http.MethodGet)
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package roles
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET request on /api/Role
|
||||||
|
func (handler *Handler) roleList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
roles, err := handler.RoleService.Roles()
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve authorization sets from the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, roles)
|
||||||
|
}
|
|
@ -27,18 +27,18 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Handle("/schedules",
|
h.Handle("/schedules",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleList))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleList))).Methods(http.MethodGet)
|
||||||
h.Handle("/schedules",
|
h.Handle("/schedules",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleCreate))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/schedules/{id}",
|
h.Handle("/schedules/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleInspect))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleInspect))).Methods(http.MethodGet)
|
||||||
h.Handle("/schedules/{id}",
|
h.Handle("/schedules/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleUpdate))).Methods(http.MethodPut)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/schedules/{id}",
|
h.Handle("/schedules/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete)
|
||||||
h.Handle("/schedules/{id}/file",
|
h.Handle("/schedules/{id}/file",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleFile))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleFile))).Methods(http.MethodGet)
|
||||||
h.Handle("/schedules/{id}/tasks",
|
h.Handle("/schedules/{id}/tasks",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,13 +30,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/settings",
|
h.Handle("/settings",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet)
|
||||||
h.Handle("/settings",
|
h.Handle("/settings",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.settingsUpdate))).Methods(http.MethodPut)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.settingsUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/settings/public",
|
h.Handle("/settings/public",
|
||||||
bouncer.PublicAccess(httperror.LoggerHandler(h.settingsPublic))).Methods(http.MethodGet)
|
bouncer.PublicAccess(httperror.LoggerHandler(h.settingsPublic))).Methods(http.MethodGet)
|
||||||
h.Handle("/settings/authentication/checkLDAP",
|
h.Handle("/settings/authentication/checkLDAP",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.settingsLDAPCheck))).Methods(http.MethodPut)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.settingsLDAPCheck))).Methods(http.MethodPut)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,9 +46,9 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch portainer.StackType(stackType) {
|
switch portainer.StackType(stackType) {
|
||||||
|
|
|
@ -4,12 +4,13 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DELETE request on /api/stacks/:id?external=<external>&endpointId=<endpointId>
|
// DELETE request on /api/stacks/:id?external=<external>&endpointId=<endpointId>
|
||||||
|
@ -38,22 +39,6 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
|
||||||
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
||||||
// The EndpointID property is not available for these stacks, this API endpoint
|
// The EndpointID property is not available for these stacks, this API endpoint
|
||||||
// can use the optional EndpointID query parameter to set a valid endpoint identifier to be
|
// can use the optional EndpointID query parameter to set a valid endpoint identifier to be
|
||||||
|
@ -74,6 +59,27 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||||
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !securityContext.IsAdmin {
|
||||||
|
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = handler.deleteStack(stack, endpoint)
|
err = handler.deleteStack(stack, endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||||
|
@ -113,9 +119,9 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
stack = &portainer.Stack{
|
stack = &portainer.Stack{
|
||||||
|
|
|
@ -30,6 +30,18 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID)
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
|
|
|
@ -25,6 +25,18 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID)
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
|
|
|
@ -44,6 +44,18 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID)
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
|
@ -71,13 +83,6 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||||
stack.EndpointID = portainer.EndpointID(endpointID)
|
stack.EndpointID = portainer.EndpointID(endpointID)
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID)
|
|
||||||
if err == portainer.ErrObjectNotFound {
|
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err}
|
|
||||||
} else if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
targetEndpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(payload.EndpointID))
|
targetEndpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(payload.EndpointID))
|
||||||
if err == portainer.ErrObjectNotFound {
|
if err == portainer.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
|
|
|
@ -4,13 +4,14 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type updateComposeStackPayload struct {
|
type updateComposeStackPayload struct {
|
||||||
|
@ -52,22 +53,6 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
|
||||||
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
||||||
// The EndpointID property is not available for these stacks, this API endpoint
|
// The EndpointID property is not available for these stacks, this API endpoint
|
||||||
// can use the optional EndpointID query parameter to associate a valid endpoint identifier to the stack.
|
// can use the optional EndpointID query parameter to associate a valid endpoint identifier to the stack.
|
||||||
|
@ -86,6 +71,27 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||||
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !securityContext.IsAdmin {
|
||||||
|
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateError := handler.updateAndDeployStack(r, stack, endpoint)
|
updateError := handler.updateAndDeployStack(r, stack, endpoint)
|
||||||
if updateError != nil {
|
if updateError != nil {
|
||||||
return updateError
|
return updateError
|
||||||
|
|
|
@ -21,11 +21,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/tags",
|
h.Handle("/tags",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/tags",
|
h.Handle("/tags",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet)
|
||||||
h.Handle("/tags/{id}",
|
h.Handle("/tags/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,13 +23,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/team_memberships",
|
h.Handle("/team_memberships",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipCreate))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/team_memberships",
|
h.Handle("/team_memberships",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet)
|
||||||
h.Handle("/team_memberships/{id}",
|
h.Handle("/team_memberships/{id}",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipUpdate))).Methods(http.MethodPut)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/team_memberships/{id}",
|
h.Handle("/team_memberships/{id}",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,17 +23,17 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/teams",
|
h.Handle("/teams",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/teams",
|
h.Handle("/teams",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamList))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamList))).Methods(http.MethodGet)
|
||||||
h.Handle("/teams/{id}",
|
h.Handle("/teams/{id}",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet)
|
||||||
h.Handle("/teams/{id}",
|
h.Handle("/teams/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.teamUpdate))).Methods(http.MethodPut)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/teams/{id}",
|
h.Handle("/teams/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete)
|
||||||
h.Handle("/teams/{id}/memberships",
|
h.Handle("/teams/{id}/memberships",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMemberships))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMemberships))).Methods(http.MethodGet)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,15 +27,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Handle("/templates",
|
h.Handle("/templates",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet)
|
||||||
h.Handle("/templates",
|
h.Handle("/templates",
|
||||||
bouncer.AdministratorAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateCreate)))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateCreate)))).Methods(http.MethodPost)
|
||||||
h.Handle("/templates/{id}",
|
h.Handle("/templates/{id}",
|
||||||
bouncer.AdministratorAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateInspect)))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateInspect)))).Methods(http.MethodGet)
|
||||||
h.Handle("/templates/{id}",
|
h.Handle("/templates/{id}",
|
||||||
bouncer.AdministratorAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateUpdate)))).Methods(http.MethodPut)
|
bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateUpdate)))).Methods(http.MethodPut)
|
||||||
h.Handle("/templates/{id}",
|
h.Handle("/templates/{id}",
|
||||||
bouncer.AdministratorAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateDelete)))).Methods(http.MethodDelete)
|
bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateDelete)))).Methods(http.MethodDelete)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/upload/tls/{certificate:(?:ca|cert|key)}",
|
h.Handle("/upload/tls/{certificate:(?:ca|cert|key)}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.uploadTLS))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.uploadTLS))).Methods(http.MethodPost)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,19 +31,19 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/users",
|
h.Handle("/users",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/users",
|
h.Handle("/users",
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userList))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userList))).Methods(http.MethodGet)
|
||||||
h.Handle("/users/{id}",
|
h.Handle("/users/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet)
|
||||||
h.Handle("/users/{id}",
|
h.Handle("/users/{id}",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdate))).Methods(http.MethodPut)
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/users/{id}",
|
h.Handle("/users/{id}",
|
||||||
bouncer.AdministratorAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete)
|
||||||
h.Handle("/users/{id}/memberships",
|
h.Handle("/users/{id}/memberships",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userMemberships))).Methods(http.MethodGet)
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.userMemberships))).Methods(http.MethodGet)
|
||||||
h.Handle("/users/{id}/passwd",
|
h.Handle("/users/{id}/passwd",
|
||||||
rateLimiter.LimitAccess(bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdatePassword)))).Methods(http.MethodPut)
|
rateLimiter.LimitAccess(bouncer.RestrictedAccess(httperror.LoggerHandler(h.userUpdatePassword)))).Methods(http.MethodPut)
|
||||||
h.Handle("/users/admin/check",
|
h.Handle("/users/admin/check",
|
||||||
bouncer.PublicAccess(httperror.LoggerHandler(h.adminCheck))).Methods(http.MethodGet)
|
bouncer.PublicAccess(httperror.LoggerHandler(h.adminCheck))).Methods(http.MethodGet)
|
||||||
h.Handle("/users/admin/init",
|
h.Handle("/users/admin/init",
|
||||||
|
|
|
@ -60,6 +60,23 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
|
||||||
user = &portainer.User{
|
user = &portainer.User{
|
||||||
Username: payload.Username,
|
Username: payload.Username,
|
||||||
Role: portainer.UserRole(payload.Role),
|
Role: portainer.UserRole(payload.Role),
|
||||||
|
PortainerAuthorizations: map[portainer.Authorization]bool{
|
||||||
|
portainer.OperationPortainerDockerHubInspect: true,
|
||||||
|
portainer.OperationPortainerEndpointGroupList: true,
|
||||||
|
portainer.OperationPortainerEndpointList: true,
|
||||||
|
portainer.OperationPortainerEndpointInspect: true,
|
||||||
|
portainer.OperationPortainerEndpointExtensionAdd: true,
|
||||||
|
portainer.OperationPortainerEndpointExtensionRemove: true,
|
||||||
|
portainer.OperationPortainerExtensionList: true,
|
||||||
|
portainer.OperationPortainerMOTD: true,
|
||||||
|
portainer.OperationPortainerRegistryList: true,
|
||||||
|
portainer.OperationPortainerRegistryInspect: true,
|
||||||
|
portainer.OperationPortainerTeamList: true,
|
||||||
|
portainer.OperationPortainerTemplateList: true,
|
||||||
|
portainer.OperationPortainerTemplateInspect: true,
|
||||||
|
portainer.OperationPortainerUserList: true,
|
||||||
|
portainer.OperationPortainerUserMemberships: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
settings, err := handler.SettingsService.Settings()
|
settings, err := handler.SettingsService.Settings()
|
||||||
|
|
|
@ -24,11 +24,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/webhooks",
|
h.Handle("/webhooks",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost)
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/webhooks",
|
h.Handle("/webhooks",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookList))).Methods(http.MethodGet)
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookList))).Methods(http.MethodGet)
|
||||||
h.Handle("/webhooks/{id}",
|
h.Handle("/webhooks/{id}",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookDelete))).Methods(http.MethodDelete)
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookDelete))).Methods(http.MethodDelete)
|
||||||
h.Handle("/webhooks/{token}",
|
h.Handle("/webhooks/{token}",
|
||||||
bouncer.PublicAccess(httperror.LoggerHandler(h.webhookExecute))).Methods(http.MethodPost)
|
bouncer.PublicAccess(httperror.LoggerHandler(h.webhookExecute))).Methods(http.MethodPost)
|
||||||
return h
|
return h
|
||||||
|
|
|
@ -39,9 +39,9 @@ func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request)
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
params := &webSocketRequestParams{
|
params := &webSocketRequestParams{
|
||||||
|
|
|
@ -46,9 +46,9 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.requestBouncer.EndpointAccess(r, endpoint)
|
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
params := &webSocketRequestParams{
|
params := &webSocketRequestParams{
|
||||||
|
|
|
@ -25,8 +25,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
requestBouncer: bouncer,
|
requestBouncer: bouncer,
|
||||||
}
|
}
|
||||||
h.PathPrefix("/websocket/exec").Handler(
|
h.PathPrefix("/websocket/exec").Handler(
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec)))
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.websocketExec)))
|
||||||
h.PathPrefix("/websocket/attach").Handler(
|
h.PathPrefix("/websocket/attach").Handler(
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketAttach)))
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.websocketAttach)))
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ type (
|
||||||
// Returns the original object and denied access (false) when no resource control is found.
|
// Returns the original object and denied access (false) when no resource control is found.
|
||||||
// Returns the original object and denied access (false) when a resource control is found and the user cannot access the resource.
|
// Returns the original object and denied access (false) when a resource control is found and the user cannot access the resource.
|
||||||
func applyResourceAccessControlFromLabel(labelsObject, resourceObject map[string]interface{}, labelIdentifier string,
|
func applyResourceAccessControlFromLabel(labelsObject, resourceObject map[string]interface{}, labelIdentifier string,
|
||||||
context *restrictedOperationContext) (map[string]interface{}, bool) {
|
context *restrictedDockerOperationContext) (map[string]interface{}, bool) {
|
||||||
|
|
||||||
if labelsObject != nil && labelsObject[labelIdentifier] != nil {
|
if labelsObject != nil && labelsObject[labelIdentifier] != nil {
|
||||||
resourceIdentifier := labelsObject[labelIdentifier].(string)
|
resourceIdentifier := labelsObject[labelIdentifier].(string)
|
||||||
|
@ -38,14 +38,14 @@ func applyResourceAccessControlFromLabel(labelsObject, resourceObject map[string
|
||||||
// Returns the original object and denied access (false) when a resource control is associated to the resource
|
// Returns the original object and denied access (false) when a resource control is associated to the resource
|
||||||
// and the user cannot access the resource.
|
// and the user cannot access the resource.
|
||||||
func applyResourceAccessControl(resourceObject map[string]interface{}, resourceIdentifier string,
|
func applyResourceAccessControl(resourceObject map[string]interface{}, resourceIdentifier string,
|
||||||
context *restrictedOperationContext) (map[string]interface{}, bool) {
|
context *restrictedDockerOperationContext) (map[string]interface{}, bool) {
|
||||||
|
|
||||||
resourceControl := getResourceControlByResourceID(resourceIdentifier, context.resourceControls)
|
resourceControl := getResourceControlByResourceID(resourceIdentifier, context.resourceControls)
|
||||||
if resourceControl == nil {
|
if resourceControl == nil {
|
||||||
return resourceObject, context.isAdmin
|
return resourceObject, context.isAdmin || context.endpointResourceAccess
|
||||||
}
|
}
|
||||||
|
|
||||||
if context.isAdmin || resourceControl.Public || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) {
|
if context.isAdmin || context.endpointResourceAccess || resourceControl.Public || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) {
|
||||||
resourceObject = decorateObject(resourceObject, resourceControl)
|
resourceObject = decorateObject(resourceObject, resourceControl)
|
||||||
return resourceObject, true
|
return resourceObject, true
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ func configListOperation(response *http.Response, executor *operationExecutor) e
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if executor.operationContext.isAdmin {
|
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
||||||
responseArray, err = decorateConfigList(responseArray, executor.operationContext.resourceControls)
|
responseArray, err = decorateConfigList(responseArray, executor.operationContext.resourceControls)
|
||||||
} else {
|
} else {
|
||||||
responseArray, err = filterConfigList(responseArray, executor.operationContext)
|
responseArray, err = filterConfigList(responseArray, executor.operationContext)
|
||||||
|
@ -87,7 +87,7 @@ func decorateConfigList(configData []interface{}, resourceControls []portainer.R
|
||||||
// Authorized configs are decorated during the process.
|
// Authorized configs are decorated during the process.
|
||||||
// Resource controls checks are based on: resource identifier.
|
// Resource controls checks are based on: resource identifier.
|
||||||
// Config object schema reference: https://docs.docker.com/engine/api/v1.30/#operation/ConfigList
|
// Config object schema reference: https://docs.docker.com/engine/api/v1.30/#operation/ConfigList
|
||||||
func filterConfigList(configData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
func filterConfigList(configData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
||||||
filteredConfigData := make([]interface{}, 0)
|
filteredConfigData := make([]interface{}, 0)
|
||||||
|
|
||||||
for _, config := range configData {
|
for _, config := range configData {
|
||||||
|
|
|
@ -26,7 +26,7 @@ func containerListOperation(response *http.Response, executor *operationExecutor
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if executor.operationContext.isAdmin {
|
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
||||||
responseArray, err = decorateContainerList(responseArray, executor.operationContext.resourceControls)
|
responseArray, err = decorateContainerList(responseArray, executor.operationContext.resourceControls)
|
||||||
} else {
|
} else {
|
||||||
responseArray, err = filterContainerList(responseArray, executor.operationContext)
|
responseArray, err = filterContainerList(responseArray, executor.operationContext)
|
||||||
|
@ -137,7 +137,7 @@ func decorateContainerList(containerData []interface{}, resourceControls []porta
|
||||||
// Authorized containers are decorated during the process.
|
// Authorized containers are decorated during the process.
|
||||||
// Resource controls checks are based on: resource identifier, service identifier (from label), stack identifier (from label).
|
// Resource controls checks are based on: resource identifier, service identifier (from label), stack identifier (from label).
|
||||||
// Container object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
// Container object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||||
func filterContainerList(containerData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
func filterContainerList(containerData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
||||||
filteredContainerData := make([]interface{}, 0)
|
filteredContainerData := make([]interface{}, 0)
|
||||||
|
|
||||||
for _, container := range containerData {
|
for _, container := range containerData {
|
||||||
|
|
|
@ -24,9 +24,11 @@ type (
|
||||||
DockerHubService portainer.DockerHubService
|
DockerHubService portainer.DockerHubService
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
endpointIdentifier portainer.EndpointID
|
||||||
}
|
}
|
||||||
restrictedOperationContext struct {
|
restrictedDockerOperationContext struct {
|
||||||
isAdmin bool
|
isAdmin bool
|
||||||
|
endpointResourceAccess bool
|
||||||
userID portainer.UserID
|
userID portainer.UserID
|
||||||
userTeamIDs []portainer.TeamID
|
userTeamIDs []portainer.TeamID
|
||||||
resourceControls []portainer.ResourceControl
|
resourceControls []portainer.ResourceControl
|
||||||
|
@ -44,7 +46,7 @@ type (
|
||||||
Serveraddress string `json:"serveraddress"`
|
Serveraddress string `json:"serveraddress"`
|
||||||
}
|
}
|
||||||
operationExecutor struct {
|
operationExecutor struct {
|
||||||
operationContext *restrictedOperationContext
|
operationContext *restrictedDockerOperationContext
|
||||||
labelBlackList []portainer.Pair
|
labelBlackList []portainer.Pair
|
||||||
}
|
}
|
||||||
restrictedOperationRequest func(*http.Response, *operationExecutor) error
|
restrictedOperationRequest func(*http.Response, *operationExecutor) error
|
||||||
|
@ -460,7 +462,7 @@ func (p *proxyTransport) createRegistryAccessContext(request *http.Request) (*re
|
||||||
return accessContext, nil
|
return accessContext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedOperationContext, error) {
|
func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedDockerOperationContext, error) {
|
||||||
var err error
|
var err error
|
||||||
tokenData, err := security.RetrieveTokenData(request)
|
tokenData, err := security.RetrieveTokenData(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -472,15 +474,21 @@ func (p *proxyTransport) createOperationContext(request *http.Request) (*restric
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
operationContext := &restrictedOperationContext{
|
operationContext := &restrictedDockerOperationContext{
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
userID: tokenData.ID,
|
userID: tokenData.ID,
|
||||||
resourceControls: resourceControls,
|
resourceControls: resourceControls,
|
||||||
|
endpointResourceAccess: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole {
|
if tokenData.Role != portainer.AdministratorRole {
|
||||||
operationContext.isAdmin = false
|
operationContext.isAdmin = false
|
||||||
|
|
||||||
|
_, ok := tokenData.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess]
|
||||||
|
if ok {
|
||||||
|
operationContext.endpointResourceAccess = true
|
||||||
|
}
|
||||||
|
|
||||||
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -40,10 +40,10 @@ func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, enableSignature bool) (http.Handler, error) {
|
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, enableSignature bool, endpointID portainer.EndpointID) (http.Handler, error) {
|
||||||
u.Scheme = "https"
|
u.Scheme = "https"
|
||||||
|
|
||||||
proxy := factory.createDockerReverseProxy(u, enableSignature)
|
proxy := factory.createDockerReverseProxy(u, enableSignature, endpointID)
|
||||||
config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify)
|
config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -53,12 +53,12 @@ func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portaine
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, enableSignature bool) http.Handler {
|
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, enableSignature bool, endpointID portainer.EndpointID) http.Handler {
|
||||||
u.Scheme = "http"
|
u.Scheme = "http"
|
||||||
return factory.createDockerReverseProxy(u, enableSignature)
|
return factory.createDockerReverseProxy(u, enableSignature, endpointID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignature bool) *httputil.ReverseProxy {
|
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignature bool, endpointID portainer.EndpointID) *httputil.ReverseProxy {
|
||||||
proxy := newSingleHostReverseProxyWithHostHeader(u)
|
proxy := newSingleHostReverseProxyWithHostHeader(u)
|
||||||
transport := &proxyTransport{
|
transport := &proxyTransport{
|
||||||
enableSignature: enableSignature,
|
enableSignature: enableSignature,
|
||||||
|
@ -68,6 +68,7 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignatur
|
||||||
RegistryService: factory.RegistryService,
|
RegistryService: factory.RegistryService,
|
||||||
DockerHubService: factory.DockerHubService,
|
DockerHubService: factory.DockerHubService,
|
||||||
dockerTransport: &http.Transport{},
|
dockerTransport: &http.Transport{},
|
||||||
|
endpointIdentifier: endpointID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if enableSignature {
|
if enableSignature {
|
||||||
|
|
|
@ -4,9 +4,11 @@ package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (factory *proxyFactory) newLocalProxy(path string) http.Handler {
|
func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.EndpointID) http.Handler {
|
||||||
proxy := &localProxy{}
|
proxy := &localProxy{}
|
||||||
transport := &proxyTransport{
|
transport := &proxyTransport{
|
||||||
enableSignature: false,
|
enableSignature: false,
|
||||||
|
@ -16,6 +18,7 @@ func (factory *proxyFactory) newLocalProxy(path string) http.Handler {
|
||||||
RegistryService: factory.RegistryService,
|
RegistryService: factory.RegistryService,
|
||||||
DockerHubService: factory.DockerHubService,
|
DockerHubService: factory.DockerHubService,
|
||||||
dockerTransport: newSocketTransport(path),
|
dockerTransport: newSocketTransport(path),
|
||||||
|
endpointIdentifier: endpointID,
|
||||||
}
|
}
|
||||||
proxy.Transport = transport
|
proxy.Transport = transport
|
||||||
return proxy
|
return proxy
|
||||||
|
|
|
@ -6,10 +6,10 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/Microsoft/go-winio"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (factory *proxyFactory) newLocalProxy(path string) http.Handler {
|
func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.EndpointID) http.Handler {
|
||||||
proxy := &localProxy{}
|
proxy := &localProxy{}
|
||||||
transport := &proxyTransport{
|
transport := &proxyTransport{
|
||||||
enableSignature: false,
|
enableSignature: false,
|
||||||
|
@ -19,6 +19,7 @@ func (factory *proxyFactory) newLocalProxy(path string) http.Handler {
|
||||||
RegistryService: factory.RegistryService,
|
RegistryService: factory.RegistryService,
|
||||||
DockerHubService: factory.DockerHubService,
|
DockerHubService: factory.DockerHubService,
|
||||||
dockerTransport: newNamedPipeTransport(path),
|
dockerTransport: newNamedPipeTransport(path),
|
||||||
|
endpointIdentifier: endpointID,
|
||||||
}
|
}
|
||||||
proxy.Transport = transport
|
proxy.Transport = transport
|
||||||
return proxy
|
return proxy
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
var extensionPorts = map[portainer.ExtensionID]string{
|
var extensionPorts = map[portainer.ExtensionID]string{
|
||||||
portainer.RegistryManagementExtension: "7001",
|
portainer.RegistryManagementExtension: "7001",
|
||||||
portainer.OAuthAuthenticationExtension: "7002",
|
portainer.OAuthAuthenticationExtension: "7002",
|
||||||
|
portainer.RBACExtension: "7003",
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -135,14 +136,14 @@ func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string)
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration) (http.Handler, error) {
|
func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration, endpointID portainer.EndpointID) (http.Handler, error) {
|
||||||
if endpointURL.Scheme == "tcp" {
|
if endpointURL.Scheme == "tcp" {
|
||||||
if tlsConfig.TLS || tlsConfig.TLSSkipVerify {
|
if tlsConfig.TLS || tlsConfig.TLSSkipVerify {
|
||||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, tlsConfig, false)
|
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, tlsConfig, false, endpointID)
|
||||||
}
|
}
|
||||||
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false), nil
|
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false, endpointID), nil
|
||||||
}
|
}
|
||||||
return manager.proxyFactory.newLocalProxy(endpointURL.Path), nil
|
return manager.proxyFactory.newLocalProxy(endpointURL.Path, endpointID), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
|
@ -153,10 +154,10 @@ func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler,
|
||||||
|
|
||||||
switch endpoint.Type {
|
switch endpoint.Type {
|
||||||
case portainer.AgentOnDockerEnvironment:
|
case portainer.AgentOnDockerEnvironment:
|
||||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true)
|
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true, endpoint.ID)
|
||||||
case portainer.AzureEnvironment:
|
case portainer.AzureEnvironment:
|
||||||
return newAzureProxy(&endpoint.AzureCredentials)
|
return newAzureProxy(&endpoint.AzureCredentials)
|
||||||
default:
|
default:
|
||||||
return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig)
|
return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig, endpoint.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ func networkListOperation(response *http.Response, executor *operationExecutor)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if executor.operationContext.isAdmin {
|
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
||||||
responseArray, err = decorateNetworkList(responseArray, executor.operationContext.resourceControls)
|
responseArray, err = decorateNetworkList(responseArray, executor.operationContext.resourceControls)
|
||||||
} else {
|
} else {
|
||||||
responseArray, err = filterNetworkList(responseArray, executor.operationContext)
|
responseArray, err = filterNetworkList(responseArray, executor.operationContext)
|
||||||
|
@ -110,7 +110,7 @@ func decorateNetworkList(networkData []interface{}, resourceControls []portainer
|
||||||
// Authorized networks are decorated during the process.
|
// Authorized networks are decorated during the process.
|
||||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
||||||
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||||
func filterNetworkList(networkData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
func filterNetworkList(networkData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
||||||
filteredNetworkData := make([]interface{}, 0)
|
filteredNetworkData := make([]interface{}, 0)
|
||||||
|
|
||||||
for _, network := range networkData {
|
for _, network := range networkData {
|
||||||
|
|
|
@ -24,7 +24,7 @@ func secretListOperation(response *http.Response, executor *operationExecutor) e
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if executor.operationContext.isAdmin {
|
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
||||||
responseArray, err = decorateSecretList(responseArray, executor.operationContext.resourceControls)
|
responseArray, err = decorateSecretList(responseArray, executor.operationContext.resourceControls)
|
||||||
} else {
|
} else {
|
||||||
responseArray, err = filterSecretList(responseArray, executor.operationContext)
|
responseArray, err = filterSecretList(responseArray, executor.operationContext)
|
||||||
|
@ -87,7 +87,7 @@ func decorateSecretList(secretData []interface{}, resourceControls []portainer.R
|
||||||
// Authorized secrets are decorated during the process.
|
// Authorized secrets are decorated during the process.
|
||||||
// Resource controls checks are based on: resource identifier.
|
// Resource controls checks are based on: resource identifier.
|
||||||
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
||||||
func filterSecretList(secretData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
func filterSecretList(secretData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
||||||
filteredSecretData := make([]interface{}, 0)
|
filteredSecretData := make([]interface{}, 0)
|
||||||
|
|
||||||
for _, secret := range secretData {
|
for _, secret := range secretData {
|
||||||
|
|
|
@ -24,7 +24,7 @@ func serviceListOperation(response *http.Response, executor *operationExecutor)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if executor.operationContext.isAdmin {
|
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
||||||
responseArray, err = decorateServiceList(responseArray, executor.operationContext.resourceControls)
|
responseArray, err = decorateServiceList(responseArray, executor.operationContext.resourceControls)
|
||||||
} else {
|
} else {
|
||||||
responseArray, err = filterServiceList(responseArray, executor.operationContext)
|
responseArray, err = filterServiceList(responseArray, executor.operationContext)
|
||||||
|
@ -118,7 +118,7 @@ func decorateServiceList(serviceData []interface{}, resourceControls []portainer
|
||||||
// Authorized services are decorated during the process.
|
// Authorized services are decorated during the process.
|
||||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
||||||
// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||||
func filterServiceList(serviceData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
func filterServiceList(serviceData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
||||||
filteredServiceData := make([]interface{}, 0)
|
filteredServiceData := make([]interface{}, 0)
|
||||||
|
|
||||||
for _, service := range serviceData {
|
for _, service := range serviceData {
|
||||||
|
|
|
@ -25,7 +25,7 @@ func taskListOperation(response *http.Response, executor *operationExecutor) err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !executor.operationContext.isAdmin {
|
if !executor.operationContext.isAdmin && !executor.operationContext.endpointResourceAccess {
|
||||||
responseArray, err = filterTaskList(responseArray, executor.operationContext)
|
responseArray, err = filterTaskList(responseArray, executor.operationContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -54,7 +54,7 @@ func extractTaskLabelsFromTaskListObject(responseObject map[string]interface{})
|
||||||
// Resource controls checks are based on: service identifier, stack identifier (from label).
|
// Resource controls checks are based on: service identifier, stack identifier (from label).
|
||||||
// Task object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
// Task object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||||
// any resource control giving access to the user based on the associated service identifier.
|
// any resource control giving access to the user based on the associated service identifier.
|
||||||
func filterTaskList(taskData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
func filterTaskList(taskData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
||||||
filteredTaskData := make([]interface{}, 0)
|
filteredTaskData := make([]interface{}, 0)
|
||||||
|
|
||||||
for _, task := range taskData {
|
for _, task := range taskData {
|
||||||
|
|
|
@ -29,7 +29,7 @@ func volumeListOperation(response *http.Response, executor *operationExecutor) e
|
||||||
if responseObject["Volumes"] != nil {
|
if responseObject["Volumes"] != nil {
|
||||||
volumeData := responseObject["Volumes"].([]interface{})
|
volumeData := responseObject["Volumes"].([]interface{})
|
||||||
|
|
||||||
if executor.operationContext.isAdmin {
|
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
||||||
volumeData, err = decorateVolumeList(volumeData, executor.operationContext.resourceControls)
|
volumeData, err = decorateVolumeList(volumeData, executor.operationContext.resourceControls)
|
||||||
} else {
|
} else {
|
||||||
volumeData, err = filterVolumeList(volumeData, executor.operationContext)
|
volumeData, err = filterVolumeList(volumeData, executor.operationContext)
|
||||||
|
@ -119,7 +119,7 @@ func decorateVolumeList(volumeData []interface{}, resourceControls []portainer.R
|
||||||
// Authorized volumes are decorated during the process.
|
// Authorized volumes are decorated during the process.
|
||||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
||||||
// Volume object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
// Volume object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||||
func filterVolumeList(volumeData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
func filterVolumeList(volumeData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
||||||
filteredVolumeData := make([]interface{}, 0)
|
filteredVolumeData := make([]interface{}, 0)
|
||||||
|
|
||||||
for _, volume := range volumeData {
|
for _, volume := range volumeData {
|
||||||
|
|
|
@ -79,7 +79,7 @@ func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl,
|
||||||
// * the Public flag is set false
|
// * the Public flag is set false
|
||||||
// * he wants to create a resource control without any user/team accesses
|
// * he wants to create a resource control without any user/team accesses
|
||||||
// * he wants to add more than one user in the user accesses
|
// * he wants to add more than one user in the user accesses
|
||||||
// * he wants tp add a user in the user accesses that is not corresponding to its id
|
// * he wants to add a user in the user accesses that is not corresponding to its id
|
||||||
// * he wants to add a team he is not a member of
|
// * he wants to add a team he is not a member of
|
||||||
func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||||
if context.IsAdmin || resourceControl.Public {
|
if context.IsAdmin || resourceControl.Public {
|
||||||
|
@ -146,9 +146,9 @@ func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedReques
|
||||||
// It will check if the user is part of the authorized users or part of a team that is
|
// It will check if the user is part of the authorized users or part of a team that is
|
||||||
// listed in the authorized teams of the endpoint and the associated group.
|
// listed in the authorized teams of the endpoint and the associated group.
|
||||||
func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||||
groupAccess := authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
|
groupAccess := authorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies)
|
||||||
if !groupAccess {
|
if !groupAccess {
|
||||||
return authorizedAccess(userID, memberships, endpoint.AuthorizedUsers, endpoint.AuthorizedTeams)
|
return authorizedAccess(userID, memberships, endpoint.UserAccessPolicies, endpoint.TeamAccessPolicies)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -157,28 +157,28 @@ func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *porta
|
||||||
// It will check if the user is part of the authorized users or part of a team that is
|
// It will check if the user is part of the authorized users or part of a team that is
|
||||||
// listed in the authorized teams.
|
// listed in the authorized teams.
|
||||||
func authorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
func authorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||||
return authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
|
return authorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedRegistryAccess ensure that the user can access the specified registry.
|
// AuthorizedRegistryAccess ensure that the user can access the specified registry.
|
||||||
// It will check if the user is part of the authorized users or part of a team that is
|
// It will check if the user is part of the authorized users or part of a team that is
|
||||||
// listed in the authorized teams.
|
// listed in the authorized teams.
|
||||||
func AuthorizedRegistryAccess(registry *portainer.Registry, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
func AuthorizedRegistryAccess(registry *portainer.Registry, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||||
return authorizedAccess(userID, memberships, registry.AuthorizedUsers, registry.AuthorizedTeams)
|
return authorizedAccess(userID, memberships, registry.UserAccessPolicies, registry.TeamAccessPolicies)
|
||||||
}
|
}
|
||||||
|
|
||||||
func authorizedAccess(userID portainer.UserID, memberships []portainer.TeamMembership, authorizedUsers []portainer.UserID, authorizedTeams []portainer.TeamID) bool {
|
func authorizedAccess(userID portainer.UserID, memberships []portainer.TeamMembership, userAccessPolicies portainer.UserAccessPolicies, teamAccessPolicies portainer.TeamAccessPolicies) bool {
|
||||||
for _, authorizedUserID := range authorizedUsers {
|
_, userAccess := userAccessPolicies[userID]
|
||||||
if authorizedUserID == userID {
|
if userAccess {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for _, membership := range memberships {
|
for _, membership := range memberships {
|
||||||
for _, authorizedTeamID := range authorizedTeams {
|
_, teamAccess := teamAccessPolicies[membership.TeamID]
|
||||||
if membership.TeamID == authorizedTeamID {
|
if teamAccess {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,10 @@ type (
|
||||||
jwtService portainer.JWTService
|
jwtService portainer.JWTService
|
||||||
userService portainer.UserService
|
userService portainer.UserService
|
||||||
teamMembershipService portainer.TeamMembershipService
|
teamMembershipService portainer.TeamMembershipService
|
||||||
|
endpointService portainer.EndpointService
|
||||||
endpointGroupService portainer.EndpointGroupService
|
endpointGroupService portainer.EndpointGroupService
|
||||||
|
extensionService portainer.ExtensionService
|
||||||
|
rbacExtensionClient *rbacExtensionClient
|
||||||
authDisabled bool
|
authDisabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,7 +26,10 @@ type (
|
||||||
JWTService portainer.JWTService
|
JWTService portainer.JWTService
|
||||||
UserService portainer.UserService
|
UserService portainer.UserService
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
|
EndpointService portainer.EndpointService
|
||||||
EndpointGroupService portainer.EndpointGroupService
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
|
ExtensionService portainer.ExtensionService
|
||||||
|
RBACExtensionURL string
|
||||||
AuthDisabled bool
|
AuthDisabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,48 +49,49 @@ func NewRequestBouncer(parameters *RequestBouncerParams) *RequestBouncer {
|
||||||
jwtService: parameters.JWTService,
|
jwtService: parameters.JWTService,
|
||||||
userService: parameters.UserService,
|
userService: parameters.UserService,
|
||||||
teamMembershipService: parameters.TeamMembershipService,
|
teamMembershipService: parameters.TeamMembershipService,
|
||||||
|
endpointService: parameters.EndpointService,
|
||||||
endpointGroupService: parameters.EndpointGroupService,
|
endpointGroupService: parameters.EndpointGroupService,
|
||||||
|
extensionService: parameters.ExtensionService,
|
||||||
|
rbacExtensionClient: newRBACExtensionClient(parameters.RBACExtensionURL),
|
||||||
authDisabled: parameters.AuthDisabled,
|
authDisabled: parameters.AuthDisabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PublicAccess defines a security check for public endpoints.
|
// PublicAccess defines a security check for public API endpoints.
|
||||||
// No authentication is required to access these endpoints.
|
// No authentication is required to access these endpoints.
|
||||||
func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler {
|
func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler {
|
||||||
h = mwSecureHeaders(h)
|
h = mwSecureHeaders(h)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthenticatedAccess defines a security check for private endpoints.
|
// AuthorizedAccess defines a security check for API endpoints that require an authorization check.
|
||||||
// Authentication is required to access these endpoints.
|
// Authentication is required to access these endpoints.
|
||||||
func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler {
|
// If the RBAC extension is enabled, authorizations are required to use these endpoints.
|
||||||
h = bouncer.mwCheckAuthentication(h)
|
// If the RBAC extension is not enabled, the administrator role is required to use these endpoints.
|
||||||
h = mwSecureHeaders(h)
|
func (bouncer *RequestBouncer) AuthorizedAccess(h http.Handler) http.Handler {
|
||||||
|
h = bouncer.mwUpgradeToRestrictedRequest(h)
|
||||||
|
h = bouncer.mwCheckPortainerAuthorizations(h)
|
||||||
|
h = bouncer.mwAuthenticatedUser(h)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestrictedAccess defines a security check for restricted endpoints.
|
// RestrictedAccess defines a security check for restricted API endpoints.
|
||||||
// Authentication is required to access these endpoints.
|
// Authentication is required to access these endpoints.
|
||||||
// The request context will be enhanced with a RestrictedRequestContext object
|
// The request context will be enhanced with a RestrictedRequestContext object
|
||||||
// that might be used later to authorize/filter access to resources.
|
// that might be used later to authorize/filter access to resources inside an endpoint.
|
||||||
func (bouncer *RequestBouncer) RestrictedAccess(h http.Handler) http.Handler {
|
func (bouncer *RequestBouncer) RestrictedAccess(h http.Handler) http.Handler {
|
||||||
h = bouncer.mwUpgradeToRestrictedRequest(h)
|
h = bouncer.mwUpgradeToRestrictedRequest(h)
|
||||||
h = bouncer.AuthenticatedAccess(h)
|
h = bouncer.mwAuthenticatedUser(h)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// AdministratorAccess defines a chain of middleware for restricted endpoints.
|
// AuthorizedEndpointOperation retrieves the JWT token from the request context and verifies
|
||||||
// Authentication as well as administrator role are required to access these endpoints.
|
|
||||||
func (bouncer *RequestBouncer) AdministratorAccess(h http.Handler) http.Handler {
|
|
||||||
h = mwCheckAdministratorRole(h)
|
|
||||||
h = bouncer.AuthenticatedAccess(h)
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndpointAccess retrieves the JWT token from the request context and verifies
|
|
||||||
// that the user can access the specified endpoint.
|
// that the user can access the specified endpoint.
|
||||||
// An error is returned when access is denied.
|
// If the RBAC extension is enabled and the authorizationCheck flag is set,
|
||||||
func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portainer.Endpoint) error {
|
// it will also validate that the user can execute the specified operation.
|
||||||
|
// An error is returned when access to the endpoint is denied or if the user do not have the required
|
||||||
|
// authorization to execute the operation.
|
||||||
|
func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint, authorizationCheck bool) error {
|
||||||
tokenData, err := RetrieveTokenData(r)
|
tokenData, err := RetrieveTokenData(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -108,9 +115,43 @@ func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portain
|
||||||
return portainer.ErrEndpointAccessDenied
|
return portainer.ErrEndpointAccessDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if authorizationCheck {
|
||||||
|
err = bouncer.checkEndpointOperationAuthorization(r, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return portainer.ErrAuthorizationRequired
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bouncer *RequestBouncer) checkEndpointOperationAuthorization(r *http.Request, endpoint *portainer.Endpoint) error {
|
||||||
|
tokenData, err := RetrieveTokenData(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.Role == portainer.AdministratorRole {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
extension, err := bouncer.extensionService.Extension(portainer.RBACExtension)
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
apiOperation := &portainer.APIOperationAuthorizationRequest{
|
||||||
|
Path: r.URL.String(),
|
||||||
|
Method: r.Method,
|
||||||
|
Authorizations: tokenData.EndpointAuthorizations[endpoint.ID],
|
||||||
|
}
|
||||||
|
|
||||||
|
bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey)
|
||||||
|
return bouncer.rbacExtensionClient.checkAuthorization(apiOperation)
|
||||||
|
}
|
||||||
|
|
||||||
// RegistryAccess retrieves the JWT token from the request context and verifies
|
// RegistryAccess retrieves the JWT token from the request context and verifies
|
||||||
// that the user can access the specified registry.
|
// that the user can access the specified registry.
|
||||||
// An error is returned when access is denied.
|
// An error is returned when access is denied.
|
||||||
|
@ -136,11 +177,50 @@ func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portain
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// mwSecureHeaders provides secure headers middleware for handlers.
|
func (bouncer *RequestBouncer) mwAuthenticatedUser(h http.Handler) http.Handler {
|
||||||
func mwSecureHeaders(next http.Handler) http.Handler {
|
h = bouncer.mwCheckAuthentication(h)
|
||||||
|
h = mwSecureHeaders(h)
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// mwCheckPortainerAuthorizations will verify that the user has the required authorization to access
|
||||||
|
// a specific API endpoint. It will leverage the RBAC extension authorization validation if the extension
|
||||||
|
// is enabled.
|
||||||
|
func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
tokenData, err := RetrieveTokenData(r)
|
||||||
w.Header().Add("X-Content-Type-Options", "nosniff")
|
if err != nil {
|
||||||
|
httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrResourceAccessDenied)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.Role == portainer.AdministratorRole {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
extension, err := bouncer.extensionService.Extension(portainer.RBACExtension)
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
httperror.WriteError(w, http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiOperation := &portainer.APIOperationAuthorizationRequest{
|
||||||
|
Path: r.URL.String(),
|
||||||
|
Method: r.Method,
|
||||||
|
Authorizations: tokenData.PortainerAuthorizations,
|
||||||
|
}
|
||||||
|
|
||||||
|
bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey)
|
||||||
|
err = bouncer.rbacExtensionClient.checkAuthorization(apiOperation)
|
||||||
|
if err != nil {
|
||||||
|
httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrAuthorizationRequired)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -166,19 +246,6 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// mwCheckAdministratorRole check the role of the user associated to the request
|
|
||||||
func mwCheckAdministratorRole(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
tokenData, err := RetrieveTokenData(r)
|
|
||||||
if err != nil || tokenData.Role != portainer.AdministratorRole {
|
|
||||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrResourceAccessDenied)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// mwCheckAuthentication provides Authentication middleware for handlers
|
// mwCheckAuthentication provides Authentication middleware for handlers
|
||||||
func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Handler {
|
func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -229,6 +296,15 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mwSecureHeaders provides secure headers middleware for handlers.
|
||||||
|
func mwSecureHeaders(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Add("X-XSS-Protection", "1; mode=block")
|
||||||
|
w.Header().Add("X-Content-Type-Options", "nosniff")
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.UserID, userRole portainer.UserRole) (*RestrictedRequestContext, error) {
|
func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.UserID, userRole portainer.UserRole) (*RestrictedRequestContext, error) {
|
||||||
requestContext := &RestrictedRequestContext{
|
requestContext := &RestrictedRequestContext{
|
||||||
IsAdmin: true,
|
IsAdmin: true,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package security
|
package security
|
||||||
|
|
||||||
import "github.com/portainer/portainer/api"
|
import (
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
// FilterUserTeams filters teams based on user role.
|
// FilterUserTeams filters teams based on user role.
|
||||||
// non-administrator users only have access to team they are member of.
|
// non-administrator users only have access to team they are member of.
|
||||||
|
@ -78,7 +80,7 @@ func FilterRegistries(registries []portainer.Registry, context *RestrictedReques
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterTemplates filters templates based on the user role.
|
// FilterTemplates filters templates based on the user role.
|
||||||
// Non-administrato template do not have access to templates where the AdministratorOnly flag is set to true.
|
// Non-administrator template do not have access to templates where the AdministratorOnly flag is set to true.
|
||||||
func FilterTemplates(templates []portainer.Template, context *RestrictedRequestContext) []portainer.Template {
|
func FilterTemplates(templates []portainer.Template, context *RestrictedRequestContext) []portainer.Template {
|
||||||
filteredTemplates := templates
|
filteredTemplates := templates
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultHTTPTimeout = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
type rbacExtensionClient struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
extensionURL string
|
||||||
|
licenseKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRBACExtensionClient(extensionURL string) *rbacExtensionClient {
|
||||||
|
return &rbacExtensionClient{
|
||||||
|
extensionURL: extensionURL,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: time.Second * time.Duration(defaultHTTPTimeout),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *rbacExtensionClient) setLicenseKey(licenseKey string) {
|
||||||
|
client.licenseKey = licenseKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *rbacExtensionClient) checkAuthorization(authRequest *portainer.APIOperationAuthorizationRequest) error {
|
||||||
|
encodedAuthRequest, err := json.Marshal(authRequest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", client.extensionURL+"/authorized_operation", nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("X-RBAC-AuthorizationRequest", string(encodedAuthRequest))
|
||||||
|
req.Header.Set("X-PortainerExtension-License", client.licenseKey)
|
||||||
|
|
||||||
|
resp, err := client.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusNoContent {
|
||||||
|
return portainer.ErrAuthorizationRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ package http
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/handler/roles"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/docker"
|
"github.com/portainer/portainer/api/docker"
|
||||||
"github.com/portainer/portainer/api/http/handler"
|
"github.com/portainer/portainer/api/http/handler"
|
||||||
|
@ -48,6 +50,7 @@ type Server struct {
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
JobScheduler portainer.JobScheduler
|
JobScheduler portainer.JobScheduler
|
||||||
Snapshotter portainer.Snapshotter
|
Snapshotter portainer.Snapshotter
|
||||||
|
RoleService portainer.RoleService
|
||||||
DockerHubService portainer.DockerHubService
|
DockerHubService portainer.DockerHubService
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
EndpointGroupService portainer.EndpointGroupService
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
|
@ -78,15 +81,6 @@ type Server struct {
|
||||||
|
|
||||||
// Start starts the HTTP server
|
// Start starts the HTTP server
|
||||||
func (server *Server) Start() error {
|
func (server *Server) Start() error {
|
||||||
requestBouncerParameters := &security.RequestBouncerParams{
|
|
||||||
JWTService: server.JWTService,
|
|
||||||
UserService: server.UserService,
|
|
||||||
TeamMembershipService: server.TeamMembershipService,
|
|
||||||
EndpointGroupService: server.EndpointGroupService,
|
|
||||||
AuthDisabled: server.AuthDisabled,
|
|
||||||
}
|
|
||||||
requestBouncer := security.NewRequestBouncer(requestBouncerParameters)
|
|
||||||
|
|
||||||
proxyManagerParameters := &proxy.ManagerParams{
|
proxyManagerParameters := &proxy.ManagerParams{
|
||||||
ResourceControlService: server.ResourceControlService,
|
ResourceControlService: server.ResourceControlService,
|
||||||
TeamMembershipService: server.TeamMembershipService,
|
TeamMembershipService: server.TeamMembershipService,
|
||||||
|
@ -97,6 +91,18 @@ func (server *Server) Start() error {
|
||||||
}
|
}
|
||||||
proxyManager := proxy.NewManager(proxyManagerParameters)
|
proxyManager := proxy.NewManager(proxyManagerParameters)
|
||||||
|
|
||||||
|
requestBouncerParameters := &security.RequestBouncerParams{
|
||||||
|
JWTService: server.JWTService,
|
||||||
|
UserService: server.UserService,
|
||||||
|
TeamMembershipService: server.TeamMembershipService,
|
||||||
|
EndpointService: server.EndpointService,
|
||||||
|
EndpointGroupService: server.EndpointGroupService,
|
||||||
|
ExtensionService: server.ExtensionService,
|
||||||
|
RBACExtensionURL: proxyManager.GetExtensionURL(portainer.RBACExtension),
|
||||||
|
AuthDisabled: server.AuthDisabled,
|
||||||
|
}
|
||||||
|
requestBouncer := security.NewRequestBouncer(requestBouncerParameters)
|
||||||
|
|
||||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||||
|
|
||||||
var authHandler = auth.NewHandler(requestBouncer, rateLimiter, server.AuthDisabled)
|
var authHandler = auth.NewHandler(requestBouncer, rateLimiter, server.AuthDisabled)
|
||||||
|
@ -108,8 +114,14 @@ func (server *Server) Start() error {
|
||||||
authHandler.TeamService = server.TeamService
|
authHandler.TeamService = server.TeamService
|
||||||
authHandler.TeamMembershipService = server.TeamMembershipService
|
authHandler.TeamMembershipService = server.TeamMembershipService
|
||||||
authHandler.ExtensionService = server.ExtensionService
|
authHandler.ExtensionService = server.ExtensionService
|
||||||
|
authHandler.EndpointService = server.EndpointService
|
||||||
|
authHandler.EndpointGroupService = server.EndpointGroupService
|
||||||
|
authHandler.RoleService = server.RoleService
|
||||||
authHandler.ProxyManager = proxyManager
|
authHandler.ProxyManager = proxyManager
|
||||||
|
|
||||||
|
var roleHandler = roles.NewHandler(requestBouncer)
|
||||||
|
roleHandler.RoleService = server.RoleService
|
||||||
|
|
||||||
var dockerHubHandler = dockerhub.NewHandler(requestBouncer)
|
var dockerHubHandler = dockerhub.NewHandler(requestBouncer)
|
||||||
dockerHubHandler.DockerHubService = server.DockerHubService
|
dockerHubHandler.DockerHubService = server.DockerHubService
|
||||||
|
|
||||||
|
@ -136,6 +148,9 @@ func (server *Server) Start() error {
|
||||||
var extensionHandler = extensions.NewHandler(requestBouncer)
|
var extensionHandler = extensions.NewHandler(requestBouncer)
|
||||||
extensionHandler.ExtensionService = server.ExtensionService
|
extensionHandler.ExtensionService = server.ExtensionService
|
||||||
extensionHandler.ExtensionManager = server.ExtensionManager
|
extensionHandler.ExtensionManager = server.ExtensionManager
|
||||||
|
extensionHandler.EndpointGroupService = server.EndpointGroupService
|
||||||
|
extensionHandler.EndpointService = server.EndpointService
|
||||||
|
extensionHandler.RegistryService = server.RegistryService
|
||||||
|
|
||||||
var registryHandler = registries.NewHandler(requestBouncer)
|
var registryHandler = registries.NewHandler(requestBouncer)
|
||||||
registryHandler.RegistryService = server.RegistryService
|
registryHandler.RegistryService = server.RegistryService
|
||||||
|
@ -208,6 +223,7 @@ func (server *Server) Start() error {
|
||||||
webhookHandler.DockerClientFactory = server.DockerClientFactory
|
webhookHandler.DockerClientFactory = server.DockerClientFactory
|
||||||
|
|
||||||
server.Handler = &handler.Handler{
|
server.Handler = &handler.Handler{
|
||||||
|
RoleHandler: roleHandler,
|
||||||
AuthHandler: authHandler,
|
AuthHandler: authHandler,
|
||||||
DockerHubHandler: dockerHubHandler,
|
DockerHubHandler: dockerHubHandler,
|
||||||
EndpointGroupHandler: endpointGroupHandler,
|
EndpointGroupHandler: endpointGroupHandler,
|
||||||
|
|
|
@ -19,6 +19,8 @@ type claims struct {
|
||||||
UserID int `json:"id"`
|
UserID int `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Role int `json:"role"`
|
Role int `json:"role"`
|
||||||
|
EndpointAuthorizations portainer.EndpointAuthorizations `json:"endpointAuthorizations"`
|
||||||
|
PortainerAuthorizations portainer.Authorizations `json:"portainerAuthorizations"`
|
||||||
jwt.StandardClaims
|
jwt.StandardClaims
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +43,8 @@ func (service *Service) GenerateToken(data *portainer.TokenData) (string, error)
|
||||||
int(data.ID),
|
int(data.ID),
|
||||||
data.Username,
|
data.Username,
|
||||||
int(data.Role),
|
int(data.Role),
|
||||||
|
data.EndpointAuthorizations,
|
||||||
|
data.PortainerAuthorizations,
|
||||||
jwt.StandardClaims{
|
jwt.StandardClaims{
|
||||||
ExpiresAt: expireToken,
|
ExpiresAt: expireToken,
|
||||||
},
|
},
|
||||||
|
@ -70,6 +74,8 @@ func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData,
|
||||||
ID: portainer.UserID(cl.UserID),
|
ID: portainer.UserID(cl.UserID),
|
||||||
Username: cl.Username,
|
Username: cl.Username,
|
||||||
Role: portainer.UserRole(cl.Role),
|
Role: portainer.UserRole(cl.Role),
|
||||||
|
EndpointAuthorizations: cl.EndpointAuthorizations,
|
||||||
|
PortainerAuthorizations: cl.PortainerAuthorizations,
|
||||||
}
|
}
|
||||||
return tokenData, nil
|
return tokenData, nil
|
||||||
}
|
}
|
||||||
|
|
287
api/portainer.go
287
api/portainer.go
|
@ -117,6 +117,7 @@ type (
|
||||||
Username string `json:"Username"`
|
Username string `json:"Username"`
|
||||||
Password string `json:"Password,omitempty"`
|
Password string `json:"Password,omitempty"`
|
||||||
Role UserRole `json:"Role"`
|
Role UserRole `json:"Role"`
|
||||||
|
PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserID represents a user identifier
|
// UserID represents a user identifier
|
||||||
|
@ -157,6 +158,8 @@ type (
|
||||||
ID UserID
|
ID UserID
|
||||||
Username string
|
Username string
|
||||||
Role UserRole
|
Role UserRole
|
||||||
|
EndpointAuthorizations EndpointAuthorizations
|
||||||
|
PortainerAuthorizations Authorizations
|
||||||
}
|
}
|
||||||
|
|
||||||
// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier)
|
// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier)
|
||||||
|
@ -193,9 +196,14 @@ type (
|
||||||
Authentication bool `json:"Authentication"`
|
Authentication bool `json:"Authentication"`
|
||||||
Username string `json:"Username"`
|
Username string `json:"Username"`
|
||||||
Password string `json:"Password,omitempty"`
|
Password string `json:"Password,omitempty"`
|
||||||
|
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
|
||||||
|
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||||
|
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||||
|
|
||||||
|
// Deprecated fields
|
||||||
|
// Deprecated in DBVersion == 18
|
||||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||||
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistryManagementConfiguration represents a configuration that can be used to query
|
// RegistryManagementConfiguration represents a configuration that can be used to query
|
||||||
|
@ -235,13 +243,13 @@ type (
|
||||||
GroupID EndpointGroupID `json:"GroupId"`
|
GroupID EndpointGroupID `json:"GroupId"`
|
||||||
PublicURL string `json:"PublicURL"`
|
PublicURL string `json:"PublicURL"`
|
||||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
|
||||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
|
||||||
Extensions []EndpointExtension `json:"Extensions"`
|
Extensions []EndpointExtension `json:"Extensions"`
|
||||||
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
|
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
|
||||||
Tags []string `json:"Tags"`
|
Tags []string `json:"Tags"`
|
||||||
Status EndpointStatus `json:"Status"`
|
Status EndpointStatus `json:"Status"`
|
||||||
Snapshots []Snapshot `json:"Snapshots"`
|
Snapshots []Snapshot `json:"Snapshots"`
|
||||||
|
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||||
|
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 4
|
// Deprecated in DBVersion == 4
|
||||||
|
@ -249,8 +257,50 @@ type (
|
||||||
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||||
TLSCertPath string `json:"TLSCert,omitempty"`
|
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||||
|
|
||||||
|
// Deprecated in DBVersion == 18
|
||||||
|
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||||
|
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authorization represents an authorization associated to an operation
|
||||||
|
Authorization string
|
||||||
|
|
||||||
|
// Authorizations represents a set of authorizations associated to a role
|
||||||
|
Authorizations map[Authorization]bool
|
||||||
|
|
||||||
|
// EndpointAuthorizations represents the authorizations associated to a set of endpoints
|
||||||
|
EndpointAuthorizations map[EndpointID]Authorizations
|
||||||
|
|
||||||
|
// APIOperationAuthorizationRequest represent an request for the authorization to execute an API operation
|
||||||
|
APIOperationAuthorizationRequest struct {
|
||||||
|
Path string
|
||||||
|
Method string
|
||||||
|
Authorizations Authorizations
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoleID represents a role identifier
|
||||||
|
RoleID int
|
||||||
|
|
||||||
|
// Role represents a set of authorizations that can be associated to a user or
|
||||||
|
// to a team.
|
||||||
|
Role struct {
|
||||||
|
ID RoleID `json:"Id"`
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Description string `json:"Description"`
|
||||||
|
Authorizations Authorizations `json:"Authorizations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccessPolicy represent a policy that can be associated to a user or team
|
||||||
|
AccessPolicy struct {
|
||||||
|
RoleID RoleID `json:"RoleId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAccessPolicies represent the association of an access policy and a user
|
||||||
|
UserAccessPolicies map[UserID]AccessPolicy
|
||||||
|
// TeamAccessPolicies represent the association of an access policy and a team
|
||||||
|
TeamAccessPolicies map[TeamID]AccessPolicy
|
||||||
|
|
||||||
// ScheduleID represents a schedule identifier.
|
// ScheduleID represents a schedule identifier.
|
||||||
ScheduleID int
|
ScheduleID int
|
||||||
|
|
||||||
|
@ -345,12 +395,16 @@ type (
|
||||||
ID EndpointGroupID `json:"Id"`
|
ID EndpointGroupID `json:"Id"`
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
Description string `json:"Description"`
|
Description string `json:"Description"`
|
||||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||||
Tags []string `json:"Tags"`
|
Tags []string `json:"Tags"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
Labels []Pair `json:"Labels"`
|
Labels []Pair `json:"Labels"`
|
||||||
|
|
||||||
|
// Deprecated in DBVersion == 18
|
||||||
|
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||||
|
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointExtension represents a deprecated form of Portainer extension
|
// EndpointExtension represents a deprecated form of Portainer extension
|
||||||
|
@ -551,6 +605,12 @@ type (
|
||||||
DeleteUser(ID UserID) error
|
DeleteUser(ID UserID) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RoleService interface {
|
||||||
|
Role(ID RoleID) (*Role, error)
|
||||||
|
Roles() ([]Role, error)
|
||||||
|
CreateRole(set *Role) error
|
||||||
|
}
|
||||||
|
|
||||||
// TeamService represents a service for managing user data
|
// TeamService represents a service for managing user data
|
||||||
TeamService interface {
|
TeamService interface {
|
||||||
Team(ID TeamID) (*Team, error)
|
Team(ID TeamID) (*Team, error)
|
||||||
|
@ -796,7 +856,7 @@ const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "1.20.2"
|
APIVersion = "1.20.2"
|
||||||
// DBVersion is the version number of the Portainer database
|
// DBVersion is the version number of the Portainer database
|
||||||
DBVersion = 17
|
DBVersion = 18
|
||||||
// AssetsServerURL represents the URL of the Portainer asset server
|
// AssetsServerURL represents the URL of the Portainer asset server
|
||||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||||
|
@ -804,7 +864,7 @@ const (
|
||||||
// MessageOfTheDayTitleURL represents the URL where Portainer MOTD title can be retrieved
|
// MessageOfTheDayTitleURL represents the URL where Portainer MOTD title can be retrieved
|
||||||
MessageOfTheDayTitleURL = AssetsServerURL + "/motd-title.txt"
|
MessageOfTheDayTitleURL = AssetsServerURL + "/motd-title.txt"
|
||||||
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
||||||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.20.2.json"
|
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.20.3.json"
|
||||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||||
PortainerAgentHeader = "Portainer-Agent"
|
PortainerAgentHeader = "Portainer-Agent"
|
||||||
// PortainerAgentTargetHeader represent the name of the header containing the target node name
|
// PortainerAgentTargetHeader represent the name of the header containing the target node name
|
||||||
|
@ -933,6 +993,8 @@ const (
|
||||||
RegistryManagementExtension
|
RegistryManagementExtension
|
||||||
// OAuthAuthenticationExtension represents the OAuth authentication extension
|
// OAuthAuthenticationExtension represents the OAuth authentication extension
|
||||||
OAuthAuthenticationExtension
|
OAuthAuthenticationExtension
|
||||||
|
// RBACExtension represents the RBAC extension
|
||||||
|
RBACExtension
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -956,3 +1018,214 @@ const (
|
||||||
// CustomRegistry represents a custom registry
|
// CustomRegistry represents a custom registry
|
||||||
CustomRegistry
|
CustomRegistry
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OperationDockerContainerArchiveInfo Authorization = "DockerContainerArchiveInfo"
|
||||||
|
OperationDockerContainerList Authorization = "DockerContainerList"
|
||||||
|
OperationDockerContainerExport Authorization = "DockerContainerExport"
|
||||||
|
OperationDockerContainerChanges Authorization = "DockerContainerChanges"
|
||||||
|
OperationDockerContainerInspect Authorization = "DockerContainerInspect"
|
||||||
|
OperationDockerContainerTop Authorization = "DockerContainerTop"
|
||||||
|
OperationDockerContainerLogs Authorization = "DockerContainerLogs"
|
||||||
|
OperationDockerContainerStats Authorization = "DockerContainerStats"
|
||||||
|
OperationDockerContainerAttachWebsocket Authorization = "DockerContainerAttachWebsocket"
|
||||||
|
OperationDockerContainerArchive Authorization = "DockerContainerArchive"
|
||||||
|
OperationDockerContainerCreate Authorization = "DockerContainerCreate"
|
||||||
|
OperationDockerContainerPrune Authorization = "DockerContainerPrune"
|
||||||
|
OperationDockerContainerKill Authorization = "DockerContainerKill"
|
||||||
|
OperationDockerContainerPause Authorization = "DockerContainerPause"
|
||||||
|
OperationDockerContainerUnpause Authorization = "DockerContainerUnpause"
|
||||||
|
OperationDockerContainerRestart Authorization = "DockerContainerRestart"
|
||||||
|
OperationDockerContainerStart Authorization = "DockerContainerStart"
|
||||||
|
OperationDockerContainerStop Authorization = "DockerContainerStop"
|
||||||
|
OperationDockerContainerWait Authorization = "DockerContainerWait"
|
||||||
|
OperationDockerContainerResize Authorization = "DockerContainerResize"
|
||||||
|
OperationDockerContainerAttach Authorization = "DockerContainerAttach"
|
||||||
|
OperationDockerContainerExec Authorization = "DockerContainerExec"
|
||||||
|
OperationDockerContainerRename Authorization = "DockerContainerRename"
|
||||||
|
OperationDockerContainerUpdate Authorization = "DockerContainerUpdate"
|
||||||
|
OperationDockerContainerPutContainerArchive Authorization = "DockerContainerPutContainerArchive"
|
||||||
|
OperationDockerContainerDelete Authorization = "DockerContainerDelete"
|
||||||
|
OperationDockerImageList Authorization = "DockerImageList"
|
||||||
|
OperationDockerImageSearch Authorization = "DockerImageSearch"
|
||||||
|
OperationDockerImageGetAll Authorization = "DockerImageGetAll"
|
||||||
|
OperationDockerImageGet Authorization = "DockerImageGet"
|
||||||
|
OperationDockerImageHistory Authorization = "DockerImageHistory"
|
||||||
|
OperationDockerImageInspect Authorization = "DockerImageInspect"
|
||||||
|
OperationDockerImageLoad Authorization = "DockerImageLoad"
|
||||||
|
OperationDockerImageCreate Authorization = "DockerImageCreate"
|
||||||
|
OperationDockerImagePrune Authorization = "DockerImagePrune"
|
||||||
|
OperationDockerImagePush Authorization = "DockerImagePush"
|
||||||
|
OperationDockerImageTag Authorization = "DockerImageTag"
|
||||||
|
OperationDockerImageDelete Authorization = "DockerImageDelete"
|
||||||
|
OperationDockerImageCommit Authorization = "DockerImageCommit"
|
||||||
|
OperationDockerImageBuild Authorization = "DockerImageBuild"
|
||||||
|
OperationDockerNetworkList Authorization = "DockerNetworkList"
|
||||||
|
OperationDockerNetworkInspect Authorization = "DockerNetworkInspect"
|
||||||
|
OperationDockerNetworkCreate Authorization = "DockerNetworkCreate"
|
||||||
|
OperationDockerNetworkConnect Authorization = "DockerNetworkConnect"
|
||||||
|
OperationDockerNetworkDisconnect Authorization = "DockerNetworkDisconnect"
|
||||||
|
OperationDockerNetworkPrune Authorization = "DockerNetworkPrune"
|
||||||
|
OperationDockerNetworkDelete Authorization = "DockerNetworkDelete"
|
||||||
|
OperationDockerVolumeList Authorization = "DockerVolumeList"
|
||||||
|
OperationDockerVolumeInspect Authorization = "DockerVolumeInspect"
|
||||||
|
OperationDockerVolumeCreate Authorization = "DockerVolumeCreate"
|
||||||
|
OperationDockerVolumePrune Authorization = "DockerVolumePrune"
|
||||||
|
OperationDockerVolumeDelete Authorization = "DockerVolumeDelete"
|
||||||
|
OperationDockerExecInspect Authorization = "DockerExecInspect"
|
||||||
|
OperationDockerExecStart Authorization = "DockerExecStart"
|
||||||
|
OperationDockerExecResize Authorization = "DockerExecResize"
|
||||||
|
OperationDockerSwarmInspect Authorization = "DockerSwarmInspect"
|
||||||
|
OperationDockerSwarmUnlockKey Authorization = "DockerSwarmUnlockKey"
|
||||||
|
OperationDockerSwarmInit Authorization = "DockerSwarmInit"
|
||||||
|
OperationDockerSwarmJoin Authorization = "DockerSwarmJoin"
|
||||||
|
OperationDockerSwarmLeave Authorization = "DockerSwarmLeave"
|
||||||
|
OperationDockerSwarmUpdate Authorization = "DockerSwarmUpdate"
|
||||||
|
OperationDockerSwarmUnlock Authorization = "DockerSwarmUnlock"
|
||||||
|
OperationDockerNodeList Authorization = "DockerNodeList"
|
||||||
|
OperationDockerNodeInspect Authorization = "DockerNodeInspect"
|
||||||
|
OperationDockerNodeUpdate Authorization = "DockerNodeUpdate"
|
||||||
|
OperationDockerNodeDelete Authorization = "DockerNodeDelete"
|
||||||
|
OperationDockerServiceList Authorization = "DockerServiceList"
|
||||||
|
OperationDockerServiceInspect Authorization = "DockerServiceInspect"
|
||||||
|
OperationDockerServiceLogs Authorization = "DockerServiceLogs"
|
||||||
|
OperationDockerServiceCreate Authorization = "DockerServiceCreate"
|
||||||
|
OperationDockerServiceUpdate Authorization = "DockerServiceUpdate"
|
||||||
|
OperationDockerServiceDelete Authorization = "DockerServiceDelete"
|
||||||
|
OperationDockerSecretList Authorization = "DockerSecretList"
|
||||||
|
OperationDockerSecretInspect Authorization = "DockerSecretInspect"
|
||||||
|
OperationDockerSecretCreate Authorization = "DockerSecretCreate"
|
||||||
|
OperationDockerSecretUpdate Authorization = "DockerSecretUpdate"
|
||||||
|
OperationDockerSecretDelete Authorization = "DockerSecretDelete"
|
||||||
|
OperationDockerConfigList Authorization = "DockerConfigList"
|
||||||
|
OperationDockerConfigInspect Authorization = "DockerConfigInspect"
|
||||||
|
OperationDockerConfigCreate Authorization = "DockerConfigCreate"
|
||||||
|
OperationDockerConfigUpdate Authorization = "DockerConfigUpdate"
|
||||||
|
OperationDockerConfigDelete Authorization = "DockerConfigDelete"
|
||||||
|
OperationDockerTaskList Authorization = "DockerTaskList"
|
||||||
|
OperationDockerTaskInspect Authorization = "DockerTaskInspect"
|
||||||
|
OperationDockerTaskLogs Authorization = "DockerTaskLogs"
|
||||||
|
OperationDockerPluginList Authorization = "DockerPluginList"
|
||||||
|
OperationDockerPluginPrivileges Authorization = "DockerPluginPrivileges"
|
||||||
|
OperationDockerPluginInspect Authorization = "DockerPluginInspect"
|
||||||
|
OperationDockerPluginPull Authorization = "DockerPluginPull"
|
||||||
|
OperationDockerPluginCreate Authorization = "DockerPluginCreate"
|
||||||
|
OperationDockerPluginEnable Authorization = "DockerPluginEnable"
|
||||||
|
OperationDockerPluginDisable Authorization = "DockerPluginDisable"
|
||||||
|
OperationDockerPluginPush Authorization = "DockerPluginPush"
|
||||||
|
OperationDockerPluginUpgrade Authorization = "DockerPluginUpgrade"
|
||||||
|
OperationDockerPluginSet Authorization = "DockerPluginSet"
|
||||||
|
OperationDockerPluginDelete Authorization = "DockerPluginDelete"
|
||||||
|
OperationDockerSessionStart Authorization = "DockerSessionStart"
|
||||||
|
OperationDockerDistributionInspect Authorization = "DockerDistributionInspect"
|
||||||
|
OperationDockerBuildPrune Authorization = "DockerBuildPrune"
|
||||||
|
OperationDockerBuildCancel Authorization = "DockerBuildCancel"
|
||||||
|
OperationDockerPing Authorization = "DockerPing"
|
||||||
|
OperationDockerInfo Authorization = "DockerInfo"
|
||||||
|
OperationDockerEvents Authorization = "DockerEvents"
|
||||||
|
OperationDockerSystem Authorization = "DockerSystem"
|
||||||
|
OperationDockerVersion Authorization = "DockerVersion"
|
||||||
|
|
||||||
|
OperationDockerAgentPing Authorization = "DockerAgentPing"
|
||||||
|
OperationDockerAgentList Authorization = "DockerAgentList"
|
||||||
|
OperationDockerAgentHostInfo Authorization = "DockerAgentHostInfo"
|
||||||
|
OperationDockerAgentBrowseDelete Authorization = "DockerAgentBrowseDelete"
|
||||||
|
OperationDockerAgentBrowseGet Authorization = "DockerAgentBrowseGet"
|
||||||
|
OperationDockerAgentBrowseList Authorization = "DockerAgentBrowseList"
|
||||||
|
OperationDockerAgentBrowsePut Authorization = "DockerAgentBrowsePut"
|
||||||
|
OperationDockerAgentBrowseRename Authorization = "DockerAgentBrowseRename"
|
||||||
|
|
||||||
|
OperationPortainerDockerHubInspect Authorization = "PortainerDockerHubInspect"
|
||||||
|
OperationPortainerDockerHubUpdate Authorization = "PortainerDockerHubUpdate"
|
||||||
|
OperationPortainerEndpointGroupCreate Authorization = "PortainerEndpointGroupCreate"
|
||||||
|
OperationPortainerEndpointGroupList Authorization = "PortainerEndpointGroupList"
|
||||||
|
OperationPortainerEndpointGroupDelete Authorization = "PortainerEndpointGroupDelete"
|
||||||
|
OperationPortainerEndpointGroupInspect Authorization = "PortainerEndpointGroupInspect"
|
||||||
|
OperationPortainerEndpointGroupUpdate Authorization = "PortainerEndpointGroupEdit"
|
||||||
|
OperationPortainerEndpointGroupAccess Authorization = "PortainerEndpointGroupAccess "
|
||||||
|
OperationPortainerEndpointList Authorization = "PortainerEndpointList"
|
||||||
|
OperationPortainerEndpointInspect Authorization = "PortainerEndpointInspect"
|
||||||
|
OperationPortainerEndpointCreate Authorization = "PortainerEndpointCreate"
|
||||||
|
OperationPortainerEndpointExtensionAdd Authorization = "PortainerEndpointExtensionAdd"
|
||||||
|
OperationPortainerEndpointJob Authorization = "PortainerEndpointJob"
|
||||||
|
OperationPortainerEndpointSnapshots Authorization = "PortainerEndpointSnapshots"
|
||||||
|
OperationPortainerEndpointSnapshot Authorization = "PortainerEndpointSnapshot"
|
||||||
|
OperationPortainerEndpointUpdate Authorization = "PortainerEndpointUpdate"
|
||||||
|
OperationPortainerEndpointUpdateAccess Authorization = "PortainerEndpointUpdateAccess"
|
||||||
|
OperationPortainerEndpointDelete Authorization = "PortainerEndpointDelete"
|
||||||
|
OperationPortainerEndpointExtensionRemove Authorization = "PortainerEndpointExtensionRemove"
|
||||||
|
OperationPortainerExtensionList Authorization = "PortainerExtensionList"
|
||||||
|
OperationPortainerExtensionInspect Authorization = "PortainerExtensionInspect"
|
||||||
|
OperationPortainerExtensionCreate Authorization = "PortainerExtensionCreate"
|
||||||
|
OperationPortainerExtensionUpdate Authorization = "PortainerExtensionUpdate"
|
||||||
|
OperationPortainerExtensionDelete Authorization = "PortainerExtensionDelete"
|
||||||
|
OperationPortainerMOTD Authorization = "PortainerMOTD"
|
||||||
|
OperationPortainerRegistryList Authorization = "PortainerRegistryList"
|
||||||
|
OperationPortainerRegistryInspect Authorization = "PortainerRegistryInspect"
|
||||||
|
OperationPortainerRegistryCreate Authorization = "PortainerRegistryCreate"
|
||||||
|
OperationPortainerRegistryConfigure Authorization = "PortainerRegistryConfigure"
|
||||||
|
OperationPortainerRegistryUpdate Authorization = "PortainerRegistryUpdate"
|
||||||
|
OperationPortainerRegistryUpdateAccess Authorization = "PortainerRegistryUpdateAccess"
|
||||||
|
OperationPortainerRegistryDelete Authorization = "PortainerRegistryDelete"
|
||||||
|
OperationPortainerResourceControlCreate Authorization = "PortainerResourceControlCreate"
|
||||||
|
OperationPortainerResourceControlUpdate Authorization = "PortainerResourceControlUpdate"
|
||||||
|
OperationPortainerResourceControlDelete Authorization = "PortainerResourceControlDelete"
|
||||||
|
OperationPortainerRoleList Authorization = "PortainerRoleList"
|
||||||
|
OperationPortainerRoleInspect Authorization = "PortainerRoleInspect"
|
||||||
|
OperationPortainerRoleCreate Authorization = "PortainerRoleCreate"
|
||||||
|
OperationPortainerRoleUpdate Authorization = "PortainerRoleUpdate"
|
||||||
|
OperationPortainerRoleDelete Authorization = "PortainerRoleDelete"
|
||||||
|
OperationPortainerScheduleList Authorization = "PortainerScheduleList"
|
||||||
|
OperationPortainerScheduleInspect Authorization = "PortainerScheduleInspect"
|
||||||
|
OperationPortainerScheduleFile Authorization = "PortainerScheduleFile"
|
||||||
|
OperationPortainerScheduleTasks Authorization = "PortainerScheduleTasks"
|
||||||
|
OperationPortainerScheduleCreate Authorization = "PortainerScheduleCreate"
|
||||||
|
OperationPortainerScheduleUpdate Authorization = "PortainerScheduleUpdate"
|
||||||
|
OperationPortainerScheduleDelete Authorization = "PortainerScheduleDelete"
|
||||||
|
OperationPortainerSettingsInspect Authorization = "PortainerSettingsInspect"
|
||||||
|
OperationPortainerSettingsUpdate Authorization = "PortainerSettingsUpdate"
|
||||||
|
OperationPortainerSettingsLDAPCheck Authorization = "PortainerSettingsLDAPCheck"
|
||||||
|
OperationPortainerStackList Authorization = "PortainerStackList"
|
||||||
|
OperationPortainerStackInspect Authorization = "PortainerStackInspect"
|
||||||
|
OperationPortainerStackFile Authorization = "PortainerStackFile"
|
||||||
|
OperationPortainerStackCreate Authorization = "PortainerStackCreate"
|
||||||
|
OperationPortainerStackMigrate Authorization = "PortainerStackMigrate"
|
||||||
|
OperationPortainerStackUpdate Authorization = "PortainerStackUpdate"
|
||||||
|
OperationPortainerStackDelete Authorization = "PortainerStackDelete"
|
||||||
|
OperationPortainerTagList Authorization = "PortainerTagList"
|
||||||
|
OperationPortainerTagCreate Authorization = "PortainerTagCreate"
|
||||||
|
OperationPortainerTagDelete Authorization = "PortainerTagDelete"
|
||||||
|
OperationPortainerTeamMembershipList Authorization = "PortainerTeamMembershipList"
|
||||||
|
OperationPortainerTeamMembershipCreate Authorization = "PortainerTeamMembershipCreate"
|
||||||
|
OperationPortainerTeamMembershipUpdate Authorization = "PortainerTeamMembershipUpdate"
|
||||||
|
OperationPortainerTeamMembershipDelete Authorization = "PortainerTeamMembershipDelete"
|
||||||
|
OperationPortainerTeamList Authorization = "PortainerTeamList"
|
||||||
|
OperationPortainerTeamInspect Authorization = "PortainerTeamInspect"
|
||||||
|
OperationPortainerTeamMemberships Authorization = "PortainerTeamMemberships"
|
||||||
|
OperationPortainerTeamCreate Authorization = "PortainerTeamCreate"
|
||||||
|
OperationPortainerTeamUpdate Authorization = "PortainerTeamUpdate"
|
||||||
|
OperationPortainerTeamDelete Authorization = "PortainerTeamDelete"
|
||||||
|
OperationPortainerTemplateList Authorization = "PortainerTemplateList"
|
||||||
|
OperationPortainerTemplateInspect Authorization = "PortainerTemplateInspect"
|
||||||
|
OperationPortainerTemplateCreate Authorization = "PortainerTemplateCreate"
|
||||||
|
OperationPortainerTemplateUpdate Authorization = "PortainerTemplateUpdate"
|
||||||
|
OperationPortainerTemplateDelete Authorization = "PortainerTemplateDelete"
|
||||||
|
OperationPortainerUploadTLS Authorization = "PortainerUploadTLS"
|
||||||
|
OperationPortainerUserList Authorization = "PortainerUserList"
|
||||||
|
OperationPortainerUserInspect Authorization = "PortainerUserInspect"
|
||||||
|
OperationPortainerUserMemberships Authorization = "PortainerUserMemberships"
|
||||||
|
OperationPortainerUserCreate Authorization = "PortainerUserCreate"
|
||||||
|
OperationPortainerUserUpdate Authorization = "PortainerUserUpdate"
|
||||||
|
OperationPortainerUserUpdatePassword Authorization = "PortainerUserUpdatePassword"
|
||||||
|
OperationPortainerUserDelete Authorization = "PortainerUserDelete"
|
||||||
|
OperationPortainerWebsocketExec Authorization = "PortainerWebsocketExec"
|
||||||
|
OperationPortainerWebhookList Authorization = "PortainerWebhookList"
|
||||||
|
OperationPortainerWebhookCreate Authorization = "PortainerWebhookCreate"
|
||||||
|
OperationPortainerWebhookDelete Authorization = "PortainerWebhookDelete"
|
||||||
|
|
||||||
|
OperationDockerUndefined Authorization = "DockerUndefined"
|
||||||
|
OperationDockerAgentUndefined Authorization = "DockerAgentUndefined"
|
||||||
|
OperationPortainerUndefined Authorization = "PortainerUndefined"
|
||||||
|
|
||||||
|
EndpointResourcesAccess Authorization = "EndpointResourcesAccess"
|
||||||
|
)
|
||||||
|
|
150
api/swagger.yaml
150
api/swagger.yaml
|
@ -465,56 +465,6 @@ paths:
|
||||||
examples:
|
examples:
|
||||||
application/json:
|
application/json:
|
||||||
err: "Endpoint management is disabled"
|
err: "Endpoint management is disabled"
|
||||||
/endpoints/{id}/access:
|
|
||||||
put:
|
|
||||||
tags:
|
|
||||||
- "endpoints"
|
|
||||||
summary: "Manage accesses to an endpoint"
|
|
||||||
description: |
|
|
||||||
Manage user and team accesses to an endpoint.
|
|
||||||
**Access policy**: administrator
|
|
||||||
operationId: "EndpointAccessUpdate"
|
|
||||||
consumes:
|
|
||||||
- "application/json"
|
|
||||||
produces:
|
|
||||||
- "application/json"
|
|
||||||
security:
|
|
||||||
- jwt: []
|
|
||||||
parameters:
|
|
||||||
- name: "id"
|
|
||||||
in: "path"
|
|
||||||
description: "Endpoint identifier"
|
|
||||||
required: true
|
|
||||||
type: "integer"
|
|
||||||
- in: "body"
|
|
||||||
name: "body"
|
|
||||||
description: "Authorizations details"
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/EndpointAccessUpdateRequest"
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "Success"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/Endpoint"
|
|
||||||
400:
|
|
||||||
description: "Invalid request"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/GenericError"
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: "Invalid request data format"
|
|
||||||
404:
|
|
||||||
description: "Endpoint not found"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/GenericError"
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: "Endpoint not found"
|
|
||||||
500:
|
|
||||||
description: "Server error"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/GenericError"
|
|
||||||
/endpoints/{id}/job:
|
/endpoints/{id}/job:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
@ -791,56 +741,6 @@ paths:
|
||||||
examples:
|
examples:
|
||||||
application/json:
|
application/json:
|
||||||
err: "EndpointGroup management is disabled"
|
err: "EndpointGroup management is disabled"
|
||||||
/endpoint_groups/{id}/access:
|
|
||||||
put:
|
|
||||||
tags:
|
|
||||||
- "endpoint_groups"
|
|
||||||
summary: "Manage accesses to an endpoint group"
|
|
||||||
description: |
|
|
||||||
Manage user and team accesses to an endpoint group.
|
|
||||||
**Access policy**: administrator
|
|
||||||
operationId: "EndpointGroupAccessUpdate"
|
|
||||||
consumes:
|
|
||||||
- "application/json"
|
|
||||||
produces:
|
|
||||||
- "application/json"
|
|
||||||
security:
|
|
||||||
- jwt: []
|
|
||||||
parameters:
|
|
||||||
- name: "id"
|
|
||||||
in: "path"
|
|
||||||
description: "EndpointGroup identifier"
|
|
||||||
required: true
|
|
||||||
type: "integer"
|
|
||||||
- in: "body"
|
|
||||||
name: "body"
|
|
||||||
description: "Authorizations details"
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/EndpointGroupAccessUpdateRequest"
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "Success"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/EndpointGroup"
|
|
||||||
400:
|
|
||||||
description: "Invalid request"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/GenericError"
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: "Invalid request data format"
|
|
||||||
404:
|
|
||||||
description: "EndpointGroup not found"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/GenericError"
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: "EndpointGroup not found"
|
|
||||||
500:
|
|
||||||
description: "Server error"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/GenericError"
|
|
||||||
/registries:
|
/registries:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@ -1045,56 +945,6 @@ paths:
|
||||||
description: "Server error"
|
description: "Server error"
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/GenericError"
|
$ref: "#/definitions/GenericError"
|
||||||
/registries/{id}/access:
|
|
||||||
put:
|
|
||||||
tags:
|
|
||||||
- "registries"
|
|
||||||
summary: "Manage accesses to a registry"
|
|
||||||
description: |
|
|
||||||
Manage user and team accesses to a registry.
|
|
||||||
**Access policy**: administrator
|
|
||||||
operationId: "RegistryAccessUpdate"
|
|
||||||
consumes:
|
|
||||||
- "application/json"
|
|
||||||
produces:
|
|
||||||
- "application/json"
|
|
||||||
security:
|
|
||||||
- jwt: []
|
|
||||||
parameters:
|
|
||||||
- name: "id"
|
|
||||||
in: "path"
|
|
||||||
description: "Registry identifier"
|
|
||||||
required: true
|
|
||||||
type: "integer"
|
|
||||||
- in: "body"
|
|
||||||
name: "body"
|
|
||||||
description: "Authorizations details"
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/RegistryAccessUpdateRequest"
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "Success"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/Registry"
|
|
||||||
400:
|
|
||||||
description: "Invalid request"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/GenericError"
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: "Invalid request data format"
|
|
||||||
404:
|
|
||||||
description: "Registry not found"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/GenericError"
|
|
||||||
examples:
|
|
||||||
application/json:
|
|
||||||
err: "Registry not found"
|
|
||||||
500:
|
|
||||||
description: "Server error"
|
|
||||||
schema:
|
|
||||||
$ref: "#/definitions/GenericError"
|
|
||||||
/resource_controls:
|
/resource_controls:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class="datatable">
|
<div class="datatable">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="{{$ctrl.titleIcon}}" title-text="{{ $ctrl.titleText }}">
|
<rd-widget-header icon="{{$ctrl.titleIcon}}" title-text="{{ $ctrl.titleText }}">
|
||||||
<file-uploader ng-if="$ctrl.isUploadAllowed" on-file-selected="$ctrl.onFileSelectedForUpload">
|
<file-uploader authorization="DockerAgentBrowsePut" ng-if="$ctrl.isUploadAllowed" on-file-selected="$ctrl.onFileSelectedForUpload">
|
||||||
</file-uploader>
|
</file-uploader>
|
||||||
</rd-widget-header>
|
</rd-widget-header>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
|
@ -71,14 +71,14 @@
|
||||||
{{ item.ModTime | getisodatefromtimestamp }}
|
{{ item.ModTime | getisodatefromtimestamp }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<btn class="btn btn-xs btn-primary space-right" ng-click="$ctrl.download({ name: item.Name })"
|
<btn authorization="DockerAgentBrowseGet" class="btn btn-xs btn-primary space-right" ng-click="$ctrl.download({ name: item.Name })"
|
||||||
ng-if="!item.Dir">
|
ng-if="!item.Dir">
|
||||||
<i class="fa fa-download" aria-hidden="true"></i> Download
|
<i class="fa fa-download" aria-hidden="true"></i> Download
|
||||||
</btn>
|
</btn>
|
||||||
<btn class="btn btn-xs btn-primary space-right" ng-click="item.newName = item.Name; item.edit = true">
|
<btn authorization="DockerAgentBrowseRename" class="btn btn-xs btn-primary space-right" ng-click="item.newName = item.Name; item.edit = true">
|
||||||
<i class="fa fa-edit" aria-hidden="true"></i> Rename
|
<i class="fa fa-edit" aria-hidden="true"></i> Rename
|
||||||
</btn>
|
</btn>
|
||||||
<btn class="btn btn-xs btn-danger" ng-click="$ctrl.delete({ name: item.Name })">
|
<btn authorization="DockerAgentBrowseDelete" class="btn btn-xs btn-danger" ng-click="$ctrl.delete({ name: item.Name })">
|
||||||
<i class="fa fa-trash" aria-hidden="true"></i> Delete
|
<i class="fa fa-trash" aria-hidden="true"></i> Delete
|
||||||
</btn>
|
</btn>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<div class="btn-group btn-group-xs" role="group" aria-label="..." style="display:inline-flex;">
|
<div class="btn-group btn-group-xs" role="group" aria-label="..." style="display:inline-flex;">
|
||||||
<a
|
<a
|
||||||
|
authorization="DockerContainerLogs"
|
||||||
ng-if="$ctrl.state.showQuickActionLogs && $ctrl.taskId === undefined"
|
ng-if="$ctrl.state.showQuickActionLogs && $ctrl.taskId === undefined"
|
||||||
style="margin: 0 2.5px;"
|
style="margin: 0 2.5px;"
|
||||||
ui-sref="docker.containers.container.logs({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
ui-sref="docker.containers.container.logs({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
||||||
|
@ -7,6 +8,7 @@
|
||||||
<i class="fa fa-file-alt space-right" aria-hidden="true"></i>
|
<i class="fa fa-file-alt space-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
authorization="DockerTaskLogs"
|
||||||
ng-if="$ctrl.state.showQuickActionLogs && $ctrl.taskId !== undefined"
|
ng-if="$ctrl.state.showQuickActionLogs && $ctrl.taskId !== undefined"
|
||||||
style="margin: 0 2.5px;"
|
style="margin: 0 2.5px;"
|
||||||
ui-sref="docker.tasks.task.logs({id: $ctrl.taskId})"
|
ui-sref="docker.tasks.task.logs({id: $ctrl.taskId})"
|
||||||
|
@ -14,6 +16,7 @@
|
||||||
<i class="fa fa-file-alt space-right" aria-hidden="true"></i>
|
<i class="fa fa-file-alt space-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
authorization="DockerContainerInspect"
|
||||||
ng-if="$ctrl.state.showQuickActionInspect && $ctrl.taskId === undefined"
|
ng-if="$ctrl.state.showQuickActionInspect && $ctrl.taskId === undefined"
|
||||||
style="margin: 0 2.5px;"
|
style="margin: 0 2.5px;"
|
||||||
ui-sref="docker.containers.container.inspect({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
ui-sref="docker.containers.container.inspect({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
||||||
|
@ -21,6 +24,7 @@
|
||||||
<i class="fa fa-info-circle space-right" aria-hidden="true"></i>
|
<i class="fa fa-info-circle space-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
authorization="DockerTaskInspect"
|
||||||
ng-if="$ctrl.state.showQuickActionInspect && $ctrl.taskId !== undefined"
|
ng-if="$ctrl.state.showQuickActionInspect && $ctrl.taskId !== undefined"
|
||||||
style="margin: 0 2.5px;"
|
style="margin: 0 2.5px;"
|
||||||
ui-sref="docker.tasks.task({id: $ctrl.taskId})"
|
ui-sref="docker.tasks.task({id: $ctrl.taskId})"
|
||||||
|
@ -28,6 +32,7 @@
|
||||||
<i class="fa fa-info-circle space-right" aria-hidden="true"></i>
|
<i class="fa fa-info-circle space-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
authorization="DockerContainerStats"
|
||||||
ng-if="$ctrl.state.showQuickActionStats && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1 && $ctrl.taskId === undefined"
|
ng-if="$ctrl.state.showQuickActionStats && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1 && $ctrl.taskId === undefined"
|
||||||
style="margin: 0 2.5px;"
|
style="margin: 0 2.5px;"
|
||||||
ui-sref="docker.containers.container.stats({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
ui-sref="docker.containers.container.stats({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
||||||
|
@ -35,6 +40,7 @@
|
||||||
<i class="fa fa-chart-area space-right" aria-hidden="true"></i>
|
<i class="fa fa-chart-area space-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
authorization="DockerExecStart"
|
||||||
ng-if="$ctrl.state.showQuickActionConsole && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1 && $ctrl.taskId === undefined"
|
ng-if="$ctrl.state.showQuickActionConsole && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1 && $ctrl.taskId === undefined"
|
||||||
style="margin: 0 2.5px;"
|
style="margin: 0 2.5px;"
|
||||||
ui-sref="docker.containers.container.exec({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
ui-sref="docker.containers.container.exec({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
||||||
|
@ -42,6 +48,7 @@
|
||||||
<i class="fa fa-terminal space-right" aria-hidden="true"></i>
|
<i class="fa fa-terminal space-right" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
|
authorization="DockerContainerAttach"
|
||||||
ng-if="$ctrl.state.showQuickActionConsole && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1 && $ctrl.taskId === undefined"
|
ng-if="$ctrl.state.showQuickActionConsole && ['starting', 'running', 'healthy', 'unhealthy'].indexOf($ctrl.status) !== -1 && $ctrl.taskId === undefined"
|
||||||
style="margin: 0 2.5px;"
|
style="margin: 0 2.5px;"
|
||||||
ui-sref="docker.containers.container.attach({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
ui-sref="docker.containers.container.attach({id: $ctrl.containerId, nodeName: $ctrl.nodeName})"
|
||||||
|
|
|
@ -5,14 +5,14 @@
|
||||||
<span>Name</span>
|
<span>Name</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select class="form-control" ng-model="$ctrl.state.editModel.name">
|
<select class="form-control" ng-model="$ctrl.state.editModel.name" disable-authorization="DockerContainerUpdate">
|
||||||
<option value="no">None</option>
|
<option value="no">None</option>
|
||||||
<option value="on-failure">On Failure</option>
|
<option value="on-failure">On Failure</option>
|
||||||
<option value="always">Always</option>
|
<option value="always">Always</option>
|
||||||
<option value="unless-stopped">Unless Stopped</option>
|
<option value="unless-stopped">Unless Stopped</option>
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
<td class="col-md-2">
|
<td class="col-md-2" authorization="DockerContainerUpdate">
|
||||||
<button class="btn btn-sm btn-primary" ng-click="$ctrl.save()">Update</button>
|
<button class="btn btn-sm btn-primary" ng-click="$ctrl.save()">Update</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar">
|
<div class="actionBar" authorization="DockerConfigDelete, DockerConfigCreate">
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger" authorization="DockerConfigDelete"
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.configs.new">
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.configs.new" authorization="DockerConfigCreate">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add config
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add config
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" authorization="DockerConfigDelete, DockerConfigCreate">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" authorization="DockerConfigDelete, DockerConfigCreate">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar">
|
<div class="actionBar">
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
<div class="row">
|
<div class="row" authorization="DockerNetworkConnect">
|
||||||
<label for="container_network" class="col-sm-3 col-lg-2 control-label text-left">Join a network</label>
|
<label for="container_network" class="col-sm-3 col-lg-2 control-label text-left">Join a network</label>
|
||||||
<div class="col-sm-5 col-lg-4">
|
<div class="col-sm-5 col-lg-4">
|
||||||
<select class="form-control" ng-model="$ctrl.selectedNetwork" id="container_network">
|
<select class="form-control" ng-model="$ctrl.selectedNetwork" id="container_network">
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
<td>{{ value.IPAddress || '-' }}</td>
|
<td>{{ value.IPAddress || '-' }}</td>
|
||||||
<td>{{ value.Gateway || '-' }}</td>
|
<td>{{ value.Gateway || '-' }}</td>
|
||||||
<td>{{ value.MacAddress || '-' }}</td>
|
<td>{{ value.MacAddress || '-' }}</td>
|
||||||
<td>
|
<td authorization="DockerNetworkDisconnect">
|
||||||
<button type="button" class="btn btn-xs btn-danger" ng-disabled="$ctrl.leaveNetworkActionInProgress" button-spinner="$ctrl.leaveNetworkActionInProgress" ng-click="$ctrl.leaveNetworkAction($ctrl.container, key)">
|
<button type="button" class="btn btn-xs btn-danger" ng-disabled="$ctrl.leaveNetworkActionInProgress" button-spinner="$ctrl.leaveNetworkActionInProgress" ng-click="$ctrl.leaveNetworkAction($ctrl.container, key)">
|
||||||
<span ng-hide="$ctrl.leaveNetworkActionInProgress"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i> Leave network</span>
|
<span ng-hide="$ctrl.leaveNetworkActionInProgress"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i> Leave network</span>
|
||||||
<span ng-show="$ctrl.leaveNetworkActionInProgress">Leaving network...</span>
|
<span ng-show="$ctrl.leaveNetworkActionInProgress">Leaving network...</span>
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
<div class="actionBar">
|
<div class="actionBar" authorization="DockerContainerStart, DockerContainerStop, DockerContainerKill, DockerContainerRestart, DockerContainerPause, DockerContainerUnpause, DockerContainerDelete, DockerContainerCreate">
|
||||||
<div class="btn-group" role="group" aria-label="...">
|
<div class="btn-group" role="group" aria-label="...">
|
||||||
<button type="button" class="btn btn-sm btn-success" ng-click="$ctrl.startAction($ctrl.selectedItems)"
|
<button authorization="DockerContainerStart" type="button" class="btn btn-sm btn-success" ng-click="$ctrl.startAction($ctrl.selectedItems)"
|
||||||
ng-disabled="$ctrl.selectedItemCount === 0 || $ctrl.noStoppedItemsSelected">
|
ng-disabled="$ctrl.selectedItemCount === 0 || $ctrl.noStoppedItemsSelected">
|
||||||
<i class="fa fa-play space-right" aria-hidden="true"></i>Start
|
<i class="fa fa-play space-right" aria-hidden="true"></i>Start
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.stopAction($ctrl.selectedItems)"
|
<button authorization="DockerContainerStop" type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.stopAction($ctrl.selectedItems)"
|
||||||
ng-disabled="$ctrl.selectedItemCount === 0 || $ctrl.noRunningItemsSelected">
|
ng-disabled="$ctrl.selectedItemCount === 0 || $ctrl.noRunningItemsSelected">
|
||||||
<i class="fa fa-stop space-right" aria-hidden="true"></i>Stop
|
<i class="fa fa-stop space-right" aria-hidden="true"></i>Stop
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.killAction($ctrl.selectedItems)"
|
<button authorization="DockerContainerKill" type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.killAction($ctrl.selectedItems)"
|
||||||
ng-disabled="$ctrl.selectedItemCount === 0">
|
ng-disabled="$ctrl.selectedItemCount === 0">
|
||||||
<i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill
|
<i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.restartAction($ctrl.selectedItems)"
|
<button authorization="DockerContainerRestart" type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.restartAction($ctrl.selectedItems)"
|
||||||
ng-disabled="$ctrl.selectedItemCount === 0">
|
ng-disabled="$ctrl.selectedItemCount === 0">
|
||||||
<i class="fa fa-sync space-right" aria-hidden="true"></i>Restart
|
<i class="fa fa-sync space-right" aria-hidden="true"></i>Restart
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.pauseAction($ctrl.selectedItems)"
|
<button authorization="DockerContainerPause" type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.pauseAction($ctrl.selectedItems)"
|
||||||
ng-disabled="$ctrl.selectedItemCount === 0 || $ctrl.noRunningItemsSelected">
|
ng-disabled="$ctrl.selectedItemCount === 0 || $ctrl.noRunningItemsSelected">
|
||||||
<i class="fa fa-pause space-right" aria-hidden="true"></i>Pause
|
<i class="fa fa-pause space-right" aria-hidden="true"></i>Pause
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.resumeAction($ctrl.selectedItems)"
|
<button authorization="DockerContainerUnpause" type="button" class="btn btn-sm btn-primary" ng-click="$ctrl.resumeAction($ctrl.selectedItems)"
|
||||||
ng-disabled="$ctrl.selectedItemCount === 0 || $ctrl.noPausedItemsSelected">
|
ng-disabled="$ctrl.selectedItemCount === 0 || $ctrl.noPausedItemsSelected">
|
||||||
<i class="fa fa-play space-right" aria-hidden="true"></i>Resume
|
<i class="fa fa-play space-right" aria-hidden="true"></i>Resume
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button authorization="DockerContainerDelete" type="button" class="btn btn-sm btn-danger"
|
||||||
ng-disabled="$ctrl.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.selectedItems)">
|
ng-disabled="$ctrl.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.containers.new" ng-if="$ctrl.showAddAction">
|
<button authorization="DockerContainerCreate" type="button" class="btn btn-sm btn-primary" ui-sref="docker.containers.new" ng-if="$ctrl.showAddAction">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add container
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add container
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -70,27 +70,29 @@
|
||||||
<label for="setting_container_trunc">Truncate container name</label>
|
<label for="setting_container_trunc">Truncate container name</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div authorization="DockerContainerStats, DockerContainerLogs, DockerExecStart, DockerContainerInspect, DockerTaskInspect, DockerTaskLogs">
|
||||||
<div class="menuHeader">
|
<div class="menuHeader">
|
||||||
Quick actions
|
Quick actions
|
||||||
</div>
|
</div>
|
||||||
<div class="menuContent">
|
<div class="menuContent">
|
||||||
<div class="md-checkbox">
|
<div class="md-checkbox" authorization="DockerContainerStats">
|
||||||
<input id="setting_show_stats" type="checkbox" ng-model="$ctrl.settings.showQuickActionStats" ng-change="$ctrl.onSettingsQuickActionChange()"/>
|
<input id="setting_show_stats" type="checkbox" ng-model="$ctrl.settings.showQuickActionStats" ng-change="$ctrl.onSettingsQuickActionChange()"/>
|
||||||
<label for="setting_show_stats">Stats</label>
|
<label for="setting_show_stats">Stats</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="md-checkbox">
|
<div class="md-checkbox" authorization="DockerContainerLogs">
|
||||||
<input id="setting_show_logs" type="checkbox" ng-model="$ctrl.settings.showQuickActionLogs" ng-change="$ctrl.onSettingsQuickActionChange()"/>
|
<input id="setting_show_logs" type="checkbox" ng-model="$ctrl.settings.showQuickActionLogs" ng-change="$ctrl.onSettingsQuickActionChange()"/>
|
||||||
<label for="setting_show_logs">Logs</label>
|
<label for="setting_show_logs">Logs</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="md-checkbox">
|
<div class="md-checkbox" authorization="DockerExecStart">
|
||||||
<input id="setting_show_console" type="checkbox" ng-model="$ctrl.settings.showQuickActionConsole" ng-change="$ctrl.onSettingsQuickActionChange()"/>
|
<input id="setting_show_console" type="checkbox" ng-model="$ctrl.settings.showQuickActionConsole" ng-change="$ctrl.onSettingsQuickActionChange()"/>
|
||||||
<label for="setting_show_console">Console</label>
|
<label for="setting_show_console">Console</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="md-checkbox">
|
<div class="md-checkbox" authorization="DockerContainerInspect">
|
||||||
<input id="setting_show_inspect" type="checkbox" ng-model="$ctrl.settings.showQuickActionInspect" ng-change="$ctrl.onSettingsQuickActionChange()"/>
|
<input id="setting_show_inspect" type="checkbox" ng-model="$ctrl.settings.showQuickActionInspect" ng-change="$ctrl.onSettingsQuickActionChange()"/>
|
||||||
<label for="setting_show_inspect">Inspect</label>
|
<label for="setting_show_inspect">Inspect</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -116,7 +118,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="DockerContainerStart, DockerContainerStop, DockerContainerKill, DockerContainerRestart, DockerContainerPause, DockerContainerUnpause, DockerContainerDelete, DockerContainerCreate">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -153,7 +155,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect" ng-show="$ctrl.columnVisibility.columns.actions.display">
|
<th ng-if="$ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect" ng-show="$ctrl.columnVisibility.columns.actions.display" authorization="DockerContainerStats, DockerContainerLogs, DockerExecStart, DockerContainerInspect, DockerTaskInspect, DockerTaskLogs">
|
||||||
Quick actions
|
Quick actions
|
||||||
</th>
|
</th>
|
||||||
<th ng-show="$ctrl.columnVisibility.columns.stack.display">
|
<th ng-show="$ctrl.columnVisibility.columns.stack.display">
|
||||||
|
@ -210,7 +212,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="DockerContainerStart, DockerContainerStop, DockerContainerKill, DockerContainerRestart, DockerContainerPause, DockerContainerUnpause, DockerContainerDelete, DockerContainerCreate">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -221,7 +223,7 @@
|
||||||
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) !== -1" class="label label-{{ item.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ item.Status }}</span>
|
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) !== -1" class="label label-{{ item.Status|containerstatusbadge }} interactive" uib-tooltip="This container has a health check">{{ item.Status }}</span>
|
||||||
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) === -1" class="label label-{{ item.Status|containerstatusbadge }}">{{ item.Status }}</span>
|
<span ng-if="['starting','healthy','unhealthy'].indexOf(item.Status) === -1" class="label label-{{ item.Status|containerstatusbadge }}">{{ item.Status }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td ng-if="!$ctrl.offlineMode && ($ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect)" ng-show="$ctrl.columnVisibility.columns.actions.display">
|
<td ng-if="!$ctrl.offlineMode && ($ctrl.settings.showQuickActionStats || $ctrl.settings.showQuickActionLogs || $ctrl.settings.showQuickActionConsole || $ctrl.settings.showQuickActionInspect)" ng-show="$ctrl.columnVisibility.columns.actions.display" authorization="DockerContainerStats, DockerContainerLogs, DockerExecStart, DockerContainerInspect, DockerTaskInspect, DockerTaskLogs">
|
||||||
<container-quick-actions container-id="item.Id" node-name="item.NodeName" status="item.Status" state="$ctrl.settings"></container-quick-actions>
|
<container-quick-actions container-id="item.Id" node-name="item.NodeName" status="item.Status" state="$ctrl.settings"></container-quick-actions>
|
||||||
</td>
|
</td>
|
||||||
<td ng-if="$ctrl.offlineMode">
|
<td ng-if="$ctrl.offlineMode">
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar" ng-if="!$ctrl.offlineMode">
|
<div class="actionBar" ng-if="!$ctrl.offlineMode" authorization="DockerImageDelete, DockerImageBuild, DockerImageLoad, DockerImageGet">
|
||||||
<div class="btn-group">
|
<div class="btn-group" authorization="DockerImageDelete">
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger"
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems, false)">
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems, false)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
|
@ -20,15 +20,15 @@
|
||||||
<li><a ng-click="$ctrl.forceRemoveAction($ctrl.state.selectedItems, true)" ng-disabled="$ctrl.state.selectedItemCount === 0">Force Remove</a></li>
|
<li><a ng-click="$ctrl.forceRemoveAction($ctrl.state.selectedItems, true)" ng-disabled="$ctrl.state.selectedItemCount === 0">Force Remove</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.images.build">
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.images.build" authorization="DockerImageBuild">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Build a new image
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Build a new image
|
||||||
</button>
|
</button>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button type="button" class="btn btn-sm btn-primary" ng-disabled="$ctrl.exportInProgress" ui-sref="docker.images.import">
|
<button type="button" class="btn btn-sm btn-primary" ng-disabled="$ctrl.exportInProgress" ui-sref="docker.images.import" authorization="DockerImageLoad">
|
||||||
<i class="fa fa-upload space-right" aria-hidden="true"></i>Import
|
<i class="fa fa-upload space-right" aria-hidden="true"></i>Import
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ng-disabled="$ctrl.state.selectedItemCount === 0 || $ctrl.exportInProgress"
|
<button type="button" class="btn btn-sm btn-primary" ng-disabled="$ctrl.state.selectedItemCount === 0 || $ctrl.exportInProgress"
|
||||||
ng-click="$ctrl.downloadAction($ctrl.state.selectedItems)" button-spinner="$ctrl.exportInProgress">
|
ng-click="$ctrl.downloadAction($ctrl.state.selectedItems)" button-spinner="$ctrl.exportInProgress" authorization="DockerImageGet">
|
||||||
<i class="fa fa-download space-right" aria-hidden="true"></i>
|
<i class="fa fa-download space-right" aria-hidden="true"></i>
|
||||||
<span ng-hide="$ctrl.exportInProgress">Export</span>
|
<span ng-hide="$ctrl.exportInProgress">Export</span>
|
||||||
<span ng-show="$ctrl.exportInProgress">Export in progress...</span>
|
<span ng-show="$ctrl.exportInProgress">Export in progress...</span>
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar" ng-if="!$ctrl.offlineMode">
|
<div class="actionBar" ng-if="!$ctrl.offlineMode" authorization="DockerNetworkDelete, DockerNetworkCreate">
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger" authorization="DockerNetworkDelete"
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.networks.new">
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.networks.new" authorization="DockerNetworkCreate">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add network
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add network
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar">
|
<div class="actionBar" authorization="DockerSecretDelete, DockerSecretCreate">
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger" authorization="DockerSecretDelete"
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.secrets.new">
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.secrets.new" authorization="DockerSecretCreate">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add secret
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add secret
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" authorization="DockerSecretDelete, DockerSecretCreate">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" authorization="DockerSecretDelete, DockerSecretCreate">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<div class="actionBar">
|
<div class="actionBar" authorization="DockerServiceUpdate, DockerServiceDelete, DockerServiceCreate">
|
||||||
<div class="btn-group" role="group" aria-label="...">
|
<div class="btn-group" role="group" aria-label="...">
|
||||||
<button ng-if="$ctrl.showUpdateAction" type="button" class="btn btn-sm btn-primary"
|
<button ng-if="$ctrl.showUpdateAction" type="button" class="btn btn-sm btn-primary" authorization="DockerServiceUpdate"
|
||||||
ng-disabled="$ctrl.selectedItemCount === 0" ng-click="$ctrl.updateAction($ctrl.selectedItems)">
|
ng-disabled="$ctrl.selectedItemCount === 0" ng-click="$ctrl.updateAction($ctrl.selectedItems)">
|
||||||
<i class="fa fa-sync space-right" aria-hidden="true"></i>Update
|
<i class="fa fa-sync space-right" aria-hidden="true"></i>Update
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger" authorization="DockerServiceDelete"
|
||||||
ng-disabled="$ctrl.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.selectedItems)">
|
ng-disabled="$ctrl.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.services.new" ng-if="$ctrl.showAddAction">
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.services.new" ng-if="$ctrl.showAddAction" authorization="DockerServiceCreate">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add service
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add service
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width:55px;">
|
<th style="width:55px;">
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" authorization="DockerServiceUpdate, DockerServiceDelete, DockerServiceCreate">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -83,7 +83,7 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-click="$ctrl.expandItem(item, !item.Expanded)" dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}" class="interactive">
|
<tr ng-click="$ctrl.expandItem(item, !item.Expanded)" dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}" class="interactive">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox" authorization="DockerServiceUpdate, DockerServiceDelete, DockerServiceCreate">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
<td ng-controller="ServicesDatatableActionsController as actionCtrl">
|
<td ng-controller="ServicesDatatableActionsController as actionCtrl">
|
||||||
{{ item.Mode }}
|
{{ item.Mode }}
|
||||||
<code>{{ item.Tasks | runningtaskscount }}</code> / <code>{{ item.Mode === 'replicated' ? item.Replicas : ($ctrl.nodes | availablenodecount:item) }}</code>
|
<code>{{ item.Tasks | runningtaskscount }}</code> / <code>{{ item.Mode === 'replicated' ? item.Replicas : ($ctrl.nodes | availablenodecount:item) }}</code>
|
||||||
<span ng-if="item.Mode === 'replicated' && !item.Scale">
|
<span ng-if="item.Mode === 'replicated' && !item.Scale" authorization="DockerServiceUpdate">
|
||||||
<a class="interactive" ng-click="item.Scale = true; item.ReplicaCount = item.Replicas; $event.stopPropagation();">
|
<a class="interactive" ng-click="item.Scale = true; item.ReplicaCount = item.Replicas; $event.stopPropagation();">
|
||||||
<i class="fa fa-arrows-alt-v" aria-hidden="true"></i> Scale
|
<i class="fa fa-arrows-alt-v" aria-hidden="true"></i> Scale
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -6,12 +6,12 @@
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar" ng-if="!$ctrl.offlineMode">
|
<div class="actionBar" ng-if="!$ctrl.offlineMode" authorization="DockerVolumeDelete, DockerVolumeCreate">
|
||||||
<button type="button" class="btn btn-sm btn-danger"
|
<button type="button" class="btn btn-sm btn-danger" authorization="DockerVolumeDelete"
|
||||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.volumes.new">
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="docker.volumes.new" authorization="DockerVolumeCreate">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add volume
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add volume
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.usage.open">
|
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.usage.open">
|
||||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="DockerVolumeDelete, DockerVolumeCreate">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
</span>
|
</span>
|
||||||
|
@ -105,13 +105,13 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="DockerVolumeDelete, DockerVolumeCreate">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</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>
|
<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>
|
<span ng-if="$ctrl.offlineMode">{{ item.Id | truncate:40 }}</span>
|
||||||
<a 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">
|
<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>
|
<i class="fa fa-search"></i> browse</a>
|
||||||
</a>
|
</a>
|
||||||
<span style="margin-left: 10px;" class="label label-warning image-tag space-left" ng-if="item.dangling">Unused</span>
|
<span style="margin-left: 10px;" class="label label-warning image-tag space-left" ng-if="item.dangling">Unused</span>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list">
|
||||||
<a ui-sref="docker.dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer-alt fa-fw"></span></a>
|
<a ui-sref="docker.dashboard" ui-sref-active="active">Dashboard <span class="menu-icon fa fa-tachometer-alt fa-fw"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="!$ctrl.offlineMode">
|
<li class="sidebar-list" ng-if="!$ctrl.offlineMode" authorization="DockerContainerCreate, PortainerStackCreate">
|
||||||
<a ui-sref="portainer.templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
|
<a ui-sref="portainer.templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list">
|
||||||
|
|
|
@ -19,8 +19,7 @@ angular.module('portainer.docker')
|
||||||
|
|
||||||
function initComponent() {
|
function initComponent() {
|
||||||
if (StateManager.getState().application.authentication) {
|
if (StateManager.getState().application.authentication) {
|
||||||
var userDetails = Authentication.getUserDetails();
|
var isAdmin = Authentication.isAdmin();
|
||||||
var isAdmin = userDetails.role === 1 ? true : false;
|
|
||||||
ctrl.isAdmin = isAdmin;
|
ctrl.isAdmin = isAdmin;
|
||||||
}
|
}
|
||||||
var provider = ctrl.applicationState.endpoint.mode.provider;
|
var provider = ctrl.applicationState.endpoint.mode.provider;
|
||||||
|
|
|
@ -77,7 +77,7 @@ class CreateConfigController {
|
||||||
async create() {
|
async create() {
|
||||||
let accessControlData = this.formValues.AccessControlData;
|
let accessControlData = this.formValues.AccessControlData;
|
||||||
let userDetails = this.Authentication.getUserDetails();
|
let userDetails = this.Authentication.getUserDetails();
|
||||||
let isAdmin = userDetails.role === 1;
|
let isAdmin = this.Authentication.isAdmin();
|
||||||
|
|
||||||
if (this.formValues.ConfigContent === "") {
|
if (this.formValues.ConfigContent === "") {
|
||||||
this.state.formValidationError = "Config content must not be empty";
|
this.state.formValidationError = "Config content must not be empty";
|
||||||
|
|
|
@ -24,8 +24,8 @@
|
||||||
<td>ID</td>
|
<td>ID</td>
|
||||||
<td>
|
<td>
|
||||||
{{ config.Id }}
|
{{ config.Id }}
|
||||||
<button class="btn btn-xs btn-danger" ng-click="removeConfig(config.Id)"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this config</button>
|
<button authorization="DockerConfigDelete" class="btn btn-xs btn-danger" ng-click="removeConfig(config.Id)"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this config</button>
|
||||||
<button class="btn btn-xs btn-primary" ui-sref="docker.configs.new({id: config.Id})"><i class="fa fa-copy space-right" aria-hidden="true"></i>Clone config</button>
|
<button authorization="DockerConfigCreate"class="btn btn-xs btn-primary" ui-sref="docker.configs.new({id: config.Id})"><i class="fa fa-copy space-right" aria-hidden="true"></i>Clone config</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -633,8 +633,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
||||||
$scope.availableLoggingDrivers = loggingDrivers;
|
$scope.availableLoggingDrivers = loggingDrivers;
|
||||||
});
|
});
|
||||||
|
|
||||||
var userDetails = Authentication.getUserDetails();
|
$scope.isAdmin = Authentication.isAdmin();
|
||||||
$scope.isAdmin = userDetails.role === 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateForm(accessControlData, isAdmin) {
|
function validateForm(accessControlData, isAdmin) {
|
||||||
|
@ -845,10 +844,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
|
||||||
|
|
||||||
function validateAccessControl() {
|
function validateAccessControl() {
|
||||||
var accessControlData = $scope.formValues.AccessControlData;
|
var accessControlData = $scope.formValues.AccessControlData;
|
||||||
var userDetails = Authentication.getUserDetails();
|
return validateForm(accessControlData, $scope.isAdmin);
|
||||||
var isAdmin = userDetails.role === 1;
|
|
||||||
|
|
||||||
return validateForm(accessControlData, isAdmin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSuccess() {
|
function onSuccess() {
|
||||||
|
|
|
@ -6,21 +6,21 @@
|
||||||
</rd-header-content>
|
</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row" authorization="DockerContainerStart, DockerContainerStop, DockerContainerKill, DockerContainerRestart, DockerContainerPause, DockerContainerUnpause, DockerContainerDelete, DockerContainerCreate">
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-cogs" title-text="Actions"></rd-widget-header>
|
<rd-widget-header icon="fa-cogs" title-text="Actions"></rd-widget-header>
|
||||||
<rd-widget-body classes="padding">
|
<rd-widget-body classes="padding">
|
||||||
<div class="btn-group" role="group" aria-label="...">
|
<div class="btn-group" role="group" aria-label="...">
|
||||||
<button class="btn btn-success btn-sm" ng-click="start()" ng-disabled="container.State.Running"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
|
<button authorization="DockerContainerStart" class="btn btn-success btn-sm" ng-click="start()" ng-disabled="container.State.Running"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
|
||||||
<button class="btn btn-danger btn-sm" ng-click="stop()" ng-disabled="!container.State.Running"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
|
<button authorization="DockerContainerStop" class="btn btn-danger btn-sm" ng-click="stop()" ng-disabled="!container.State.Running"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
|
||||||
<button class="btn btn-danger btn-sm" ng-click="kill()" ng-disabled="!container.State.Running"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
|
<button authorization=DockerContainerKill" class="btn btn-danger btn-sm" ng-click="kill()" ng-disabled="!container.State.Running"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
|
||||||
<button class="btn btn-primary btn-sm" ng-click="restart()" ng-disabled="!container.State.Running"><i class="fa fa-sync space-right" aria-hidden="true"></i>Restart</button>
|
<button authorization="DockerContainerRestart" class="btn btn-primary btn-sm" ng-click="restart()" ng-disabled="!container.State.Running"><i class="fa fa-sync space-right" aria-hidden="true"></i>Restart</button>
|
||||||
<button class="btn btn-primary btn-sm" ng-click="pause()" ng-disabled="!container.State.Running || container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
<button authorization="DockerContainerPause" class="btn btn-primary btn-sm" ng-click="pause()" ng-disabled="!container.State.Running || container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
||||||
<button class="btn btn-primary btn-sm" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
<button authorization="DockerContainerUnpause" class="btn btn-primary btn-sm" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
||||||
<button class="btn btn-danger btn-sm" ng-click="confirmRemove()"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove</button>
|
<button authorization="DockerContainerDelete" class="btn btn-danger btn-sm" ng-click="confirmRemove()"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group" role="group" aria-label="..." ng-if="!container.Config.Labels['com.docker.swarm.service.id']">
|
<div class="btn-group" role="group" aria-label="..." ng-if="!container.Config.Labels['com.docker.swarm.service.id']" authorization="DockerContainerCreate">
|
||||||
<button type="button" class="btn btn-danger btn-sm" ng-disabled="state.recreateContainerInProgress" ng-click="recreate()" button-spinner="state.recreateContainerInProgress">
|
<button type="button" class="btn btn-danger btn-sm" ng-disabled="state.recreateContainerInProgress" ng-click="recreate()" button-spinner="state.recreateContainerInProgress">
|
||||||
<span ng-hide="state.recreateContainerInProgress"><i class="fa fa-sync space-right" aria-hidden="true"></i>Recreate</span>
|
<span ng-hide="state.recreateContainerInProgress"><i class="fa fa-sync space-right" aria-hidden="true"></i>Recreate</span>
|
||||||
<span ng-show="state.recreateContainerInProgress">Recreation in progress...</span>
|
<span ng-show="state.recreateContainerInProgress">Recreation in progress...</span>
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
<td>Name</td>
|
<td>Name</td>
|
||||||
<td ng-if="!container.edit">
|
<td ng-if="!container.edit">
|
||||||
{{ container.Name|trimcontainername }}
|
{{ container.Name|trimcontainername }}
|
||||||
<a href="" data-toggle="tooltip" title="Edit container name" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
|
<a authorization="DockerContainerRename" href="" data-toggle="tooltip" title="Edit container name" ng-click="container.edit = true;"><i class="fa fa-edit"></i></a>
|
||||||
</td>
|
</td>
|
||||||
<td ng-if="container.edit">
|
<td ng-if="container.edit">
|
||||||
<form ng-submit="renameContainer()">
|
<form ng-submit="renameContainer()">
|
||||||
|
@ -81,14 +81,14 @@
|
||||||
<td>Finished</td>
|
<td>Finished</td>
|
||||||
<td>{{ container.State.FinishedAt|getisodate }}</td>
|
<td>{{ container.State.FinishedAt|getisodate }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr authorization="DockerContainerLogs, DockerContainerInspect, DockerContainerStats, DockerExecStart">
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<div class="btn-group" role="group" aria-label="...">
|
<div class="btn-group" role="group" aria-label="...">
|
||||||
<a class="btn" type="button" ui-sref="docker.containers.container.logs({ id: container.Id })"><i class="fa fa-file-alt space-right" aria-hidden="true"></i>Logs</a>
|
<a authorization="DockerContainerLogs" class="btn" type="button" ui-sref="docker.containers.container.logs({ id: container.Id })"><i class="fa fa-file-alt space-right" aria-hidden="true"></i>Logs</a>
|
||||||
<a class="btn" type="button" ui-sref="docker.containers.container.inspect({ id: container.Id })"><i class="fa fa-info-circle space-right" aria-hidden="true"></i>Inspect</a>
|
<a authorization="DockerContainerInspect" class="btn" type="button" ui-sref="docker.containers.container.inspect({ id: container.Id })"><i class="fa fa-info-circle space-right" aria-hidden="true"></i>Inspect</a>
|
||||||
<a class="btn" type="button" ui-sref="docker.containers.container.stats({ id: container.Id })"><i class="fa fa-chart-area space-right" aria-hidden="true"></i>Stats</a>
|
<a authorization="DockerContainerStats" class="btn" type="button" ui-sref="docker.containers.container.stats({ id: container.Id })"><i class="fa fa-chart-area space-right" aria-hidden="true"></i>Stats</a>
|
||||||
<a class="btn" type="button" ui-sref="docker.containers.container.exec({ id: container.Id })"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Exec</a>
|
<a authorization="DockerExecStart" class="btn" type="button" ui-sref="docker.containers.container.exec({ id: container.Id })"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Console</a>
|
||||||
<a class="btn" type="button" ui-sref="docker.containers.container.attach({ id: container.Id })"><i class="fa fa-plug space-right" aria-hidden="true"></i>Attach</a>
|
<a authorization="DockerContainerAttach" class="btn" type="button" ui-sref="docker.containers.container.attach({ id: container.Id })"><i class="fa fa-plug space-right" aria-hidden="true"></i>Attach</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row" authorization="DockerImageCreate">
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-header icon="fa-clone" title-text="Create image"></rd-widget-header>
|
<rd-widget-header icon="fa-clone" title-text="Create image"></rd-widget-header>
|
||||||
|
|
|
@ -19,7 +19,7 @@ angular.module('portainer.docker').controller('HostViewController', [
|
||||||
function initView() {
|
function initView() {
|
||||||
var applicationState = StateManager.getState();
|
var applicationState = StateManager.getState();
|
||||||
ctrl.state.isAgent = applicationState.endpoint.mode.agentProxy;
|
ctrl.state.isAgent = applicationState.endpoint.mode.agentProxy;
|
||||||
ctrl.state.isAdmin = Authentication.getUserDetails().role === 1;
|
ctrl.state.isAdmin = Authentication.isAdmin();
|
||||||
var agentApiVersion = applicationState.endpoint.agentApiVersion;
|
var agentApiVersion = applicationState.endpoint.agentApiVersion;
|
||||||
ctrl.state.agentApiVersion = agentApiVersion;
|
ctrl.state.agentApiVersion = agentApiVersion;
|
||||||
ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures;
|
ctrl.state.enableHostManagementFeatures = applicationState.application.enableHostManagementFeatures;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue