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

Browse Source

feat(api): rewrite access control management in Docker (#3337)

* feat(api): decorate Docker resource creation response with resource control

* fix(api): fix a potential resource control conflict between stacks/volumes

* feat(api): generate a default private resource control instead of admin only

* fix(api): fix default RC value

* fix(api): update RC authorizations check to support admin only flag

* refactor(api): relocate access control related methods

* fix(api): fix a potential conflict when fetching RC from database

* refactor(api): refactor access control logic

* refactor(api): remove the concept of DecoratedStack

* feat(api): automatically remove RC when removing a Docker resource

* refactor(api): update filter resource methods documentation

* refactor(api): update proxy package structure

* refactor(api): renamed proxy/misc package

* feat(api): re-introduce ResourceControlDelete operation as admin restricted

* refactor(api): relocate default endpoint authorizations

* feat(api): migrate RBAC data

* feat(app): ResourceControl management refactor

* fix(api): fix access control issue on stack deletion and automatically delete RC

* fix(api): fix stack filtering

* fix(api): fix UpdateResourceControl operation checks

* refactor(api): introduce a NewTransport builder method

* refactor(api): inject endpoint in Docker transport

* refactor(api): introduce Docker client into Docker transport

* refactor(api): refactor http/proxy package

* feat(api): inspect a Docker resource labels during access control validation

* fix(api): only apply automatic resource control creation on success response

* fix(api): fix stack access control check

* fix(api): use StatusCreated instead of StatusOK for automatic resource control creation

* fix(app): resource control fixes

* fix(api): fix an issue preventing administrator to inspect a resource with a RC

* refactor(api): remove useless error return

* refactor(api): document DecorateStacks function

* fix(api): fix invalid resource control type for container deletion

* feat(api): support Docker system networks

* feat(api): update Swagger docs

* refactor(api): rename transport variable

* refactor(api): rename transport variable

* feat(networks): add system tag for system networks

* feat(api): add support for resource control labels

* feat(api): upgrade to DBVersion 22

* refactor(api): refactor access control management in Docker proxy

* refactor(api): re-implement docker proxy taskListOperation

* refactor(api): review parameters declaration

* refactor(api): remove extra blank line

* refactor(api): review method comments

* fix(api): fix invalid ServerAddress property and review method visibility

* feat(api): update error message

* feat(api): update restrictedVolumeBrowserOperation method

* refactor(api): refactor method parameters

* refactor(api): minor refactor

* refactor(api): change Azure transport visibility

* refactor(api): update struct documentation

* refactor(api): update struct documentation

* feat(api): review restrictedResourceOperation method

* refactor(api): remove unused authorization methods

* feat(api): apply RBAC when enabled on stack operations

* fix(api): fix invalid data migration procedure for DBVersion = 22

* fix(app): RC duplicate on private resource

* feat(api): change Docker API version logic for libcompose/client factory

* fix(api): update access denied error message to be Docker API compliant

* fix(api): update volume browsing authorizations data migration

* fix(api): fix an issue with access control in multi-node agent Swarm cluster
tags/1.23.0^2
Anthony Lapenna GitHub 4 months ago
parent
commit
19d4db13be
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 3260 additions and 2900 deletions
  1. +154
    -0
      api/access_control.go
  2. +388
    -0
      api/authorizations.go
  3. +12
    -355
      api/bolt/init.go
  4. +68
    -5
      api/bolt/migrator/migrate_dbversion20.go
  5. +8
    -2
      api/bolt/migrator/migrator.go
  6. +5
    -8
      api/bolt/resourcecontrol/resourcecontrol.go
  7. +5
    -4
      api/docker/client.go
  8. +2
    -2
      api/http/handler/endpointproxy/proxy_azure.go
  9. +3
    -3
      api/http/handler/endpointproxy/proxy_docker.go
  10. +1
    -1
      api/http/handler/endpoints/endpoint_delete.go
  11. +1
    -1
      api/http/handler/endpoints/endpoint_update.go
  12. +2
    -3
      api/http/handler/resourcecontrols/handler.go
  13. +25
    -28
      api/http/handler/resourcecontrols/resourcecontrol_create.go
  14. +1
    -11
      api/http/handler/resourcecontrols/resourcecontrol_delete.go
  15. +13
    -6
      api/http/handler/resourcecontrols/resourcecontrol_update.go
  16. +6
    -7
      api/http/handler/stacks/create_compose_stack.go
  17. +6
    -7
      api/http/handler/stacks/create_swarm_stack.go
  18. +35
    -0
      api/http/handler/stacks/handler.go
  19. +30
    -12
      api/http/handler/stacks/stack_create.go
  20. +15
    -7
      api/http/handler/stacks/stack_delete.go
  21. +7
    -13
      api/http/handler/stacks/stack_file.go
  22. +12
    -14
      api/http/handler/stacks/stack_inspect.go
  23. +24
    -4
      api/http/handler/stacks/stack_list.go
  24. +8
    -7
      api/http/handler/stacks/stack_migrate.go
  25. +8
    -7
      api/http/handler/stacks/stack_update.go
  26. +0
    -166
      api/http/proxy/access_control.go
  27. +0
    -107
      api/http/proxy/configs.go
  28. +0
    -204
      api/http/proxy/containers.go
  29. +0
    -600
      api/http/proxy/docker_transport.go
  30. +0
    -101
      api/http/proxy/factory.go
  31. +20
    -0
      api/http/proxy/factory/azure.go
  32. +19
    -20
      api/http/proxy/factory/azure/transport.go
  33. +118
    -0
      api/http/proxy/factory/docker.go
  34. +306
    -0
      api/http/proxy/factory/docker/access_control.go
  35. +1
    -1
      api/http/proxy/factory/docker/build.go
  36. +86
    -0
      api/http/proxy/factory/docker/configs.go
  37. +146
    -0
      api/http/proxy/factory/docker/containers.go
  38. +101
    -0
      api/http/proxy/factory/docker/networks.go
  39. +16
    -1
      api/http/proxy/factory/docker/registry.go
  40. +87
    -0
      api/http/proxy/factory/docker/secrets.go
  41. +86
    -0
      api/http/proxy/factory/docker/services.go
  42. +5
    -3
      api/http/proxy/factory/docker/swarm.go
  43. +50
    -0
      api/http/proxy/factory/docker/tasks.go
  44. +725
    -0
      api/http/proxy/factory/docker/transport.go
  45. +89
    -0
      api/http/proxy/factory/docker/volumes.go
  46. +46
    -0
      api/http/proxy/factory/docker_unix.go
  47. +47
    -0
      api/http/proxy/factory/docker_windows.go
  48. +109
    -0
      api/http/proxy/factory/factory.go
  49. +11
    -0
      api/http/proxy/factory/responseutils/json.go
  50. +106
    -0
      api/http/proxy/factory/responseutils/response.go
  51. +1
    -1
      api/http/proxy/factory/reverse_proxy.go
  52. +0
    -29
      api/http/proxy/factory_local.go
  53. +0
    -40
      api/http/proxy/factory_local_windows.go
  54. +0
    -43
      api/http/proxy/local.go
  55. +55
    -98
      api/http/proxy/manager.go
  56. +0
    -135
      api/http/proxy/networks.go
  57. +0
    -109
      api/http/proxy/response.go
  58. +0
    -107
      api/http/proxy/secrets.go
  59. +0
    -143
      api/http/proxy/services.go
  60. +0
    -79
      api/http/proxy/tasks.go
  61. +0
    -144
      api/http/proxy/volumes.go
  62. +6
    -57
      api/http/security/authorization.go
  63. +4
    -0
      api/http/server.go
  64. +5
    -1
      api/libcompose/compose_stack.go
  65. +20
    -22
      api/portainer.go
  66. +2
    -2
      api/swagger.yaml
  67. +1
    -1
      app/docker/components/datatables/configs-datatable/configsDatatable.html
  68. +1
    -1
      app/docker/components/datatables/containers-datatable/containersDatatable.html
  69. +2
    -1
      app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html
  70. +5
    -0
      app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js
  71. +4
    -4
      app/docker/components/datatables/networks-datatable/networksDatatableController.js
  72. +1
    -1
      app/docker/components/datatables/secrets-datatable/secretsDatatable.html
  73. +1
    -1
      app/docker/components/datatables/services-datatable/servicesDatatable.html
  74. +1
    -1
      app/docker/components/datatables/volumes-datatable/volumesDatatable.html
  75. +3
    -5
      app/docker/models/config.js
  76. +3
    -5
      app/docker/models/container.js
  77. +1
    -1
      app/docker/models/network.js
  78. +1
    -1
      app/docker/models/secret.js
  79. +1
    -1
      app/docker/models/service.js
  80. +1
    -1
      app/docker/models/volume.js
  81. +6
    -10
      app/docker/services/containerService.js
  82. +5
    -6
      app/docker/services/serviceService.js
  83. +3
    -7
      app/docker/services/volumeService.js
  84. +8
    -14
      app/docker/views/configs/create/createConfigController.js
  85. +8
    -10
      app/docker/views/containers/create/createContainerController.js
  86. +1
    -1
      app/docker/views/containers/edit/container.html
  87. +8
    -16
      app/docker/views/containers/edit/containerController.js
  88. +4
    -3
      app/docker/views/networks/create/createNetworkController.js
  89. +3
    -2
      app/docker/views/networks/edit/network.html
  90. +8
    -6
      app/docker/views/networks/edit/networkController.js
  91. +6
    -6
      app/docker/views/secrets/create/createSecretController.js
  92. +6
    -7
      app/docker/views/services/create/createServiceController.js
  93. +3
    -3
      app/docker/views/volumes/create/createVolumeController.js
  94. +2
    -2
      app/portainer/components/accessControlForm/porAccessControlForm.html
  95. +6
    -3
      app/portainer/components/accessControlForm/porAccessControlFormController.js
  96. +3
    -1
      app/portainer/components/accessControlForm/porAccessControlFormModel.js
  97. +3
    -1
      app/portainer/components/accessControlPanel/por-access-control-panel.js
  98. +13
    -17
      app/portainer/components/accessControlPanel/porAccessControlPanel.html
  99. +25
    -41
      app/portainer/components/accessControlPanel/porAccessControlPanelController.js
  100. +7
    -0
      app/portainer/components/accessControlPanel/porAccessControlPanelModel.js

+ 154
- 0
api/access_control.go View File

@@ -0,0 +1,154 @@
package portainer

// NewPrivateResourceControl will create a new private resource control associated to the resource specified by the
// identifier and type parameters. It automatically assigns it to the user specified by the userID parameter.
func NewPrivateResourceControl(resourceIdentifier string, resourceType ResourceControlType, userID UserID) *ResourceControl {
return &ResourceControl{
Type: resourceType,
ResourceID: resourceIdentifier,
SubResourceIDs: []string{},
UserAccesses: []UserResourceAccess{
{
UserID: userID,
AccessLevel: ReadWriteAccessLevel,
},
},
TeamAccesses: []TeamResourceAccess{},
AdministratorsOnly: false,
Public: false,
System: false,
}
}

// NewSystemResourceControl will create a new public resource control with the System flag set to true.
// These kind of resource control are not persisted and are created on the fly by the Portainer API.
func NewSystemResourceControl(resourceIdentifier string, resourceType ResourceControlType) *ResourceControl {
return &ResourceControl{
Type: resourceType,
ResourceID: resourceIdentifier,
SubResourceIDs: []string{},
UserAccesses: []UserResourceAccess{},
TeamAccesses: []TeamResourceAccess{},
AdministratorsOnly: false,
Public: true,
System: true,
}
}

// NewPublicResourceControl will create a new public resource control.
func NewPublicResourceControl(resourceIdentifier string, resourceType ResourceControlType) *ResourceControl {
return &ResourceControl{
Type: resourceType,
ResourceID: resourceIdentifier,
SubResourceIDs: []string{},
UserAccesses: []UserResourceAccess{},
TeamAccesses: []TeamResourceAccess{},
AdministratorsOnly: false,
Public: true,
System: false,
}
}

// NewRestrictedResourceControl will create a new resource control with user and team accesses restrictions.
func NewRestrictedResourceControl(resourceIdentifier string, resourceType ResourceControlType, userIDs []UserID, teamIDs []TeamID) *ResourceControl {
userAccesses := make([]UserResourceAccess, 0)
teamAccesses := make([]TeamResourceAccess, 0)

for _, id := range userIDs {
access := UserResourceAccess{
UserID: id,
AccessLevel: ReadWriteAccessLevel,
}

userAccesses = append(userAccesses, access)
}

for _, id := range teamIDs {
access := TeamResourceAccess{
TeamID: id,
AccessLevel: ReadWriteAccessLevel,
}

teamAccesses = append(teamAccesses, access)
}

return &ResourceControl{
Type: resourceType,
ResourceID: resourceIdentifier,
SubResourceIDs: []string{},
UserAccesses: userAccesses,
TeamAccesses: teamAccesses,
AdministratorsOnly: false,
Public: false,
System: false,
}
}

// DecorateStacks will iterate through a list of stacks, check for an associated resource control for each
// stack and decorate the stack element if a resource control is found.
func DecorateStacks(stacks []Stack, resourceControls []ResourceControl) []Stack {
for idx, stack := range stacks {

resourceControl := GetResourceControlByResourceIDAndType(stack.Name, StackResourceControl, resourceControls)
if resourceControl != nil {
stacks[idx].ResourceControl = resourceControl
}
}

return stacks
}

// FilterAuthorizedStacks returns a list of decorated stacks filtered through resource control access checks.
func FilterAuthorizedStacks(stacks []Stack, user *User, userTeamIDs []TeamID, rbacEnabled bool) []Stack {
authorizedStacks := make([]Stack, 0)

for _, stack := range stacks {
_, ok := user.EndpointAuthorizations[stack.EndpointID][EndpointResourcesAccess]
if rbacEnabled && ok {
authorizedStacks = append(authorizedStacks, stack)
continue
}

if stack.ResourceControl != nil && UserCanAccessResource(user.ID, userTeamIDs, stack.ResourceControl) {
authorizedStacks = append(authorizedStacks, stack)
}
}

return authorizedStacks
}

// UserCanAccessResource will valide that a user has permissions defined in the specified resource control
// based on its identifier and the team(s) he is part of.
func UserCanAccessResource(userID UserID, userTeamIDs []TeamID, resourceControl *ResourceControl) bool {
for _, authorizedUserAccess := range resourceControl.UserAccesses {
if userID == authorizedUserAccess.UserID {
return true
}
}

for _, authorizedTeamAccess := range resourceControl.TeamAccesses {
for _, userTeamID := range userTeamIDs {
if userTeamID == authorizedTeamAccess.TeamID {
return true
}
}
}

return resourceControl.Public
}

// GetResourceControlByResourceIDAndType retrieves the first matching resource control in a set of resource controls
// based on the specified id and resource type parameters.
func GetResourceControlByResourceIDAndType(resourceID string, resourceType ResourceControlType, resourceControls []ResourceControl) *ResourceControl {
for _, resourceControl := range resourceControls {
if resourceID == resourceControl.ResourceID && resourceType == resourceControl.Type {
return &resourceControl
}
for _, subResourceID := range resourceControl.SubResourceIDs {
if resourceID == subResourceID {
return &resourceControl
}
}
}
return nil
}

+ 388
- 0
api/authorizations.go View File

@@ -34,6 +34,394 @@ func NewAuthorizationService(parameters *AuthorizationServiceParameters) *Author
}
}

