THIS IS A TEST INSTANCE ONLY! REPOSITORIES CAN BE DELETED AT ANY TIME!

Browse Source

feat(extensions): introduce RBAC extension (#2900)

tags/1.21.0^2
Anthony Lapenna GitHub 9 months ago
parent
commit
8057aa45c4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 2010 additions and 861 deletions
  1. +1
    -0
      .eslintrc.yml
  2. +9
    -23
      api/bolt/datastore.go
  3. +431
    -0
      api/bolt/init.go
  4. +125
    -0
      api/bolt/migrator/migrate_dbversion17.go
  5. +27
    -0
      api/bolt/migrator/migrator.go
  6. +83
    -0
      api/bolt/role/role.go
  7. +25
    -24
      api/cmd/portainer/main.go
  8. +1
    -0
      api/errors.go
  9. +4
    -0
      api/exec/extension.go
  10. +52
    -3
      api/http/handler/auth/authenticate.go
  11. +1
    -1
      api/http/handler/auth/authenticate_oauth.go
  12. +122
    -0
      api/http/handler/auth/authorization.go
  13. +3
    -0
      api/http/handler/auth/handler.go
  14. +2
    -2
      api/http/handler/dockerhub/handler.go
  15. +5
    -5
      api/http/handler/endpointgroups/endpointgroup_create.go
  16. +20
    -8
      api/http/handler/endpointgroups/endpointgroup_update.go
  17. +0
    -63
      api/http/handler/endpointgroups/endpointgroup_update_access.go
  18. +5
    -7
      api/http/handler/endpointgroups/handler.go
  19. +3
    -3
      api/http/handler/endpointproxy/handler.go
  20. +2
    -2
      api/http/handler/endpointproxy/proxy_azure.go
  21. +2
    -2
      api/http/handler/endpointproxy/proxy_docker.go
  22. +2
    -2
      api/http/handler/endpointproxy/proxy_storidge.go
  23. +25
    -25
      api/http/handler/endpoints/endpoint_create.go
  24. +2
    -2
      api/http/handler/endpoints/endpoint_inspect.go
  25. +0
    -5
      api/http/handler/endpoints/endpoint_job.go
  26. +10
    -0
      api/http/handler/endpoints/endpoint_update.go
  27. +0
    -67
      api/http/handler/endpoints/endpoint_update_access.go
  28. +11
    -13
      api/http/handler/endpoints/handler.go
  29. +7
    -0
      api/http/handler/extensions/extension_create.go
  30. +10
    -7
      api/http/handler/extensions/handler.go
  31. +59
    -0
      api/http/handler/extensions/upgrade.go
  32. +13
    -8
      api/http/handler/handler.go
  33. +1
    -1
      api/http/handler/motd/handler.go
  34. +6
    -8
      api/http/handler/registries/handler.go
  35. +11
    -0
      api/http/handler/registries/proxy.go
  36. +8
    -8
      api/http/handler/registries/registry_create.go
  37. +15
    -5
      api/http/handler/registries/registry_update.go
  38. +0
    -63
      api/http/handler/registries/registry_update_access.go
  39. +27
    -0
      api/http/handler/roles/handler.go
  40. +18
    -0
      api/http/handler/roles/role_list.go
  41. +7
    -7
      api/http/handler/schedules/handler.go
  42. +3
    -3
      api/http/handler/settings/handler.go
  43. +2
    -2
      api/http/handler/stacks/stack_create.go
  44. +26
    -20
      api/http/handler/stacks/stack_delete.go
  45. +12
    -0
      api/http/handler/stacks/stack_file.go
  46. +12
    -0
      api/http/handler/stacks/stack_inspect.go
  47. +12
    -7
      api/http/handler/stacks/stack_migrate.go
  48. +24
    -18
      api/http/handler/stacks/stack_update.go
  49. +3
    -3
      api/http/handler/tags/handler.go
  50. +4
    -4
      api/http/handler/teammemberships/handler.go
  51. +6
    -6
      api/http/handler/teams/handler.go
  52. +5
    -5
      api/http/handler/templates/handler.go
  53. +1
    -1
      api/http/handler/upload/handler.go
  54. +7
    -7
      api/http/handler/users/handler.go
  55. +17
    -0
      api/http/handler/users/user_create.go
  56. +3
    -3
      api/http/handler/webhooks/handler.go
  57. +2
    -2
      api/http/handler/websocket/attach.go
  58. +2
    -2
      api/http/handler/websocket/exec.go
  59. +2
    -2
      api/http/handler/websocket/handler.go
  60. +4
    -4
      api/http/proxy/access_control.go
  61. +2
    -2
      api/http/proxy/configs.go
  62. +2
    -2
      api/http/proxy/containers.go
  63. +19
    -11
      api/http/proxy/docker_transport.go
  64. +6
    -5
      api/http/proxy/factory.go
  65. +4
    -1
      api/http/proxy/factory_local.go
  66. +3
    -2
      api/http/proxy/factory_local_windows.go
  67. +7
    -6
      api/http/proxy/manager.go
  68. +2
    -2
      api/http/proxy/networks.go
  69. +2
    -2
      api/http/proxy/secrets.go
  70. +2
    -2
      api/http/proxy/services.go
  71. +2
    -2
      api/http/proxy/tasks.go
  72. +2
    -2
      api/http/proxy/volumes.go
  73. +14
    -14
      api/http/security/authorization.go
  74. +112
    -36
      api/http/security/bouncer.go
  75. +4
    -2
      api/http/security/filter.go
  76. +59
    -0
      api/http/security/rbac.go
  77. +25
    -9
      api/http/server.go
  78. +12
    -6
      api/jwt/jwt.go
  79. +304
    -31
      api/portainer.go
  80. +0
    -150
      api/swagger.yaml
  81. +4
    -4
      app/agent/components/files-datatable/files-datatable.html
  82. +27
    -20
      app/docker/components/container-quick-actions/containerQuickActions.html
  83. +2
    -2
      app/docker/components/container-restart-policy/container-restart-policy.html
  84. +5
    -5
      app/docker/components/datatables/configs-datatable/configsDatatable.html
  85. +2
    -2
      app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html
  86. +9
    -9
      app/docker/components/datatables/containers-datatable/actions/containersDatatableActions.html
  87. +25
    -23
      app/docker/components/datatables/containers-datatable/containersDatatable.html
  88. +5
    -5
      app/docker/components/datatables/images-datatable/imagesDatatable.html
  89. +3
    -3
      app/docker/components/datatables/networks-datatable/networksDatatable.html
  90. +5
    -5
      app/docker/components/datatables/secrets-datatable/secretsDatatable.html
  91. +4
    -4
      app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html
  92. +3
    -3
      app/docker/components/datatables/services-datatable/servicesDatatable.html
  93. +6
    -6
      app/docker/components/datatables/volumes-datatable/volumesDatatable.html
  94. +1
    -1
      app/docker/components/dockerSidebarContent/dockerSidebarContent.html
  95. +1
    -2
      app/docker/components/network-macvlan-form/networkMacvlanFormController.js
  96. +1
    -1
      app/docker/views/configs/create/createConfigController.js
  97. +2
    -2
      app/docker/views/configs/edit/config.html
  98. +2
    -6
      app/docker/views/containers/create/createContainerController.js
  99. +17
    -17
      app/docker/views/containers/edit/container.html
  100. +1
    -1
      app/docker/views/host/host-view-controller.js

+ 1
- 0
.eslintrc.yml View File

@@ -2,6 +2,7 @@ env:
browser: true
jquery: true
node: true
es6: true

globals:
angular: true


+ 9
- 23
api/bolt/datastore.go View File

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


+ 431
- 0
api/bolt/init.go View File

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

+ 125
- 0
api/bolt/migrator/migrate_dbversion17.go View File

@@ -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, &registry)
if err != nil {
return err
}
}

