diff --git a/.eslintrc.yml b/.eslintrc.yml index 235dd5cd6..f1c43d95c 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -2,6 +2,7 @@ env: browser: true jquery: true node: true + es6: true globals: angular: true diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index c4ef68423..adc3df1e7 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -14,6 +14,7 @@ import ( "github.com/portainer/portainer/api/bolt/migrator" "github.com/portainer/portainer/api/bolt/registry" "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/settings" "github.com/portainer/portainer/api/bolt/stack" @@ -37,6 +38,7 @@ type Store struct { db *bolt.DB checkForDataMigration bool fileService portainer.FileService + RoleService *role.Service DockerHubService *dockerhub.Service EndpointGroupService *endpointgroup.Service EndpointService *endpoint.Service @@ -89,29 +91,6 @@ func (store *Store) Open() error { 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. func (store *Store) Close() error { if store.db != nil { @@ -140,6 +119,7 @@ func (store *Store) MigrateData() error { EndpointGroupService: store.EndpointGroupService, EndpointService: store.EndpointService, ExtensionService: store.ExtensionService, + RegistryService: store.RegistryService, ResourceControlService: store.ResourceControlService, SettingsService: store.SettingsService, StackService: store.StackService, @@ -162,6 +142,12 @@ func (store *Store) MigrateData() 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) if err != nil { return err diff --git a/api/bolt/init.go b/api/bolt/init.go new file mode 100644 index 000000000..81ee26dff --- /dev/null +++ b/api/bolt/init.go @@ -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 +} diff --git a/api/bolt/migrator/migrate_dbversion17.go b/api/bolt/migrator/migrate_dbversion17.go new file mode 100644 index 000000000..4e17090a8 --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion17.go @@ -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 +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index 705de70b7..49577f85a 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -6,6 +6,7 @@ import ( "github.com/portainer/portainer/api/bolt/endpoint" "github.com/portainer/portainer/api/bolt/endpointgroup" "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/settings" "github.com/portainer/portainer/api/bolt/stack" @@ -22,6 +23,7 @@ type ( endpointGroupService *endpointgroup.Service endpointService *endpoint.Service extensionService *extension.Service + registryService *registry.Service resourceControlService *resourcecontrol.Service settingsService *settings.Service stackService *stack.Service @@ -38,6 +40,7 @@ type ( EndpointGroupService *endpointgroup.Service EndpointService *endpoint.Service ExtensionService *extension.Service + RegistryService *registry.Service ResourceControlService *resourcecontrol.Service SettingsService *settings.Service StackService *stack.Service @@ -56,6 +59,7 @@ func NewMigrator(parameters *Parameters) *Migrator { endpointGroupService: parameters.EndpointGroupService, endpointService: parameters.EndpointService, extensionService: parameters.ExtensionService, + registryService: parameters.RegistryService, resourceControlService: parameters.ResourceControlService, settingsService: parameters.SettingsService, 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) } diff --git a/api/bolt/role/role.go b/api/bolt/role/role.go new file mode 100644 index 000000000..8a4e3e975 --- /dev/null +++ b/api/bolt/role/role.go @@ -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) + }) +} diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index f0a13ebdc..ea39c358e 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -377,18 +377,18 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain endpointID := endpointService.GetNextIdentifier() endpoint := &portainer.Endpoint{ - ID: portainer.EndpointID(endpointID), - Name: "primary", - URL: *flags.EndpointURL, - GroupID: portainer.EndpointGroupID(1), - Type: portainer.DockerEnvironment, - TLSConfig: tlsConfiguration, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Extensions: []portainer.EndpointExtension{}, - Tags: []string{}, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + ID: portainer.EndpointID(endpointID), + Name: "primary", + URL: *flags.EndpointURL, + GroupID: portainer.EndpointGroupID(1), + Type: portainer.DockerEnvironment, + TLSConfig: tlsConfiguration, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + Tags: []string{}, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, } if strings.HasPrefix(endpoint.URL, "tcp://") { @@ -420,18 +420,18 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo endpointID := endpointService.GetNextIdentifier() endpoint := &portainer.Endpoint{ - ID: portainer.EndpointID(endpointID), - Name: "primary", - URL: endpointURL, - GroupID: portainer.EndpointGroupID(1), - Type: portainer.DockerEnvironment, - TLSConfig: portainer.TLSConfiguration{}, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Extensions: []portainer.EndpointExtension{}, - Tags: []string{}, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + ID: portainer.EndpointID(endpointID), + Name: "primary", + URL: endpointURL, + GroupID: portainer.EndpointGroupID(1), + Type: portainer.DockerEnvironment, + TLSConfig: portainer.TLSConfiguration{}, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + Tags: []string{}, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, } return snapshotAndPersistEndpoint(endpoint, endpointService, snapshotter) @@ -647,6 +647,7 @@ func main() { AssetsPath: *flags.Assets, AuthDisabled: *flags.NoAuth, EndpointManagement: endpointManagement, + RoleService: store.RoleService, UserService: store.UserService, TeamService: store.TeamService, TeamMembershipService: store.TeamMembershipService, diff --git a/api/errors.go b/api/errors.go index ef11d5522..8e09838a1 100644 --- a/api/errors.go +++ b/api/errors.go @@ -4,6 +4,7 @@ package portainer const ( ErrUnauthorized = Error("Unauthorized") ErrResourceAccessDenied = Error("Access denied to resource") + ErrAuthorizationRequired = Error("Authorization required for this operation") ErrObjectNotFound = Error("Object not found inside the database") ErrMissingSecurityContext = Error("Unable to find security details in request context") ) diff --git a/api/exec/extension.go b/api/exec/extension.go index 3eec0a9e1..b0b015599 100644 --- a/api/exec/extension.go +++ b/api/exec/extension.go @@ -9,6 +9,7 @@ import ( "runtime" "strconv" "strings" + "time" "github.com/orcaman/concurrent-map" "github.com/portainer/portainer/api" @@ -20,6 +21,7 @@ var extensionDownloadBaseURL = "https://portainer-io-assets.sfo2.digitaloceanspa var extensionBinaryMap = map[portainer.ExtensionID]string{ portainer.RegistryManagementExtension: "extension-registry-management", portainer.OAuthAuthenticationExtension: "extension-oauth-authentication", + portainer.RBACExtension: "extension-rbac", } // ExtensionManager represents a service used to @@ -206,6 +208,8 @@ func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Exte return err } + time.Sleep(3 * time.Second) + manager.processes.Set(processKey(extension.ID), extensionProcess) return nil } diff --git a/api/http/handler/auth/authenticate.go b/api/http/handler/auth/authenticate.go index 1dcb8c774..6462b03ca 100644 --- a/api/http/handler/auth/authenticate.go +++ b/api/http/handler/auth/authenticate.go @@ -117,11 +117,60 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError { tokenData := &portainer.TokenData{ - ID: user.ID, - Username: user.Username, - Role: user.Role, + ID: user.ID, + Username: user.Username, + 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) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err} diff --git a/api/http/handler/auth/authenticate_oauth.go b/api/http/handler/auth/authenticate_oauth.go index 91d0d4fcc..7144d01f3 100644 --- a/api/http/handler/auth/authenticate_oauth.go +++ b/api/http/handler/auth/authenticate_oauth.go @@ -3,8 +3,8 @@ package auth import ( "encoding/json" "io/ioutil" - "net/http" "log" + "net/http" "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" diff --git a/api/http/handler/auth/authorization.go b/api/http/handler/auth/authorization.go new file mode 100644 index 000000000..64f2e5cd8 --- /dev/null +++ b/api/http/handler/auth/authorization.go @@ -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 +} diff --git a/api/http/handler/auth/handler.go b/api/http/handler/auth/handler.go index ebf001132..8d10abf29 100644 --- a/api/http/handler/auth/handler.go +++ b/api/http/handler/auth/handler.go @@ -30,6 +30,9 @@ type Handler struct { TeamService portainer.TeamService TeamMembershipService portainer.TeamMembershipService ExtensionService portainer.ExtensionService + EndpointService portainer.EndpointService + EndpointGroupService portainer.EndpointGroupService + RoleService portainer.RoleService ProxyManager *proxy.Manager } diff --git a/api/http/handler/dockerhub/handler.go b/api/http/handler/dockerhub/handler.go index 5821e4219..7e2cb4bd0 100644 --- a/api/http/handler/dockerhub/handler.go +++ b/api/http/handler/dockerhub/handler.go @@ -25,9 +25,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/dockerhub", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet) h.Handle("/dockerhub", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut) return h } diff --git a/api/http/handler/endpointgroups/endpointgroup_create.go b/api/http/handler/endpointgroups/endpointgroup_create.go index a5f3fe8a4..6cfb72468 100644 --- a/api/http/handler/endpointgroups/endpointgroup_create.go +++ b/api/http/handler/endpointgroups/endpointgroup_create.go @@ -36,11 +36,11 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque } endpointGroup := &portainer.EndpointGroup{ - Name: payload.Name, - Description: payload.Description, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Tags: payload.Tags, + Name: payload.Name, + Description: payload.Description, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Tags: payload.Tags, } err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup) diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index a08c6b101..1aa523403 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -14,6 +14,8 @@ type endpointGroupUpdatePayload struct { Description string AssociatedEndpoints []portainer.EndpointID Tags []string + UserAccessPolicies portainer.UserAccessPolicies + TeamAccessPolicies portainer.TeamAccessPolicies } func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error { @@ -52,20 +54,30 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque 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) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err} } - endpoints, err := handler.EndpointService.Endpoints() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} - } - - for _, endpoint := range endpoints { - err = handler.updateEndpointGroup(endpoint, portainer.EndpointGroupID(endpointGroupID), payload.AssociatedEndpoints) + if payload.AssociatedEndpoints != nil { + endpoints, err := handler.EndpointService.Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + } + + for _, endpoint := range endpoints { + err = handler.updateEndpointGroup(endpoint, portainer.EndpointGroupID(endpointGroupID), payload.AssociatedEndpoints) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} + } } } diff --git a/api/http/handler/endpointgroups/endpointgroup_update_access.go b/api/http/handler/endpointgroups/endpointgroup_update_access.go deleted file mode 100644 index 369583f4f..000000000 --- a/api/http/handler/endpointgroups/endpointgroup_update_access.go +++ /dev/null @@ -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) -} diff --git a/api/http/handler/endpointgroups/handler.go b/api/http/handler/endpointgroups/handler.go index 3233a74df..c210373e9 100644 --- a/api/http/handler/endpointgroups/handler.go +++ b/api/http/handler/endpointgroups/handler.go @@ -22,17 +22,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } 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", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet) 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}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut) - h.Handle("/endpoint_groups/{id}/access", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupUpdateAccess))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut) 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 } diff --git a/api/http/handler/endpointproxy/handler.go b/api/http/handler/endpointproxy/handler.go index 4bb7560a9..7f0dec6b2 100644 --- a/api/http/handler/endpointproxy/handler.go +++ b/api/http/handler/endpointproxy/handler.go @@ -23,10 +23,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { requestBouncer: bouncer, } h.PathPrefix("/{id}/azure").Handler( - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI))) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI))) h.PathPrefix("/{id}/docker").Handler( - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI))) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI))) h.PathPrefix("/{id}/extensions/storidge").Handler( - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI))) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI))) return h } diff --git a/api/http/handler/endpointproxy/proxy_azure.go b/api/http/handler/endpointproxy/proxy_azure.go index 1b5539916..a756a4fb7 100644 --- a/api/http/handler/endpointproxy/proxy_azure.go +++ b/api/http/handler/endpointproxy/proxy_azure.go @@ -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} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false) 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 diff --git a/api/http/handler/endpointproxy/proxy_docker.go b/api/http/handler/endpointproxy/proxy_docker.go index 7a0834c82..b1b911452 100644 --- a/api/http/handler/endpointproxy/proxy_docker.go +++ b/api/http/handler/endpointproxy/proxy_docker.go @@ -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")} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) 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 diff --git a/api/http/handler/endpointproxy/proxy_storidge.go b/api/http/handler/endpointproxy/proxy_storidge.go index 497fef198..b375cee09 100644 --- a/api/http/handler/endpointproxy/proxy_storidge.go +++ b/api/http/handler/endpointproxy/proxy_storidge.go @@ -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} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false) 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 diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 34e06cca4..40348899f 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -172,19 +172,19 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po endpointID := handler.EndpointService.GetNextIdentifier() endpoint := &portainer.Endpoint{ - ID: portainer.EndpointID(endpointID), - Name: payload.Name, - URL: "https://management.azure.com", - Type: portainer.AzureEnvironment, - GroupID: portainer.EndpointGroupID(payload.GroupID), - PublicURL: payload.PublicURL, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Extensions: []portainer.EndpointExtension{}, - AzureCredentials: credentials, - Tags: payload.Tags, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + ID: portainer.EndpointID(endpointID), + Name: payload.Name, + URL: "https://management.azure.com", + Type: portainer.AzureEnvironment, + GroupID: portainer.EndpointGroupID(payload.GroupID), + PublicURL: payload.PublicURL, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + AzureCredentials: credentials, + Tags: payload.Tags, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, } err = handler.EndpointService.CreateEndpoint(endpoint) @@ -224,12 +224,12 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) TLSConfig: portainer.TLSConfiguration{ TLS: false, }, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Extensions: []portainer.EndpointExtension{}, - Tags: payload.Tags, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + Tags: payload.Tags, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, } err := handler.snapshotAndPersistEndpoint(endpoint) @@ -268,12 +268,12 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload) TLS: payload.TLS, TLSSkipVerify: payload.TLSSkipVerify, }, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Extensions: []portainer.EndpointExtension{}, - Tags: payload.Tags, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + Tags: payload.Tags, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, } filesystemError := handler.storeTLSFiles(endpoint, payload) diff --git a/api/http/handler/endpoints/endpoint_inspect.go b/api/http/handler/endpoints/endpoint_inspect.go index c21b92d5a..10cf34ed9 100644 --- a/api/http/handler/endpoints/endpoint_inspect.go +++ b/api/http/handler/endpoints/endpoint_inspect.go @@ -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} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false) 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) diff --git a/api/http/handler/endpoints/endpoint_job.go b/api/http/handler/endpoints/endpoint_job.go index c7219a35e..78d00bc9c 100644 --- a/api/http/handler/endpoints/endpoint_job.go +++ b/api/http/handler/endpoints/endpoint_job.go @@ -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} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) - if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied} - } - switch method { case "file": return handler.executeJobFromFile(w, r, endpoint, nodeName) diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 9adb95bca..f62ee638b 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -24,6 +24,8 @@ type endpointUpdatePayload struct { AzureTenantID *string AzureAuthenticationKey *string Tags []string + UserAccessPolicies portainer.UserAccessPolicies + TeamAccessPolicies portainer.TeamAccessPolicies } 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 } + if payload.UserAccessPolicies != nil { + endpoint.UserAccessPolicies = payload.UserAccessPolicies + } + + if payload.TeamAccessPolicies != nil { + endpoint.TeamAccessPolicies = payload.TeamAccessPolicies + } + if payload.Status != nil { switch *payload.Status { case 1: diff --git a/api/http/handler/endpoints/endpoint_update_access.go b/api/http/handler/endpoints/endpoint_update_access.go deleted file mode 100644 index 980ed7651..000000000 --- a/api/http/handler/endpoints/endpoint_update_access.go +++ /dev/null @@ -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) -} diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index fe0a889fb..03192f47f 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -37,32 +37,30 @@ type Handler struct { // NewHandler creates a handler to manage endpoint operations. func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *Handler { h := &Handler{ - Router: mux.NewRouter(), + Router: mux.NewRouter(), authorizeEndpointManagement: authorizeEndpointManagement, requestBouncer: bouncer, } 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", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost) 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}", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet) h.Handle("/endpoints/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut) - h.Handle("/endpoints/{id}/access", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointUpdateAccess))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut) 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", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost) 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", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost) 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 } diff --git a/api/http/handler/extensions/extension_create.go b/api/http/handler/extensions/extension_create.go index d3a2d881d..7e41c9595 100644 --- a/api/http/handler/extensions/extension_create.go +++ b/api/http/handler/extensions/extension_create.go @@ -70,6 +70,13 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request) 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) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err} diff --git a/api/http/handler/extensions/handler.go b/api/http/handler/extensions/handler.go index eb3d74ad1..a7ab38feb 100644 --- a/api/http/handler/extensions/handler.go +++ b/api/http/handler/extensions/handler.go @@ -12,8 +12,11 @@ import ( // Handler is the HTTP handler used to handle extension operations. type Handler struct { *mux.Router - ExtensionService portainer.ExtensionService - ExtensionManager portainer.ExtensionManager + ExtensionService portainer.ExtensionService + ExtensionManager portainer.ExtensionManager + EndpointGroupService portainer.EndpointGroupService + EndpointService portainer.EndpointService + RegistryService portainer.RegistryService } // NewHandler creates a handler to manage extension operations. @@ -23,15 +26,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { } h.Handle("/extensions", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet) 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}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet) 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", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost) return h } diff --git a/api/http/handler/extensions/upgrade.go b/api/http/handler/extensions/upgrade.go new file mode 100644 index 000000000..7e3203412 --- /dev/null +++ b/api/http/handler/extensions/upgrade.go @@ -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 +} diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 85f41d0be..4d867540b 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -4,6 +4,10 @@ import ( "net/http" "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/dockerhub" "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/registries" "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/stacks" "github.com/portainer/portainer/api/http/handler/status" @@ -30,8 +33,7 @@ import ( // Handler is a collection of all the service handlers. type Handler struct { - AuthHandler *auth.Handler - + AuthHandler *auth.Handler DockerHubHandler *dockerhub.Handler EndpointGroupHandler *endpointgroups.Handler EndpointHandler *endpoints.Handler @@ -41,6 +43,8 @@ type Handler struct { ExtensionHandler *extensions.Handler RegistryHandler *registries.Handler ResourceControlHandler *resourcecontrols.Handler + RoleHandler *roles.Handler + SchedulesHanlder *schedules.Handler SettingsHandler *settings.Handler StackHandler *stacks.Handler StatusHandler *status.Handler @@ -52,7 +56,6 @@ type Handler struct { UserHandler *users.Handler WebSocketHandler *websocket.Handler WebhookHandler *webhooks.Handler - SchedulesHanlder *schedules.Handler } // ServeHTTP delegates a request to the appropriate subhandler. @@ -75,14 +78,18 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { default: 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"): 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"): http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/resource_controls"): 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"): http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r) 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) case strings.HasPrefix(r.URL.Path, "/api/webhooks"): 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, "/"): h.FileHandler.ServeHTTP(w, r) } diff --git a/api/http/handler/motd/handler.go b/api/http/handler/motd/handler.go index 26f5b6a91..aa2d1d002 100644 --- a/api/http/handler/motd/handler.go +++ b/api/http/handler/motd/handler.go @@ -18,7 +18,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/motd", - bouncer.AuthenticatedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet) return h } diff --git a/api/http/handler/registries/handler.go b/api/http/handler/registries/handler.go index e38f4e4f4..3c90e6e67 100644 --- a/api/http/handler/registries/handler.go +++ b/api/http/handler/registries/handler.go @@ -33,19 +33,17 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { } h.Handle("/registries", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost) 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}", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet) h.Handle("/registries/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut) - h.Handle("/registries/{id}/access", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdateAccess))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut) 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}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete) h.PathPrefix("/registries/{id}/v2").Handler( bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI))) diff --git a/api/http/handler/registries/proxy.go b/api/http/handler/registries/proxy.go index d54520508..1da1ed88d 100644 --- a/api/http/handler/registries/proxy.go +++ b/api/http/handler/registries/proxy.go @@ -5,6 +5,8 @@ import ( "net/http" "strconv" + "github.com/portainer/portainer/api/http/security" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "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} } + 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) if err != nil { return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied} diff --git a/api/http/handler/registries/registry_create.go b/api/http/handler/registries/registry_create.go index ab412b937..1b6dbf638 100644 --- a/api/http/handler/registries/registry_create.go +++ b/api/http/handler/registries/registry_create.go @@ -53,14 +53,14 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) * } registry := &portainer.Registry{ - Type: portainer.RegistryType(payload.Type), - Name: payload.Name, - URL: payload.URL, - Authentication: payload.Authentication, - Username: payload.Username, - Password: payload.Password, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, + Type: portainer.RegistryType(payload.Type), + Name: payload.Name, + URL: payload.URL, + Authentication: payload.Authentication, + Username: payload.Username, + Password: payload.Password, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, } err = handler.RegistryService.CreateRegistry(registry) diff --git a/api/http/handler/registries/registry_update.go b/api/http/handler/registries/registry_update.go index 17f2304fd..14c11cbba 100644 --- a/api/http/handler/registries/registry_update.go +++ b/api/http/handler/registries/registry_update.go @@ -11,11 +11,13 @@ import ( ) type registryUpdatePayload struct { - Name string - URL string - Authentication bool - Username string - Password string + Name string + URL string + Authentication bool + Username string + Password string + UserAccessPolicies portainer.UserAccessPolicies + TeamAccessPolicies portainer.TeamAccessPolicies } func (payload *registryUpdatePayload) Validate(r *http.Request) error { @@ -73,6 +75,14 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) * 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) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err} diff --git a/api/http/handler/registries/registry_update_access.go b/api/http/handler/registries/registry_update_access.go deleted file mode 100644 index 4d8925f19..000000000 --- a/api/http/handler/registries/registry_update_access.go +++ /dev/null @@ -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) -} diff --git a/api/http/handler/roles/handler.go b/api/http/handler/roles/handler.go new file mode 100644 index 000000000..e6bb7c4c7 --- /dev/null +++ b/api/http/handler/roles/handler.go @@ -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 +} diff --git a/api/http/handler/roles/role_list.go b/api/http/handler/roles/role_list.go new file mode 100644 index 000000000..e39e38595 --- /dev/null +++ b/api/http/handler/roles/role_list.go @@ -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) +} diff --git a/api/http/handler/schedules/handler.go b/api/http/handler/schedules/handler.go index 5e5d0f46e..5a160cd3b 100644 --- a/api/http/handler/schedules/handler.go +++ b/api/http/handler/schedules/handler.go @@ -27,18 +27,18 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { } h.Handle("/schedules", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleList))).Methods(http.MethodGet) 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}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleInspect))).Methods(http.MethodGet) 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}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete) 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", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet) return h } diff --git a/api/http/handler/settings/handler.go b/api/http/handler/settings/handler.go index 34f467d5a..2a5348b2e 100644 --- a/api/http/handler/settings/handler.go +++ b/api/http/handler/settings/handler.go @@ -30,13 +30,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/settings", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet) 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", bouncer.PublicAccess(httperror.LoggerHandler(h.settingsPublic))).Methods(http.MethodGet) 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 } diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 977820ddd..0d1adae53 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -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} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) 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) { diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index a7174a8f5..2607fa10c 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -4,12 +4,13 @@ import ( "net/http" "strconv" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "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=&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} } - 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 // 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 @@ -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} } + 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) if err != nil { 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} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) 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{ diff --git a/api/http/handler/stacks/stack_file.go b/api/http/handler/stacks/stack_file.go index 776e3b3b0..fd9767ffd 100644 --- a/api/http/handler/stacks/stack_file.go +++ b/api/http/handler/stacks/stack_file.go @@ -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} } + 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) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} diff --git a/api/http/handler/stacks/stack_inspect.go b/api/http/handler/stacks/stack_inspect.go index 9ca2dc950..878a20361 100644 --- a/api/http/handler/stacks/stack_inspect.go +++ b/api/http/handler/stacks/stack_inspect.go @@ -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} } + 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) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} diff --git a/api/http/handler/stacks/stack_migrate.go b/api/http/handler/stacks/stack_migrate.go index 1a077f867..340d57310 100644 --- a/api/http/handler/stacks/stack_migrate.go +++ b/api/http/handler/stacks/stack_migrate.go @@ -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} } + 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) if err != nil && err != portainer.ErrObjectNotFound { 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) } - 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)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go index 1d5c995cc..63f8eb323 100644 --- a/api/http/handler/stacks/stack_update.go +++ b/api/http/handler/stacks/stack_update.go @@ -4,13 +4,14 @@ import ( "net/http" "strconv" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" + "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/http/proxy" - "github.com/portainer/portainer/api/http/security" ) 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} } - 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 // 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. @@ -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} } + 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) if updateError != nil { return updateError diff --git a/api/http/handler/tags/handler.go b/api/http/handler/tags/handler.go index fe8d011c7..d6461e2dc 100644 --- a/api/http/handler/tags/handler.go +++ b/api/http/handler/tags/handler.go @@ -21,11 +21,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/tags", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost) 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}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete) return h } diff --git a/api/http/handler/teammemberships/handler.go b/api/http/handler/teammemberships/handler.go index d2d712f28..3fd56bfc0 100644 --- a/api/http/handler/teammemberships/handler.go +++ b/api/http/handler/teammemberships/handler.go @@ -23,13 +23,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } 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", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet) 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}", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete) return h } diff --git a/api/http/handler/teams/handler.go b/api/http/handler/teams/handler.go index c86d3a2e4..1aad28ef0 100644 --- a/api/http/handler/teams/handler.go +++ b/api/http/handler/teams/handler.go @@ -23,17 +23,17 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/teams", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost) 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}", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet) 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}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete) 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 } diff --git a/api/http/handler/templates/handler.go b/api/http/handler/templates/handler.go index 7e14f2d37..026b137ee 100644 --- a/api/http/handler/templates/handler.go +++ b/api/http/handler/templates/handler.go @@ -27,15 +27,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { } h.Handle("/templates", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet) 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}", - 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}", - 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}", - bouncer.AdministratorAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateDelete)))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateDelete)))).Methods(http.MethodDelete) return h } diff --git a/api/http/handler/upload/handler.go b/api/http/handler/upload/handler.go index 757206a80..fe3060dac 100644 --- a/api/http/handler/upload/handler.go +++ b/api/http/handler/upload/handler.go @@ -22,6 +22,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } 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 } diff --git a/api/http/handler/users/handler.go b/api/http/handler/users/handler.go index ef20c6ccb..8e7f5035b 100644 --- a/api/http/handler/users/handler.go +++ b/api/http/handler/users/handler.go @@ -31,19 +31,19 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi Router: mux.NewRouter(), } h.Handle("/users", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost) 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}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet) 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}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete) 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", - 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", bouncer.PublicAccess(httperror.LoggerHandler(h.adminCheck))).Methods(http.MethodGet) h.Handle("/users/admin/init", diff --git a/api/http/handler/users/user_create.go b/api/http/handler/users/user_create.go index 5aead6b96..ab89cf1ff 100644 --- a/api/http/handler/users/user_create.go +++ b/api/http/handler/users/user_create.go @@ -60,6 +60,23 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http user = &portainer.User{ Username: payload.Username, 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() diff --git a/api/http/handler/webhooks/handler.go b/api/http/handler/webhooks/handler.go index 2e342114e..f2deb2e5c 100644 --- a/api/http/handler/webhooks/handler.go +++ b/api/http/handler/webhooks/handler.go @@ -24,11 +24,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/webhooks", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost) 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}", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookDelete))).Methods(http.MethodDelete) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookDelete))).Methods(http.MethodDelete) h.Handle("/webhooks/{token}", bouncer.PublicAccess(httperror.LoggerHandler(h.webhookExecute))).Methods(http.MethodPost) return h diff --git a/api/http/handler/websocket/attach.go b/api/http/handler/websocket/attach.go index b85c8d7d6..95702179c 100644 --- a/api/http/handler/websocket/attach.go +++ b/api/http/handler/websocket/attach.go @@ -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} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) 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{ diff --git a/api/http/handler/websocket/exec.go b/api/http/handler/websocket/exec.go index 7b7b8a753..3b3fad969 100644 --- a/api/http/handler/websocket/exec.go +++ b/api/http/handler/websocket/exec.go @@ -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} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) 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{ diff --git a/api/http/handler/websocket/handler.go b/api/http/handler/websocket/handler.go index 891257823..853dea038 100644 --- a/api/http/handler/websocket/handler.go +++ b/api/http/handler/websocket/handler.go @@ -25,8 +25,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { requestBouncer: bouncer, } h.PathPrefix("/websocket/exec").Handler( - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec))) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.websocketExec))) h.PathPrefix("/websocket/attach").Handler( - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketAttach))) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.websocketAttach))) return h } diff --git a/api/http/proxy/access_control.go b/api/http/proxy/access_control.go index c84f67c04..fea0014ec 100644 --- a/api/http/proxy/access_control.go +++ b/api/http/proxy/access_control.go @@ -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 a resource control is found and the user cannot access the resource. 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 { 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 // and the user cannot access the resource. 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) 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) return resourceObject, true } diff --git a/api/http/proxy/configs.go b/api/http/proxy/configs.go index e252a7cee..6863b5971 100644 --- a/api/http/proxy/configs.go +++ b/api/http/proxy/configs.go @@ -24,7 +24,7 @@ func configListOperation(response *http.Response, executor *operationExecutor) e return err } - if executor.operationContext.isAdmin { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { responseArray, err = decorateConfigList(responseArray, executor.operationContext.resourceControls) } else { responseArray, err = filterConfigList(responseArray, executor.operationContext) @@ -87,7 +87,7 @@ func decorateConfigList(configData []interface{}, resourceControls []portainer.R // Authorized configs are decorated during the process. // Resource controls checks are based on: resource identifier. // 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) for _, config := range configData { diff --git a/api/http/proxy/containers.go b/api/http/proxy/containers.go index a605a3c5f..81bde4c4f 100644 --- a/api/http/proxy/containers.go +++ b/api/http/proxy/containers.go @@ -26,7 +26,7 @@ func containerListOperation(response *http.Response, executor *operationExecutor return err } - if executor.operationContext.isAdmin { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { responseArray, err = decorateContainerList(responseArray, executor.operationContext.resourceControls) } else { responseArray, err = filterContainerList(responseArray, executor.operationContext) @@ -137,7 +137,7 @@ func decorateContainerList(containerData []interface{}, resourceControls []porta // Authorized containers are decorated during the process. // 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 -func filterContainerList(containerData []interface{}, context *restrictedOperationContext) ([]interface{}, error) { +func filterContainerList(containerData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { filteredContainerData := make([]interface{}, 0) for _, container := range containerData { diff --git a/api/http/proxy/docker_transport.go b/api/http/proxy/docker_transport.go index 110b65f83..873b099cf 100644 --- a/api/http/proxy/docker_transport.go +++ b/api/http/proxy/docker_transport.go @@ -24,12 +24,14 @@ type ( DockerHubService portainer.DockerHubService SettingsService portainer.SettingsService SignatureService portainer.DigitalSignatureService + endpointIdentifier portainer.EndpointID } - restrictedOperationContext struct { - isAdmin bool - userID portainer.UserID - userTeamIDs []portainer.TeamID - resourceControls []portainer.ResourceControl + restrictedDockerOperationContext struct { + isAdmin bool + endpointResourceAccess bool + userID portainer.UserID + userTeamIDs []portainer.TeamID + resourceControls []portainer.ResourceControl } registryAccessContext struct { isAdmin bool @@ -44,7 +46,7 @@ type ( Serveraddress string `json:"serveraddress"` } operationExecutor struct { - operationContext *restrictedOperationContext + operationContext *restrictedDockerOperationContext labelBlackList []portainer.Pair } restrictedOperationRequest func(*http.Response, *operationExecutor) error @@ -460,7 +462,7 @@ func (p *proxyTransport) createRegistryAccessContext(request *http.Request) (*re return accessContext, nil } -func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedOperationContext, error) { +func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedDockerOperationContext, error) { var err error tokenData, err := security.RetrieveTokenData(request) if err != nil { @@ -472,15 +474,21 @@ func (p *proxyTransport) createOperationContext(request *http.Request) (*restric return nil, err } - operationContext := &restrictedOperationContext{ - isAdmin: true, - userID: tokenData.ID, - resourceControls: resourceControls, + operationContext := &restrictedDockerOperationContext{ + isAdmin: true, + userID: tokenData.ID, + resourceControls: resourceControls, + endpointResourceAccess: false, } if tokenData.Role != portainer.AdministratorRole { operationContext.isAdmin = false + _, ok := tokenData.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess] + if ok { + operationContext.endpointResourceAccess = true + } + teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID) if err != nil { return nil, err diff --git a/api/http/proxy/factory.go b/api/http/proxy/factory.go index ee67ca5cf..7308b7f34 100644 --- a/api/http/proxy/factory.go +++ b/api/http/proxy/factory.go @@ -40,10 +40,10 @@ func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error 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" - proxy := factory.createDockerReverseProxy(u, enableSignature) + proxy := factory.createDockerReverseProxy(u, enableSignature, endpointID) config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify) if err != nil { return nil, err @@ -53,12 +53,12 @@ func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portaine 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" - 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) transport := &proxyTransport{ enableSignature: enableSignature, @@ -68,6 +68,7 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignatur RegistryService: factory.RegistryService, DockerHubService: factory.DockerHubService, dockerTransport: &http.Transport{}, + endpointIdentifier: endpointID, } if enableSignature { diff --git a/api/http/proxy/factory_local.go b/api/http/proxy/factory_local.go index da154e03c..431fd5604 100644 --- a/api/http/proxy/factory_local.go +++ b/api/http/proxy/factory_local.go @@ -4,9 +4,11 @@ package proxy import ( "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{} transport := &proxyTransport{ enableSignature: false, @@ -16,6 +18,7 @@ func (factory *proxyFactory) newLocalProxy(path string) http.Handler { RegistryService: factory.RegistryService, DockerHubService: factory.DockerHubService, dockerTransport: newSocketTransport(path), + endpointIdentifier: endpointID, } proxy.Transport = transport return proxy diff --git a/api/http/proxy/factory_local_windows.go b/api/http/proxy/factory_local_windows.go index 5e98e47ac..590553576 100644 --- a/api/http/proxy/factory_local_windows.go +++ b/api/http/proxy/factory_local_windows.go @@ -6,10 +6,10 @@ import ( "net" "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{} transport := &proxyTransport{ enableSignature: false, @@ -19,6 +19,7 @@ func (factory *proxyFactory) newLocalProxy(path string) http.Handler { RegistryService: factory.RegistryService, DockerHubService: factory.DockerHubService, dockerTransport: newNamedPipeTransport(path), + endpointIdentifier: endpointID, } proxy.Transport = transport return proxy diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index 328ad81ed..f7d875fcf 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -14,6 +14,7 @@ import ( var extensionPorts = map[portainer.ExtensionID]string{ portainer.RegistryManagementExtension: "7001", portainer.OAuthAuthenticationExtension: "7002", + portainer.RBACExtension: "7003", } type ( @@ -135,14 +136,14 @@ func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) 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 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) { @@ -153,10 +154,10 @@ func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, switch endpoint.Type { case portainer.AgentOnDockerEnvironment: - return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true) + return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true, endpoint.ID) case portainer.AzureEnvironment: return newAzureProxy(&endpoint.AzureCredentials) default: - return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig) + return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig, endpoint.ID) } } diff --git a/api/http/proxy/networks.go b/api/http/proxy/networks.go index 63290e4d3..084f8604a 100644 --- a/api/http/proxy/networks.go +++ b/api/http/proxy/networks.go @@ -24,7 +24,7 @@ func networkListOperation(response *http.Response, executor *operationExecutor) return err } - if executor.operationContext.isAdmin { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { responseArray, err = decorateNetworkList(responseArray, executor.operationContext.resourceControls) } else { responseArray, err = filterNetworkList(responseArray, executor.operationContext) @@ -110,7 +110,7 @@ func decorateNetworkList(networkData []interface{}, resourceControls []portainer // Authorized networks are decorated during the process. // 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 -func filterNetworkList(networkData []interface{}, context *restrictedOperationContext) ([]interface{}, error) { +func filterNetworkList(networkData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { filteredNetworkData := make([]interface{}, 0) for _, network := range networkData { diff --git a/api/http/proxy/secrets.go b/api/http/proxy/secrets.go index a49418bbb..67af61936 100644 --- a/api/http/proxy/secrets.go +++ b/api/http/proxy/secrets.go @@ -24,7 +24,7 @@ func secretListOperation(response *http.Response, executor *operationExecutor) e return err } - if executor.operationContext.isAdmin { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { responseArray, err = decorateSecretList(responseArray, executor.operationContext.resourceControls) } else { responseArray, err = filterSecretList(responseArray, executor.operationContext) @@ -87,7 +87,7 @@ func decorateSecretList(secretData []interface{}, resourceControls []portainer.R // Authorized secrets are decorated during the process. // Resource controls checks are based on: resource identifier. // 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) for _, secret := range secretData { diff --git a/api/http/proxy/services.go b/api/http/proxy/services.go index d48895278..b14b50a47 100644 --- a/api/http/proxy/services.go +++ b/api/http/proxy/services.go @@ -24,7 +24,7 @@ func serviceListOperation(response *http.Response, executor *operationExecutor) return err } - if executor.operationContext.isAdmin { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { responseArray, err = decorateServiceList(responseArray, executor.operationContext.resourceControls) } else { responseArray, err = filterServiceList(responseArray, executor.operationContext) @@ -118,7 +118,7 @@ func decorateServiceList(serviceData []interface{}, resourceControls []portainer // Authorized services are decorated during the process. // 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 -func filterServiceList(serviceData []interface{}, context *restrictedOperationContext) ([]interface{}, error) { +func filterServiceList(serviceData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { filteredServiceData := make([]interface{}, 0) for _, service := range serviceData { diff --git a/api/http/proxy/tasks.go b/api/http/proxy/tasks.go index e9d5bdd0a..1da630ff7 100644 --- a/api/http/proxy/tasks.go +++ b/api/http/proxy/tasks.go @@ -25,7 +25,7 @@ func taskListOperation(response *http.Response, executor *operationExecutor) err return err } - if !executor.operationContext.isAdmin { + if !executor.operationContext.isAdmin && !executor.operationContext.endpointResourceAccess { responseArray, err = filterTaskList(responseArray, executor.operationContext) if err != nil { return err @@ -54,7 +54,7 @@ func extractTaskLabelsFromTaskListObject(responseObject map[string]interface{}) // 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 // 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) for _, task := range taskData { diff --git a/api/http/proxy/volumes.go b/api/http/proxy/volumes.go index 4ee885609..59099d9ed 100644 --- a/api/http/proxy/volumes.go +++ b/api/http/proxy/volumes.go @@ -29,7 +29,7 @@ func volumeListOperation(response *http.Response, executor *operationExecutor) e if responseObject["Volumes"] != nil { volumeData := responseObject["Volumes"].([]interface{}) - if executor.operationContext.isAdmin { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { volumeData, err = decorateVolumeList(volumeData, executor.operationContext.resourceControls) } else { volumeData, err = filterVolumeList(volumeData, executor.operationContext) @@ -119,7 +119,7 @@ func decorateVolumeList(volumeData []interface{}, resourceControls []portainer.R // Authorized volumes are decorated during the process. // 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 -func filterVolumeList(volumeData []interface{}, context *restrictedOperationContext) ([]interface{}, error) { +func filterVolumeList(volumeData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { filteredVolumeData := make([]interface{}, 0) for _, volume := range volumeData { diff --git a/api/http/security/authorization.go b/api/http/security/authorization.go index e1bb256ea..d2a0550c6 100644 --- a/api/http/security/authorization.go +++ b/api/http/security/authorization.go @@ -79,7 +79,7 @@ func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl, // * the Public flag is set false // * 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 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 func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool { 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 // 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 { - groupAccess := authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams) + groupAccess := authorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies) if !groupAccess { - return authorizedAccess(userID, memberships, endpoint.AuthorizedUsers, endpoint.AuthorizedTeams) + return authorizedAccess(userID, memberships, endpoint.UserAccessPolicies, endpoint.TeamAccessPolicies) } 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 // listed in the authorized teams. 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. // It will check if the user is part of the authorized users or part of a team that is // listed in the authorized teams. 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 { - for _, authorizedUserID := range authorizedUsers { - if authorizedUserID == userID { +func authorizedAccess(userID portainer.UserID, memberships []portainer.TeamMembership, userAccessPolicies portainer.UserAccessPolicies, teamAccessPolicies portainer.TeamAccessPolicies) bool { + _, userAccess := userAccessPolicies[userID] + if userAccess { + return true + } + + for _, membership := range memberships { + _, teamAccess := teamAccessPolicies[membership.TeamID] + if teamAccess { return true } } - for _, membership := range memberships { - for _, authorizedTeamID := range authorizedTeams { - if membership.TeamID == authorizedTeamID { - return true - } - } - } + return false } diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index 66397b916..ad2152c77 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -14,7 +14,10 @@ type ( jwtService portainer.JWTService userService portainer.UserService teamMembershipService portainer.TeamMembershipService + endpointService portainer.EndpointService endpointGroupService portainer.EndpointGroupService + extensionService portainer.ExtensionService + rbacExtensionClient *rbacExtensionClient authDisabled bool } @@ -23,7 +26,10 @@ type ( JWTService portainer.JWTService UserService portainer.UserService TeamMembershipService portainer.TeamMembershipService + EndpointService portainer.EndpointService EndpointGroupService portainer.EndpointGroupService + ExtensionService portainer.ExtensionService + RBACExtensionURL string AuthDisabled bool } @@ -43,48 +49,49 @@ func NewRequestBouncer(parameters *RequestBouncerParams) *RequestBouncer { jwtService: parameters.JWTService, userService: parameters.UserService, teamMembershipService: parameters.TeamMembershipService, + endpointService: parameters.EndpointService, endpointGroupService: parameters.EndpointGroupService, + extensionService: parameters.ExtensionService, + rbacExtensionClient: newRBACExtensionClient(parameters.RBACExtensionURL), 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. func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler { h = mwSecureHeaders(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. -func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler { - h = bouncer.mwCheckAuthentication(h) - h = mwSecureHeaders(h) +// If the RBAC extension is enabled, authorizations are required to use these endpoints. +// If the RBAC extension is not enabled, the administrator role is required to use these endpoints. +func (bouncer *RequestBouncer) AuthorizedAccess(h http.Handler) http.Handler { + h = bouncer.mwUpgradeToRestrictedRequest(h) + h = bouncer.mwCheckPortainerAuthorizations(h) + h = bouncer.mwAuthenticatedUser(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. // 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 { h = bouncer.mwUpgradeToRestrictedRequest(h) - h = bouncer.AuthenticatedAccess(h) + h = bouncer.mwAuthenticatedUser(h) return h } -// AdministratorAccess defines a chain of middleware for restricted endpoints. -// 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 +// AuthorizedEndpointOperation retrieves the JWT token from the request context and verifies // that the user can access the specified endpoint. -// An error is returned when access is denied. -func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portainer.Endpoint) error { +// If the RBAC extension is enabled and the authorizationCheck flag is set, +// 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) if err != nil { return err @@ -108,9 +115,43 @@ func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portain return portainer.ErrEndpointAccessDenied } + if authorizationCheck { + err = bouncer.checkEndpointOperationAuthorization(r, endpoint) + if err != nil { + return portainer.ErrAuthorizationRequired + } + } + 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 // that the user can access the specified registry. // An error is returned when access is denied. @@ -136,11 +177,50 @@ func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portain return nil } -// mwSecureHeaders provides secure headers middleware for handlers. -func mwSecureHeaders(next http.Handler) http.Handler { +func (bouncer *RequestBouncer) mwAuthenticatedUser(h 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) { - w.Header().Add("X-XSS-Protection", "1; mode=block") - w.Header().Add("X-Content-Type-Options", "nosniff") + tokenData, err := RetrieveTokenData(r) + 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) }) } @@ -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 func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Handler { 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) { requestContext := &RestrictedRequestContext{ IsAdmin: true, diff --git a/api/http/security/filter.go b/api/http/security/filter.go index f8ca4f67e..ba7872c39 100644 --- a/api/http/security/filter.go +++ b/api/http/security/filter.go @@ -1,6 +1,8 @@ package security -import "github.com/portainer/portainer/api" +import ( + "github.com/portainer/portainer/api" +) // FilterUserTeams filters teams based on user role. // 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. -// 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 { filteredTemplates := templates diff --git a/api/http/security/rbac.go b/api/http/security/rbac.go new file mode 100644 index 000000000..08366cb72 --- /dev/null +++ b/api/http/security/rbac.go @@ -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 +} diff --git a/api/http/server.go b/api/http/server.go index b2428a473..b232adcdc 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -3,6 +3,8 @@ package http import ( "time" + "github.com/portainer/portainer/api/http/handler/roles" + "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/docker" "github.com/portainer/portainer/api/http/handler" @@ -48,6 +50,7 @@ type Server struct { SignatureService portainer.DigitalSignatureService JobScheduler portainer.JobScheduler Snapshotter portainer.Snapshotter + RoleService portainer.RoleService DockerHubService portainer.DockerHubService EndpointService portainer.EndpointService EndpointGroupService portainer.EndpointGroupService @@ -78,15 +81,6 @@ type Server struct { // Start starts the HTTP server 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{ ResourceControlService: server.ResourceControlService, TeamMembershipService: server.TeamMembershipService, @@ -97,6 +91,18 @@ func (server *Server) Start() error { } 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) var authHandler = auth.NewHandler(requestBouncer, rateLimiter, server.AuthDisabled) @@ -108,8 +114,14 @@ func (server *Server) Start() error { authHandler.TeamService = server.TeamService authHandler.TeamMembershipService = server.TeamMembershipService authHandler.ExtensionService = server.ExtensionService + authHandler.EndpointService = server.EndpointService + authHandler.EndpointGroupService = server.EndpointGroupService + authHandler.RoleService = server.RoleService authHandler.ProxyManager = proxyManager + var roleHandler = roles.NewHandler(requestBouncer) + roleHandler.RoleService = server.RoleService + var dockerHubHandler = dockerhub.NewHandler(requestBouncer) dockerHubHandler.DockerHubService = server.DockerHubService @@ -136,6 +148,9 @@ func (server *Server) Start() error { var extensionHandler = extensions.NewHandler(requestBouncer) extensionHandler.ExtensionService = server.ExtensionService extensionHandler.ExtensionManager = server.ExtensionManager + extensionHandler.EndpointGroupService = server.EndpointGroupService + extensionHandler.EndpointService = server.EndpointService + extensionHandler.RegistryService = server.RegistryService var registryHandler = registries.NewHandler(requestBouncer) registryHandler.RegistryService = server.RegistryService @@ -208,6 +223,7 @@ func (server *Server) Start() error { webhookHandler.DockerClientFactory = server.DockerClientFactory server.Handler = &handler.Handler{ + RoleHandler: roleHandler, AuthHandler: authHandler, DockerHubHandler: dockerHubHandler, EndpointGroupHandler: endpointGroupHandler, diff --git a/api/jwt/jwt.go b/api/jwt/jwt.go index 8880b8674..9e18a4bbb 100644 --- a/api/jwt/jwt.go +++ b/api/jwt/jwt.go @@ -16,9 +16,11 @@ type Service struct { } type claims struct { - UserID int `json:"id"` - Username string `json:"username"` - Role int `json:"role"` + UserID int `json:"id"` + Username string `json:"username"` + Role int `json:"role"` + EndpointAuthorizations portainer.EndpointAuthorizations `json:"endpointAuthorizations"` + PortainerAuthorizations portainer.Authorizations `json:"portainerAuthorizations"` jwt.StandardClaims } @@ -41,6 +43,8 @@ func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) int(data.ID), data.Username, int(data.Role), + data.EndpointAuthorizations, + data.PortainerAuthorizations, jwt.StandardClaims{ ExpiresAt: expireToken, }, @@ -67,9 +71,11 @@ func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData, if err == nil && parsedToken != nil { if cl, ok := parsedToken.Claims.(*claims); ok && parsedToken.Valid { tokenData := &portainer.TokenData{ - ID: portainer.UserID(cl.UserID), - Username: cl.Username, - Role: portainer.UserRole(cl.Role), + ID: portainer.UserID(cl.UserID), + Username: cl.Username, + Role: portainer.UserRole(cl.Role), + EndpointAuthorizations: cl.EndpointAuthorizations, + PortainerAuthorizations: cl.PortainerAuthorizations, } return tokenData, nil } diff --git a/api/portainer.go b/api/portainer.go index 0b2eff4f4..69a0ee124 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -113,10 +113,11 @@ type ( // User represents a user account User struct { - ID UserID `json:"Id"` - Username string `json:"Username"` - Password string `json:"Password,omitempty"` - Role UserRole `json:"Role"` + ID UserID `json:"Id"` + Username string `json:"Username"` + Password string `json:"Password,omitempty"` + Role UserRole `json:"Role"` + PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"` } // UserID represents a user identifier @@ -154,9 +155,11 @@ type ( // TokenData represents the data embedded in a JWT token TokenData struct { - ID UserID - Username string - Role UserRole + ID UserID + Username string + Role UserRole + EndpointAuthorizations EndpointAuthorizations + PortainerAuthorizations Authorizations } // 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"` Username string `json:"Username"` Password string `json:"Password,omitempty"` - AuthorizedUsers []UserID `json:"AuthorizedUsers"` - AuthorizedTeams []TeamID `json:"AuthorizedTeams"` ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"` + UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` + TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` + + // Deprecated fields + // Deprecated in DBVersion == 18 + AuthorizedUsers []UserID `json:"AuthorizedUsers"` + AuthorizedTeams []TeamID `json:"AuthorizedTeams"` } // RegistryManagementConfiguration represents a configuration that can be used to query @@ -228,20 +236,20 @@ type ( // Endpoint represents a Docker endpoint with all the info required // to connect to it Endpoint struct { - ID EndpointID `json:"Id"` - Name string `json:"Name"` - Type EndpointType `json:"Type"` - URL string `json:"URL"` - GroupID EndpointGroupID `json:"GroupId"` - PublicURL string `json:"PublicURL"` - TLSConfig TLSConfiguration `json:"TLSConfig"` - AuthorizedUsers []UserID `json:"AuthorizedUsers"` - AuthorizedTeams []TeamID `json:"AuthorizedTeams"` - Extensions []EndpointExtension `json:"Extensions"` - AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` - Tags []string `json:"Tags"` - Status EndpointStatus `json:"Status"` - Snapshots []Snapshot `json:"Snapshots"` + ID EndpointID `json:"Id"` + Name string `json:"Name"` + Type EndpointType `json:"Type"` + URL string `json:"URL"` + GroupID EndpointGroupID `json:"GroupId"` + PublicURL string `json:"PublicURL"` + TLSConfig TLSConfiguration `json:"TLSConfig"` + Extensions []EndpointExtension `json:"Extensions"` + AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` + Tags []string `json:"Tags"` + Status EndpointStatus `json:"Status"` + Snapshots []Snapshot `json:"Snapshots"` + UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` + TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` // Deprecated fields // Deprecated in DBVersion == 4 @@ -249,8 +257,50 @@ type ( TLSCACertPath string `json:"TLSCACert,omitempty"` TLSCertPath string `json:"TLSCert,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 int @@ -342,15 +392,19 @@ type ( // EndpointGroup represents a group of endpoints EndpointGroup struct { - ID EndpointGroupID `json:"Id"` - Name string `json:"Name"` - Description string `json:"Description"` - AuthorizedUsers []UserID `json:"AuthorizedUsers"` - AuthorizedTeams []TeamID `json:"AuthorizedTeams"` - Tags []string `json:"Tags"` + ID EndpointGroupID `json:"Id"` + Name string `json:"Name"` + Description string `json:"Description"` + UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` + TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` + Tags []string `json:"Tags"` // Deprecated fields Labels []Pair `json:"Labels"` + + // Deprecated in DBVersion == 18 + AuthorizedUsers []UserID `json:"AuthorizedUsers"` + AuthorizedTeams []TeamID `json:"AuthorizedTeams"` } // EndpointExtension represents a deprecated form of Portainer extension @@ -551,6 +605,12 @@ type ( 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 interface { Team(ID TeamID) (*Team, error) @@ -796,7 +856,7 @@ const ( // APIVersion is the version number of the Portainer API APIVersion = "1.20.2" // DBVersion is the version number of the Portainer database - DBVersion = 17 + DBVersion = 18 // AssetsServerURL represents the URL of the Portainer asset server AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved @@ -804,7 +864,7 @@ const ( // MessageOfTheDayTitleURL represents the URL where Portainer MOTD title can be retrieved MessageOfTheDayTitleURL = AssetsServerURL + "/motd-title.txt" // 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 = "Portainer-Agent" // PortainerAgentTargetHeader represent the name of the header containing the target node name @@ -933,6 +993,8 @@ const ( RegistryManagementExtension // OAuthAuthenticationExtension represents the OAuth authentication extension OAuthAuthenticationExtension + // RBACExtension represents the RBAC extension + RBACExtension ) const ( @@ -956,3 +1018,214 @@ const ( // CustomRegistry represents a custom registry 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" +) diff --git a/api/swagger.yaml b/api/swagger.yaml index a6d50a8b5..ad3ee7089 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -465,56 +465,6 @@ paths: examples: application/json: 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: post: tags: @@ -791,56 +741,6 @@ paths: examples: application/json: 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: get: tags: @@ -1045,56 +945,6 @@ paths: description: "Server error" schema: $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: post: tags: diff --git a/app/agent/components/files-datatable/files-datatable.html b/app/agent/components/files-datatable/files-datatable.html index 65dc569a8..f3d7ba564 100644 --- a/app/agent/components/files-datatable/files-datatable.html +++ b/app/agent/components/files-datatable/files-datatable.html @@ -1,7 +1,7 @@
- + @@ -71,14 +71,14 @@ {{ item.ModTime | getisodatefromtimestamp }} - Download - + Rename - + Delete diff --git a/app/docker/components/container-quick-actions/containerQuickActions.html b/app/docker/components/container-quick-actions/containerQuickActions.html index e24fb93f0..4bdc54d2a 100644 --- a/app/docker/components/container-quick-actions/containerQuickActions.html +++ b/app/docker/components/container-quick-actions/containerQuickActions.html @@ -1,40 +1,46 @@
-
- -
@@ -24,7 +24,7 @@ - + @@ -53,7 +53,7 @@ - + diff --git a/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html b/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html index bb8a5d4c6..faf668ba0 100644 --- a/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html +++ b/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html @@ -8,7 +8,7 @@
-
+
@@ -70,25 +70,27 @@
- - -
-
- -
@@ -24,7 +24,7 @@ - + @@ -53,7 +53,7 @@ - + diff --git a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html index 4ca83adcb..aaf97d95c 100644 --- a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html +++ b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html @@ -1,15 +1,15 @@ -
-
- -
@@ -24,7 +24,7 @@ - + @@ -105,13 +105,13 @@ - + {{ item.Id | truncate:40 }} {{ item.Id | truncate:40 }} - + browse Unused diff --git a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html index 12151542a..55113a2d8 100644 --- a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html +++ b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html @@ -1,7 +1,7 @@ - - -