// DefaultEndpointAuthorizationsForEndpointAdministratorRole returns the default endpoint authorizations
// associated to the endpoint administrator role.
func DefaultEndpointAuthorizationsForEndpointAdministratorRole() Authorizations {
return map[Authorization]bool{
OperationDockerContainerArchiveInfo: true,
OperationDockerContainerList: true,
OperationDockerContainerExport: true,
OperationDockerContainerChanges: true,
OperationDockerContainerInspect: true,
OperationDockerContainerTop: true,
OperationDockerContainerLogs: true,
OperationDockerContainerStats: true,
OperationDockerContainerAttachWebsocket: true,
OperationDockerContainerArchive: true,
OperationDockerContainerCreate: true,
OperationDockerContainerPrune: true,
OperationDockerContainerKill: true,
OperationDockerContainerPause: true,
OperationDockerContainerUnpause: true,
OperationDockerContainerRestart: true,
OperationDockerContainerStart: true,
OperationDockerContainerStop: true,
OperationDockerContainerWait: true,
OperationDockerContainerResize: true,
OperationDockerContainerAttach: true,
OperationDockerContainerExec: true,
OperationDockerContainerRename: true,
OperationDockerContainerUpdate: true,
OperationDockerContainerPutContainerArchive: true,
OperationDockerContainerDelete: true,
OperationDockerImageList: true,
OperationDockerImageSearch: true,
OperationDockerImageGetAll: true,
OperationDockerImageGet: true,
OperationDockerImageHistory: true,
OperationDockerImageInspect: true,
OperationDockerImageLoad: true,
OperationDockerImageCreate: true,
OperationDockerImagePrune: true,
OperationDockerImagePush: true,
OperationDockerImageTag: true,
OperationDockerImageDelete: true,
OperationDockerImageCommit: true,
OperationDockerImageBuild: true,
OperationDockerNetworkList: true,
OperationDockerNetworkInspect: true,
OperationDockerNetworkCreate: true,
OperationDockerNetworkConnect: true,
OperationDockerNetworkDisconnect: true,
OperationDockerNetworkPrune: true,
OperationDockerNetworkDelete: true,
OperationDockerVolumeList: true,
OperationDockerVolumeInspect: true,
OperationDockerVolumeCreate: true,
OperationDockerVolumePrune: true,
OperationDockerVolumeDelete: true,
OperationDockerExecInspect: true,
OperationDockerExecStart: true,
OperationDockerExecResize: true,
OperationDockerSwarmInspect: true,
OperationDockerSwarmUnlockKey: true,
OperationDockerSwarmInit: true,
OperationDockerSwarmJoin: true,
OperationDockerSwarmLeave: true,
OperationDockerSwarmUpdate: true,
OperationDockerSwarmUnlock: true,
OperationDockerNodeList: true,
OperationDockerNodeInspect: true,
OperationDockerNodeUpdate: true,
OperationDockerNodeDelete: true,
OperationDockerServiceList: true,
OperationDockerServiceInspect: true,
OperationDockerServiceLogs: true,
OperationDockerServiceCreate: true,
OperationDockerServiceUpdate: true,
OperationDockerServiceDelete: true,
OperationDockerSecretList: true,
OperationDockerSecretInspect: true,
OperationDockerSecretCreate: true,
OperationDockerSecretUpdate: true,
OperationDockerSecretDelete: true,
OperationDockerConfigList: true,
OperationDockerConfigInspect: true,
OperationDockerConfigCreate: true,
OperationDockerConfigUpdate: true,
OperationDockerConfigDelete: true,
OperationDockerTaskList: true,
OperationDockerTaskInspect: true,
OperationDockerTaskLogs: true,
OperationDockerPluginList: true,
OperationDockerPluginPrivileges: true,
OperationDockerPluginInspect: true,
OperationDockerPluginPull: true,
OperationDockerPluginCreate: true,
OperationDockerPluginEnable: true,
OperationDockerPluginDisable: true,
OperationDockerPluginPush: true,
OperationDockerPluginUpgrade: true,
OperationDockerPluginSet: true,
OperationDockerPluginDelete: true,
OperationDockerSessionStart: true,
OperationDockerDistributionInspect: true,
OperationDockerBuildPrune: true,
OperationDockerBuildCancel: true,
OperationDockerPing: true,
OperationDockerInfo: true,
OperationDockerVersion: true,
OperationDockerEvents: true,
OperationDockerSystem: true,
OperationDockerUndefined: true,
OperationDockerAgentPing: true,
OperationDockerAgentList: true,
OperationDockerAgentHostInfo: true,
OperationDockerAgentBrowseDelete: true,
OperationDockerAgentBrowseGet: true,
OperationDockerAgentBrowseList: true,
OperationDockerAgentBrowsePut: true,
OperationDockerAgentBrowseRename: true,
OperationDockerAgentUndefined: true,
OperationPortainerResourceControlCreate: true,
OperationPortainerResourceControlUpdate: true,
OperationPortainerStackList: true,
OperationPortainerStackInspect: true,
OperationPortainerStackFile: true,
OperationPortainerStackCreate: true,
OperationPortainerStackMigrate: true,
OperationPortainerStackUpdate: true,
OperationPortainerStackDelete: true,
OperationPortainerWebsocketExec: true,
OperationPortainerWebhookList: true,
OperationPortainerWebhookCreate: true,
OperationPortainerWebhookDelete: true,
OperationIntegrationStoridgeAdmin: true,
EndpointResourcesAccess: true,
}
}