return nil
}

+ 27
- 0
api/bolt/migrator/migrator.go View File

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

+ 83
- 0
api/bolt/role/role.go View File

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

+ 25
- 24
api/cmd/portainer/main.go View File

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


+ 1
- 0
api/errors.go View File

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


+ 4
- 0
api/exec/extension.go View File

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

+ 52
- 3
api/http/handler/auth/authenticate.go View File

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


+ 1
- 1
api/http/handler/auth/authenticate_oauth.go View File

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


+ 122
- 0
api/http/handler/auth/authorization.go View File

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

+ 3
- 0
api/http/handler/auth/handler.go View File

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



+ 2
- 2
api/http/handler/dockerhub/handler.go View File

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

+ 5
- 5
api/http/handler/endpointgroups/endpointgroup_create.go View File

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


+ 20
- 8
api/http/handler/endpointgroups/endpointgroup_update.go View File

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

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}
if payload.UserAccessPolicies != nil {
endpointGroup.UserAccessPolicies = payload.UserAccessPolicies
}

if payload.TeamAccessPolicies != nil {
endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies
}

endpoints, err := handler.EndpointService.Endpoints()
err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside 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}
}
}
}



+ 0
- 63
api/http/handler/endpointgroups/endpointgroup_update_access.go View File

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

+ 5
- 7
api/http/handler/endpointgroups/handler.go View File

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


+ 3
- 3
api/http/handler/endpointproxy/handler.go View File

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

+ 2
- 2
api/http/handler/endpointproxy/proxy_azure.go View File

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


+ 2
- 2
api/http/handler/endpointproxy/proxy_docker.go View File

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


+ 2
- 2
api/http/handler/endpointproxy/proxy_storidge.go View File

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


+ 25
- 25
api/http/handler/endpoints/endpoint_create.go View File

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


+ 2
- 2
api/http/handler/endpoints/endpoint_inspect.go View File

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


+ 0
- 5
api/http/handler/endpoints/endpoint_job.go View File

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


+ 10
- 0
api/http/handler/endpoints/endpoint_update.go View File

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


+ 0
- 67
api/http/handler/endpoints/endpoint_update_access.go View File

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

+ 11
- 13
api/http/handler/endpoints/handler.go View File

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

+ 7
- 0
api/http/handler/extensions/extension_create.go View File

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


+ 10
- 7
api/http/handler/extensions/handler.go View File

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

+ 59
- 0
api/http/handler/extensions/upgrade.go View File

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

+ 13
- 8
api/http/handler/handler.go View File

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


+ 1
- 1
api/http/handler/motd/handler.go View File

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

+ 6
- 8
api/http/handler/registries/handler.go View File

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



+ 11
- 0
api/http/handler/registries/proxy.go View File

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


+ 8
- 8
api/http/handler/registries/registry_create.go View File

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


+ 15
- 5
api/http/handler/registries/registry_update.go View File

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