// DefaultEndpointAuthorizationsForHelpDeskRole returns the default endpoint authorizations
// associated to the helpdesk role.
func DefaultEndpointAuthorizationsForHelpDeskRole(volumeBrowsingAuthorizations bool) Authorizations {
authorizations := map[Authorization]bool{
OperationDockerContainerArchiveInfo: true,
OperationDockerContainerList: true,
OperationDockerContainerChanges: true,
OperationDockerContainerInspect: true,
OperationDockerContainerTop: true,
OperationDockerContainerLogs: true,
OperationDockerContainerStats: true,
OperationDockerImageList: true,
OperationDockerImageSearch: true,
OperationDockerImageGetAll: true,
OperationDockerImageGet: true,
OperationDockerImageHistory: true,
OperationDockerImageInspect: true,
OperationDockerNetworkList: true,
OperationDockerNetworkInspect: true,
OperationDockerVolumeList: true,
OperationDockerVolumeInspect: true,
OperationDockerSwarmInspect: true,
OperationDockerNodeList: true,
OperationDockerNodeInspect: true,
OperationDockerServiceList: true,
OperationDockerServiceInspect: true,
OperationDockerServiceLogs: true,
OperationDockerSecretList: true,
OperationDockerSecretInspect: true,
OperationDockerConfigList: true,
OperationDockerConfigInspect: true,
OperationDockerTaskList: true,
OperationDockerTaskInspect: true,
OperationDockerTaskLogs: true,
OperationDockerPluginList: true,
OperationDockerDistributionInspect: true,
OperationDockerPing: true,
OperationDockerInfo: true,
OperationDockerVersion: true,
OperationDockerEvents: true,
OperationDockerSystem: true,
OperationDockerAgentPing: true,
OperationDockerAgentList: true,
OperationDockerAgentHostInfo: true,
OperationPortainerStackList: true,
OperationPortainerStackInspect: true,
OperationPortainerStackFile: true,
OperationPortainerWebhookList: true,
EndpointResourcesAccess: true,
}

if volumeBrowsingAuthorizations {
authorizations[OperationDockerAgentBrowseGet] = true
authorizations[OperationDockerAgentBrowseList] = true
}

return authorizations
}

// DefaultEndpointAuthorizationsForStandardUserRole returns the default endpoint authorizations
// associated to the standard user role.
func DefaultEndpointAuthorizationsForStandardUserRole(volumeBrowsingAuthorizations bool) Authorizations {
authorizations := map[Authorization]bool{
OperationDockerContainerArchiveInfo: true,
OperationDockerContainerList: true,
OperationDockerContainerExport: true,
OperationDockerContainerChanges: true,
OperationDockerContainerInspect: true,
OperationDockerContainerTop: true,
OperationDockerContainerLogs: true,
OperationDockerContainerStats: true,
OperationDockerContainerAttachWebsocket: true,
OperationDockerContainerArchive: true,
OperationDockerContainerCreate: true,
OperationDockerContainerKill: true,
OperationDockerContainerPause: true,
OperationDockerContainerUnpause: true,
OperationDockerContainerRestart: true,
OperationDockerContainerStart: true,
OperationDockerContainerStop: true,
OperationDockerContainerWait: true,
OperationDockerContainerResize: true,
OperationDockerContainerAttach: true,
OperationDockerContainerExec: true,
OperationDockerContainerRename: true,
OperationDockerContainerUpdate: true,
OperationDockerContainerPutContainerArchive: true,
OperationDockerContainerDelete: true,
OperationDockerImageList: true,
OperationDockerImageSearch: true,
OperationDockerImageGetAll: true,
OperationDockerImageGet: true,
OperationDockerImageHistory: true,
OperationDockerImageInspect: true,
OperationDockerImageLoad: true,
OperationDockerImageCreate: true,
OperationDockerImagePush: true,
OperationDockerImageTag: true,
OperationDockerImageDelete: true,
OperationDockerImageCommit: true,
OperationDockerImageBuild: true,
OperationDockerNetworkList: true,
OperationDockerNetworkInspect: true,
OperationDockerNetworkCreate: true,
OperationDockerNetworkConnect: true,
OperationDockerNetworkDisconnect: true,
OperationDockerNetworkDelete: true,
OperationDockerVolumeList: true,
OperationDockerVolumeInspect: true,
OperationDockerVolumeCreate: true,
OperationDockerVolumeDelete: true,
OperationDockerExecInspect: true,
OperationDockerExecStart: true,
OperationDockerExecResize: true,
OperationDockerSwarmInspect: true,
OperationDockerSwarmUnlockKey: true,
OperationDockerSwarmInit: true,
OperationDockerSwarmJoin: true,
OperationDockerSwarmLeave: true,
OperationDockerSwarmUpdate: true,
OperationDockerSwarmUnlock: true,
OperationDockerNodeList: true,
OperationDockerNodeInspect: true,
OperationDockerNodeUpdate: true,
OperationDockerNodeDelete: true,
OperationDockerServiceList: true,
OperationDockerServiceInspect: true,
OperationDockerServiceLogs: true,
OperationDockerServiceCreate: true,
OperationDockerServiceUpdate: true,
OperationDockerServiceDelete: true,
OperationDockerSecretList: true,
OperationDockerSecretInspect: true,
OperationDockerSecretCreate: true,
OperationDockerSecretUpdate: true,
OperationDockerSecretDelete: true,
OperationDockerConfigList: true,
OperationDockerConfigInspect: true,
OperationDockerConfigCreate: true,
OperationDockerConfigUpdate: true,
OperationDockerConfigDelete: true,
OperationDockerTaskList: true,
OperationDockerTaskInspect: true,
OperationDockerTaskLogs: true,
OperationDockerPluginList: true,
OperationDockerPluginPrivileges: true,
OperationDockerPluginInspect: true,
OperationDockerPluginPull: true,
OperationDockerPluginCreate: true,
OperationDockerPluginEnable: true,
OperationDockerPluginDisable: true,
OperationDockerPluginPush: true,
OperationDockerPluginUpgrade: true,
OperationDockerPluginSet: true,
OperationDockerPluginDelete: true,
OperationDockerSessionStart: true,
OperationDockerDistributionInspect: true,
OperationDockerBuildPrune: true,
OperationDockerBuildCancel: true,
OperationDockerPing: true,
OperationDockerInfo: true,
OperationDockerVersion: true,
OperationDockerEvents: true,
OperationDockerSystem: true,
OperationDockerUndefined: true,
OperationDockerAgentPing: true,
OperationDockerAgentList: true,
OperationDockerAgentHostInfo: true,
OperationDockerAgentUndefined: true,
OperationPortainerResourceControlUpdate: true,
OperationPortainerStackList: true,
OperationPortainerStackInspect: true,
OperationPortainerStackFile: true,
OperationPortainerStackCreate: true,
OperationPortainerStackMigrate: true,
OperationPortainerStackUpdate: true,
OperationPortainerStackDelete: true,
OperationPortainerWebsocketExec: true,
OperationPortainerWebhookList: true,
OperationPortainerWebhookCreate: true,
}

if volumeBrowsingAuthorizations {
authorizations[OperationDockerAgentBrowseGet] = true
authorizations[OperationDockerAgentBrowseList] = true
authorizations[OperationDockerAgentBrowseDelete] = true
authorizations[OperationDockerAgentBrowsePut] = true
authorizations[OperationDockerAgentBrowseRename] = true
}

return authorizations
}

// DefaultEndpointAuthorizationsForReadOnlyUserRole returns the default endpoint authorizations
// associated to the readonly user role.
func DefaultEndpointAuthorizationsForReadOnlyUserRole(volumeBrowsingAuthorizations bool) Authorizations {
authorizations := map[Authorization]bool{
OperationDockerContainerArchiveInfo: true,
OperationDockerContainerList: true,
OperationDockerContainerChanges: true,
OperationDockerContainerInspect: true,
OperationDockerContainerTop: true,
OperationDockerContainerLogs: true,
OperationDockerContainerStats: true,
OperationDockerImageList: true,
OperationDockerImageSearch: true,
OperationDockerImageGetAll: true,
OperationDockerImageGet: true,
OperationDockerImageHistory: true,
OperationDockerImageInspect: true,
OperationDockerNetworkList: true,
OperationDockerNetworkInspect: true,
OperationDockerVolumeList: true,
OperationDockerVolumeInspect: true,
OperationDockerSwarmInspect: true,
OperationDockerNodeList: true,
OperationDockerNodeInspect: true,
OperationDockerServiceList: true,
OperationDockerServiceInspect: true,
OperationDockerServiceLogs: true,
OperationDockerSecretList: true,
OperationDockerSecretInspect: true,
OperationDockerConfigList: true,
OperationDockerConfigInspect: true,
OperationDockerTaskList: true,
OperationDockerTaskInspect: true,
OperationDockerTaskLogs: true,
OperationDockerPluginList: true,
OperationDockerDistributionInspect: true,
OperationDockerPing: true,
OperationDockerInfo: true,
OperationDockerVersion: true,
OperationDockerEvents: true,
OperationDockerSystem: true,
OperationDockerAgentPing: true,
OperationDockerAgentList: true,
OperationDockerAgentHostInfo: true,
OperationPortainerStackList: true,
OperationPortainerStackInspect: true,
OperationPortainerStackFile: true,
OperationPortainerWebhookList: true,
}

if volumeBrowsingAuthorizations {
authorizations[OperationDockerAgentBrowseGet] = true
authorizations[OperationDockerAgentBrowseList] = true
}

return authorizations
}

// DefaultPortainerAuthorizations returns the default Portainer authorizations used by non-admin users.
func DefaultPortainerAuthorizations() Authorizations {
return map[Authorization]bool{


+ 12
- 355
api/bolt/init.go View File

@@ -32,141 +32,9 @@ func (store *Store) Init() error {

if len(roles) == 0 {
environmentAdministratorRole := &portainer.Role{
Name: "Endpoint administrator",
Description: "Full control of all resources in 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.OperationIntegrationStoridgeAdmin: true,
portainer.EndpointResourcesAccess: true,
},
Name: "Endpoint administrator",
Description: "Full control of all resources in an endpoint",
Authorizations: portainer.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
}

err = store.RoleService.CreateRole(environmentAdministratorRole)
@@ -175,55 +43,9 @@ func (store *Store) Init() error {
}

environmentReadOnlyUserRole := &portainer.Role{
Name: "Helpdesk",
Description: "Read-only access of all resources in 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.OperationPortainerStackList: true,
portainer.OperationPortainerStackInspect: true,
portainer.OperationPortainerStackFile: true,
portainer.OperationPortainerWebhookList: true,
portainer.EndpointResourcesAccess: true,
},
Name: "Helpdesk",
Description: "Read-only access of all resources in an endpoint",
Authorizations: portainer.DefaultEndpointAuthorizationsForHelpDeskRole(false),
}

err = store.RoleService.CreateRole(environmentReadOnlyUserRole)
@@ -232,129 +54,9 @@ func (store *Store) Init() error {
}

standardUserRole := &portainer.Role{
Name: "Standard user",
Description: "Full control of assigned resources in 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.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.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,
},
Name: "Standard user",
Description: "Full control of assigned resources in an endpoint",
Authorizations: portainer.DefaultEndpointAuthorizationsForStandardUserRole(false),
}

err = store.RoleService.CreateRole(standardUserRole)
@@ -363,54 +65,9 @@ func (store *Store) Init() error {
}

readOnlyUserRole := &portainer.Role{
Name: "Read-only user",
Description: "Read-only access of assigned resources in 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.OperationPortainerStackList: true,
portainer.OperationPortainerStackInspect: true,
portainer.OperationPortainerStackFile: true,
portainer.OperationPortainerWebhookList: true,
},
Name: "Read-only user",
Description: "Read-only access of assigned resources in an endpoint",
Authorizations: portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(false),
}

err = store.RoleService.CreateRole(readOnlyUserRole)


+ 68
- 5
api/bolt/migrator/migrate_dbversion20.go View File

@@ -1,15 +1,36 @@
package migrator

import (
portainer "github.com/portainer/portainer/api"
)
import portainer "github.com/portainer/portainer/api"

func (m *Migrator) updateUsersToDBVersion21() error {
func (m *Migrator) updateResourceControlsToDBVersion22() error {
legacyResourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
}

for _, resourceControl := range legacyResourceControls {
resourceControl.AdministratorsOnly = false

err := m.resourceControlService.UpdateResourceControl(resourceControl.ID, &resourceControl)
if err != nil {
return err
}
}

return nil
}

func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
legacyUsers, err := m.userService.Users()
if err != nil {
return err
}

settings, err := m.settingsService.Settings()
if err != nil {
return err
}

for _, user := range legacyUsers {
user.PortainerAuthorizations = portainer.DefaultPortainerAuthorizations()
err = m.userService.UpdateUser(user.ID, &user)
@@ -18,5 +39,47 @@ func (m *Migrator) updateUsersToDBVersion21() error {
}
}

return nil
endpointAdministratorRole, err := m.roleService.Role(portainer.RoleID(1))
if err != nil {
return err
}
endpointAdministratorRole.Authorizations = portainer.DefaultEndpointAuthorizationsForEndpointAdministratorRole()

err = m.roleService.UpdateRole(endpointAdministratorRole.ID, endpointAdministratorRole)

helpDeskRole, err := m.roleService.Role(portainer.RoleID(2))
if err != nil {
return err
}
helpDeskRole.Authorizations = portainer.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)

err = m.roleService.UpdateRole(helpDeskRole.ID, helpDeskRole)

standardUserRole, err := m.roleService.Role(portainer.RoleID(3))
if err != nil {
return err
}
standardUserRole.Authorizations = portainer.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)

err = m.roleService.UpdateRole(standardUserRole.ID, standardUserRole)

readOnlyUserRole, err := m.roleService.Role(portainer.RoleID(4))
if err != nil {
return err
}
readOnlyUserRole.Authorizations = portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)

err = m.roleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole)

authorizationServiceParameters := &portainer.AuthorizationServiceParameters{
EndpointService: m.endpointService,
EndpointGroupService: m.endpointGroupService,
RegistryService: m.registryService,
RoleService: m.roleService,
TeamMembershipService: m.teamMembershipService,
UserService: m.userService,
}

authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters)
return authorizationService.UpdateUsersAuthorizations()
}

+ 8
- 2
api/bolt/migrator/migrator.go View File

@@ -288,8 +288,14 @@ func (m *Migrator) Migrate() error {
}

// Portainer next
if m.currentDBVersion < 21 {
err := m.updateUsersToDBVersion21()
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
if m.currentDBVersion < 22 {
err := m.updateResourceControlsToDBVersion22()
if err != nil {
return err
}

err = m.updateUsersAndRolesToDBVersion22()
if err != nil {
return err
}


+ 5
- 8
api/bolt/resourcecontrol/resourcecontrol.go View File

@@ -42,9 +42,10 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
return &resourceControl, nil
}

// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
// to the main ResourceID or in SubResourceIDs
func (service *Service) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
// ResourceControlByResourceIDAndType returns a ResourceControl object by checking if the resourceID is equal
// to the main ResourceID or in SubResourceIDs. It also performs a check on the resource type. Return nil
// if no ResourceControl was found.
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl

err := service.db.View(func(tx *bolt.Tx) error {
@@ -58,7 +59,7 @@ func (service *Service) ResourceControlByResourceID(resourceID string) (*portain
return err
}

if rc.ResourceID == resourceID {
if rc.ResourceID == resourceID && rc.Type == resourceType {
resourceControl = &rc
break
}
@@ -71,10 +72,6 @@ func (service *Service) ResourceControlByResourceID(resourceID string) (*portain
}
}

if resourceControl == nil {
return portainer.ErrObjectNotFound
}

return nil
})



+ 5
- 4
api/docker/client.go View File

@@ -14,6 +14,7 @@ import (
const (
unsupportedEnvironmentType = portainer.Error("Environment not supported")
defaultDockerRequestTimeout = 60
dockerClientVersion = "1.40"
)

// ClientFactory is used to create Docker clients
@@ -51,7 +52,7 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam
func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
return client.NewClientWithOpts(
client.WithHost(endpoint.URL),
client.WithVersion(portainer.SupportedDockerAPIVersion),
client.WithVersion(dockerClientVersion),
)
}

@@ -63,7 +64,7 @@ func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {

return client.NewClientWithOpts(
client.WithHost(endpoint.URL),
client.WithVersion(portainer.SupportedDockerAPIVersion),
client.WithVersion(dockerClientVersion),
client.WithHTTPClient(httpCli),
)
}
@@ -84,7 +85,7 @@ func createEdgeClient(endpoint *portainer.Endpoint, reverseTunnelService portain

return client.NewClientWithOpts(
client.WithHost(endpointURL),
client.WithVersion(portainer.SupportedDockerAPIVersion),
client.WithVersion(dockerClientVersion),
client.WithHTTPClient(httpCli),
client.WithHTTPHeaders(headers),
)
@@ -112,7 +113,7 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.

return client.NewClientWithOpts(
client.WithHost(endpoint.URL),
client.WithVersion(portainer.SupportedDockerAPIVersion),
client.WithVersion(dockerClientVersion),
client.WithHTTPClient(httpCli),
client.WithHTTPHeaders(headers),
)


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

@@ -29,9 +29,9 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R
}

var proxy http.Handler
proxy = handler.ProxyManager.GetProxy(endpoint)
proxy = handler.ProxyManager.GetEndpointProxy(endpoint)
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
}


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

@@ -37,7 +37,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.

tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle {
handler.ProxyManager.DeleteProxy(endpoint)
handler.ProxyManager.DeleteEndpointProxy(endpoint)

err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
@@ -55,9 +55,9 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
}

var proxy http.Handler
proxy = handler.ProxyManager.GetProxy(endpoint)
proxy = handler.ProxyManager.GetEndpointProxy(endpoint)
if proxy == nil {
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
}


+ 1
- 1
api/http/handler/endpoints/endpoint_delete.go View File

@@ -41,7 +41,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
}

handler.ProxyManager.DeleteProxy(endpoint)
handler.ProxyManager.DeleteEndpointProxy(endpoint)

if len(endpoint.UserAccessPolicies) > 0 || len(endpoint.TeamAccessPolicies) > 0 {
err = handler.AuthorizationService.UpdateUsersAuthorizations()


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

@@ -166,7 +166,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}

if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
}


+ 2
- 3
api/http/handler/resourcecontrols/handler.go View File

@@ -21,11 +21,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
Router: mux.NewRouter(),
}
h.Handle("/resource_controls",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlCreate))).Methods(http.MethodPost)
bouncer.AdminAccess(httperror.LoggerHandler(h.resourceControlCreate))).Methods(http.MethodPost)
h.Handle("/resource_controls/{id}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlUpdate))).Methods(http.MethodPut)
h.Handle("/resource_controls/{id}",
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlDelete))).Methods(http.MethodDelete)

bouncer.AdminAccess(httperror.LoggerHandler(h.resourceControlDelete))).Methods(http.MethodDelete)
return h
}

+ 25
- 28
api/http/handler/resourcecontrols/resourcecontrol_create.go View File

@@ -1,6 +1,7 @@
package resourcecontrols

import (
"errors"
"net/http"

"github.com/asaskevich/govalidator"
@@ -8,29 +9,33 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)

type resourceControlCreatePayload struct {
ResourceID string
Type string
Public bool
Users []int
Teams []int
SubResourceIDs []string
ResourceID string
Type string
Public bool
AdministratorsOnly bool
Users []int
Teams []int
SubResourceIDs []string
}

func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.ResourceID) {
return portainer.Error("Invalid resource identifier")
return errors.New("invalid payload: invalid resource identifier")
}

if govalidator.IsNull(payload.Type) {
return portainer.Error("Invalid type")
return errors.New("invalid payload: invalid type")
}

if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public {
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or Public")
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public && !payload.AdministratorsOnly {
return errors.New("invalid payload: must specify Users, Teams, Public or AdministratorsOnly")
}

if payload.Public && payload.AdministratorsOnly {
return errors.New("invalid payload: cannot set both public and administrators only flags to true")
}
return nil
}
@@ -63,8 +68,8 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusBadRequest, "Invalid type value. Value must be one of: container, service, volume, network, secret, stack or config", portainer.ErrInvalidResourceControlType}
}

rc, err := handler.ResourceControlService.ResourceControlByResourceID(payload.ResourceID)
if err != nil && err != portainer.ErrObjectNotFound {
rc, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(payload.ResourceID, resourceControlType)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err}
}
if rc != nil {
@@ -90,21 +95,13 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req
}

resourceControl := portainer.ResourceControl{
ResourceID: payload.ResourceID,
SubResourceIDs: payload.SubResourceIDs,
Type: resourceControlType,
Public: payload.Public,
UserAccesses: userAccesses,
TeamAccesses: teamAccesses,
}

securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}

if !security.AuthorizedResourceControlCreation(&resourceControl, securityContext) {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create a resource control for the specified resource", portainer.ErrResourceAccessDenied}
ResourceID: payload.ResourceID,
SubResourceIDs: payload.SubResourceIDs,
Type: resourceControlType,
Public: payload.Public,
AdministratorsOnly: payload.AdministratorsOnly,
UserAccesses: userAccesses,
TeamAccesses: teamAccesses,
}

err = handler.ResourceControlService.CreateResourceControl(&resourceControl)


+ 1
- 11
api/http/handler/resourcecontrols/resourcecontrol_delete.go View File

@@ -7,7 +7,6 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)

// DELETE request on /api/resource_controls/:id
@@ -17,22 +16,13 @@ func (handler *Handler) resourceControlDelete(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusBadRequest, "Invalid resource control identifier route variable", err}
}

resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
_, err = handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a resource control with 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 !security.AuthorizedResourceControlDeletion(resourceControl, securityContext) {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to delete the resource control", portainer.ErrResourceAccessDenied}
}

err = handler.ResourceControlService.DeleteResourceControl(portainer.ResourceControlID(resourceControlID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the resource control from the database", err}


+ 13
- 6
api/http/handler/resourcecontrols/resourcecontrol_update.go View File

@@ -1,6 +1,7 @@
package resourcecontrols

import (
"errors"
"net/http"

httperror "github.com/portainer/libhttp/error"
@@ -11,14 +12,19 @@ import (
)

type resourceControlUpdatePayload struct {
Public bool
Users []int
Teams []int
Public bool
Users []int
Teams []int
AdministratorsOnly bool
}

func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error {
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public {
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or Public")
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public && !payload.AdministratorsOnly {
return errors.New("invalid payload: must specify Users, Teams, Public or AdministratorsOnly")
}

if payload.Public && payload.AdministratorsOnly {
return errors.New("invalid payload: cannot set public and administrators only")
}
return nil
}
@@ -49,10 +55,11 @@ func (handler *Handler) resourceControlUpdate(w http.ResponseWriter, r *http.Req
}

if !security.AuthorizedResourceControlAccess(resourceControl, securityContext) {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the resource control", portainer.ErrResourceAccessDenied}
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access the resource control", portainer.ErrResourceAccessDenied}
}

resourceControl.Public = payload.Public
resourceControl.AdministratorsOnly = payload.AdministratorsOnly

var userAccesses = make([]portainer.UserResourceAccess, 0)
for _, v := range payload.Users {


+ 6
- 7
api/http/handler/stacks/create_compose_stack.go View File

@@ -10,7 +10,6 @@ import (
"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/filesystem"
"github.com/portainer/portainer/api/http/security"
@@ -32,7 +31,7 @@ func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) err
return nil
}

func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload composeStackFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
@@ -86,7 +85,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
}

doCleanUp = false
return response.JSON(w, stack)
return handler.decorateStackResponse(w, stack, userID)
}

type composeStackFromGitRepositoryPayload struct {
@@ -116,7 +115,7 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
return nil
}

func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload composeStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
@@ -180,7 +179,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
}

doCleanUp = false
return response.JSON(w, stack)
return handler.decorateStackResponse(w, stack, userID)
}

type composeStackFromFileUploadPayload struct {
@@ -211,7 +210,7 @@ func (payload *composeStackFromFileUploadPayload) Validate(r *http.Request) erro
return nil
}

func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
payload := &composeStackFromFileUploadPayload{}
err := payload.Validate(r)
if err != nil {
@@ -265,7 +264,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
}

doCleanUp = false
return response.JSON(w, stack)
return handler.decorateStackResponse(w, stack, userID)
}

type composeStackDeploymentConfig struct {


+ 6
- 7
api/http/handler/stacks/create_swarm_stack.go View File

@@ -10,7 +10,6 @@ import (
"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/filesystem"
"github.com/portainer/portainer/api/http/security"
@@ -36,7 +35,7 @@ func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error
return nil
}

func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload swarmStackFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
@@ -91,7 +90,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
}

doCleanUp = false
return response.JSON(w, stack)
return handler.decorateStackResponse(w, stack, userID)
}

type swarmStackFromGitRepositoryPayload struct {
@@ -125,7 +124,7 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
return nil
}

func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload swarmStackFromGitRepositoryPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
@@ -190,7 +189,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
}

doCleanUp = false
return response.JSON(w, stack)
return handler.decorateStackResponse(w, stack, userID)
}

type swarmStackFromFileUploadPayload struct {
@@ -228,7 +227,7 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error
return nil
}

func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
payload := &swarmStackFromFileUploadPayload{}
err := payload.Validate(r)
if err != nil {
@@ -283,7 +282,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
}

doCleanUp = false
return response.JSON(w, stack)
return handler.decorateStackResponse(w, stack, userID)
}

type swarmStackDeploymentConfig struct {


+ 35
- 0
api/http/handler/stacks/handler.go View File

@@ -26,6 +26,8 @@ type Handler struct {
SwarmStackManager portainer.SwarmStackManager
ComposeStackManager portainer.ComposeStackManager
SettingsService portainer.SettingsService
UserService portainer.UserService
ExtensionService portainer.ExtensionService
}

// NewHandler creates a handler to manage stack operations.
@@ -52,3 +54,36 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackMigrate))).Methods(http.MethodPost)
return h
}

func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID, resourceControl *portainer.ResourceControl) (bool, error) {
if securityContext.IsAdmin {
return true, nil
}

userTeamIDs := make([]portainer.TeamID, 0)
for _, membership := range securityContext.UserMemberships {
userTeamIDs = append(userTeamIDs, membership.TeamID)
}

if resourceControl != nil && portainer.UserCanAccessResource(securityContext.UserID, userTeamIDs, resourceControl) {
return true, nil
}

_, err := handler.ExtensionService.Extension(portainer.RBACExtension)
if err == portainer.ErrObjectNotFound {
return false, nil
} else if err != nil && err != portainer.ErrObjectNotFound {
return false, err
}

user, err := handler.UserService.User(securityContext.UserID)
if err != nil {
return false, err
}

_, ok := user.EndpointAuthorizations[endpointID][portainer.EndpointResourcesAccess]
if ok {
return true, nil
}
return false, nil
}

+ 30
- 12
api/http/handler/stacks/stack_create.go View File

@@ -5,12 +5,13 @@ import (
"log"
"net/http"

"github.com/docker/cli/cli/compose/types"

"github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/types"
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/security"
)

func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error {
@@ -54,38 +55,43 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}

tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user details from authentication token", err}
}

switch portainer.StackType(stackType) {
case portainer.DockerSwarmStack:
return handler.createSwarmStack(w, r, method, endpoint)
return handler.createSwarmStack(w, r, method, endpoint, tokenData.ID)
case portainer.DockerComposeStack:
return handler.createComposeStack(w, r, method, endpoint)
return handler.createComposeStack(w, r, method, endpoint, tokenData.ID)
}

return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", errors.New(request.ErrInvalidQueryParameter)}
}

func (handler *Handler) createComposeStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
func (handler *Handler) createComposeStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {

switch method {
case "string":
return handler.createComposeStackFromFileContent(w, r, endpoint)
return handler.createComposeStackFromFileContent(w, r, endpoint, userID)
case "repository":
return handler.createComposeStackFromGitRepository(w, r, endpoint)
return handler.createComposeStackFromGitRepository(w, r, endpoint, userID)
case "file":
return handler.createComposeStackFromFileUpload(w, r, endpoint)
return handler.createComposeStackFromFileUpload(w, r, endpoint, userID)
}

return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
}

func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
switch method {
case "string":
return handler.createSwarmStackFromFileContent(w, r, endpoint)
return handler.createSwarmStackFromFileContent(w, r, endpoint, userID)
case "repository":
return handler.createSwarmStackFromGitRepository(w, r, endpoint)
return handler.createSwarmStackFromGitRepository(w, r, endpoint, userID)
case "file":
return handler.createSwarmStackFromFileUpload(w, r, endpoint)
return handler.createSwarmStackFromFileUpload(w, r, endpoint, userID)
}

return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
@@ -125,3 +131,15 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error)

return true, nil
}

func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {
resourceControl := portainer.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)

err := handler.ResourceControlService.CreateResourceControl(resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err}
}

stack.ResourceControl = resourceControl
return response.JSON(w, stack)
}

+ 15
- 7
api/http/handler/stacks/stack_delete.go View File

@@ -4,7 +4,6 @@ import (
"net/http"
"strconv"

"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/security"

httperror "github.com/portainer/libhttp/error"
@@ -64,8 +63,8 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}

resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
if err != nil && err != portainer.ErrObjectNotFound {
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

@@ -74,10 +73,12 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
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}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
}

err = handler.deleteStack(stack, endpoint)
@@ -90,6 +91,13 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the stack from the database", err}
}

if resourceControl != nil {
err = handler.ResourceControlService.DeleteResourceControl(resourceControl.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the associated resource control from the database", err}
}
}

err = handler.FileService.RemoveDirectory(stack.ProjectPath)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove stack files from disk", err}


+ 7
- 13
api/http/handler/stacks/stack_file.go View File

@@ -8,7 +8,6 @@ import (
"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"
)

@@ -42,8 +41,8 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}

resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
if err != nil && err != portainer.ErrObjectNotFound {
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

@@ -52,17 +51,12 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}

extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
if !securityContext.IsAdmin && resourceControl == nil {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}

if resourceControl != nil {
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
extendedStack.ResourceControl = *resourceControl
} else {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
}

stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))


+ 12
- 14
api/http/handler/stacks/stack_inspect.go View File

@@ -7,7 +7,6 @@ import (
"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"
)

@@ -37,28 +36,27 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
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 user info from request context", err}
}

securityContext, err := security.RetrieveRestrictedRequestContext(r)
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}

extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
if !securityContext.IsAdmin && resourceControl == nil {
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
}

if resourceControl != nil {
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
extendedStack.ResourceControl = *resourceControl
} else {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
}
stack.ResourceControl = resourceControl
}

return response.JSON(w, extendedStack)
return response.JSON(w, stack)
}

+ 24
- 4
api/http/handler/stacks/stack_list.go View File

@@ -7,7 +7,6 @@ import (
"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"
)

@@ -40,10 +39,31 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}

filteredStacks := proxy.FilterStacks(stacks, resourceControls, securityContext.IsAdmin,
securityContext.UserID, securityContext.UserMemberships)
stacks = portainer.DecorateStacks(stacks, resourceControls)

return response.JSON(w, filteredStacks)
if !securityContext.IsAdmin {
rbacExtensionEnabled := true
_, err := handler.ExtensionService.Extension(portainer.RBACExtension)
if err == portainer.ErrObjectNotFound {
rbacExtensionEnabled = false
} else if err != nil && err != portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check if RBAC extension is enabled", err}
}

user, err := handler.UserService.User(securityContext.UserID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user information from the database", err}
}

userTeamIDs := make([]portainer.TeamID, 0)
for _, membership := range securityContext.UserMemberships {
userTeamIDs = append(userTeamIDs, membership.TeamID)
}

stacks = portainer.FilterAuthorizedStacks(stacks, user, userTeamIDs, rbacExtensionEnabled)
}

return response.JSON(w, stacks)
}

func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters) []portainer.Stack {


+ 8
- 7
api/http/handler/stacks/stack_migrate.go View File

@@ -7,7 +7,6 @@ import (
"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"
)

@@ -56,8 +55,8 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
}

resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
if err != nil && err != portainer.ErrObjectN