diff --git a/api/access_control.go b/api/access_control.go new file mode 100644 index 000000000..7c767ce03 --- /dev/null +++ b/api/access_control.go @@ -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 +} diff --git a/api/authorizations.go b/api/authorizations.go index 5fb3f1b77..865067301 100644 --- a/api/authorizations.go +++ b/api/authorizations.go @@ -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{ diff --git a/api/bolt/init.go b/api/bolt/init.go index a9b941179..f1161c536 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -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) diff --git a/api/bolt/migrator/migrate_dbversion20.go b/api/bolt/migrator/migrate_dbversion20.go index 09b642855..6afefe1b3 100644 --- a/api/bolt/migrator/migrate_dbversion20.go +++ b/api/bolt/migrator/migrate_dbversion20.go @@ -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() } diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index fa012355c..0194346c0 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -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 } diff --git a/api/bolt/resourcecontrol/resourcecontrol.go b/api/bolt/resourcecontrol/resourcecontrol.go index d4fdb3026..ef07aff03 100644 --- a/api/bolt/resourcecontrol/resourcecontrol.go +++ b/api/bolt/resourcecontrol/resourcecontrol.go @@ -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 }) diff --git a/api/docker/client.go b/api/docker/client.go index b093f23cf..9b63484e9 100644 --- a/api/docker/client.go +++ b/api/docker/client.go @@ -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), ) diff --git a/api/http/handler/endpointproxy/proxy_azure.go b/api/http/handler/endpointproxy/proxy_azure.go index 782f81a25..6ffc4598a 100644 --- a/api/http/handler/endpointproxy/proxy_azure.go +++ b/api/http/handler/endpointproxy/proxy_azure.go @@ -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} } diff --git a/api/http/handler/endpointproxy/proxy_docker.go b/api/http/handler/endpointproxy/proxy_docker.go index 35490a8ff..d84aacc62 100644 --- a/api/http/handler/endpointproxy/proxy_docker.go +++ b/api/http/handler/endpointproxy/proxy_docker.go @@ -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} } diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go index 4d82f9c60..acd85e03c 100644 --- a/api/http/handler/endpoints/endpoint_delete.go +++ b/api/http/handler/endpoints/endpoint_delete.go @@ -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() diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 6816ec0d7..dd87c22f2 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -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} } diff --git a/api/http/handler/resourcecontrols/handler.go b/api/http/handler/resourcecontrols/handler.go index a2227f2c8..e6851c649 100644 --- a/api/http/handler/resourcecontrols/handler.go +++ b/api/http/handler/resourcecontrols/handler.go @@ -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 } diff --git a/api/http/handler/resourcecontrols/resourcecontrol_create.go b/api/http/handler/resourcecontrols/resourcecontrol_create.go index ad954805d..0a6696bd9 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_create.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_create.go @@ -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) diff --git a/api/http/handler/resourcecontrols/resourcecontrol_delete.go b/api/http/handler/resourcecontrols/resourcecontrol_delete.go index 1d1ea8ddf..76794e423 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_delete.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_delete.go @@ -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} diff --git a/api/http/handler/resourcecontrols/resourcecontrol_update.go b/api/http/handler/resourcecontrols/resourcecontrol_update.go index 360e704c9..fc170f1bd 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_update.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_update.go @@ -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 { diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index 7cae40cf9..e53050721 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -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 { diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index 4210cab0e..143292ea9 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -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 { diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index b81270543..d0a6b4ea5 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -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 +} diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index aba79482c..dc374c4b1 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -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) +} diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index 2607fa10c..180279217 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -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} diff --git a/api/http/handler/stacks/stack_file.go b/api/http/handler/stacks/stack_file.go index fd9767ffd..7c966a08e 100644 --- a/api/http/handler/stacks/stack_file.go +++ b/api/http/handler/stacks/stack_file.go @@ -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)) diff --git a/api/http/handler/stacks/stack_inspect.go b/api/http/handler/stacks/stack_inspect.go index 878a20361..42d0ad9c5 100644 --- a/api/http/handler/stacks/stack_inspect.go +++ b/api/http/handler/stacks/stack_inspect.go @@ -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 { + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err} + } + + 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} } - securityContext, err := security.RetrieveRestrictedRequestContext(r) + access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl) if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err} } - - extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}} - if !securityContext.IsAdmin && resourceControl == nil { + 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) } diff --git a/api/http/handler/stacks/stack_list.go b/api/http/handler/stacks/stack_list.go index aabd68eee..2c87cb40b 100644 --- a/api/http/handler/stacks/stack_list.go +++ b/api/http/handler/stacks/stack_list.go @@ -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 { diff --git a/api/http/handler/stacks/stack_migrate.go b/api/http/handler/stacks/stack_migrate.go index 340d57310..690622a7a 100644 --- a/api/http/handler/stacks/stack_migrate.go +++ b/api/http/handler/stacks/stack_migrate.go @@ -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.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} } @@ -66,10 +65,12 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht 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} } // TODO: this is a work-around for stacks created with Portainer version >= 1.17.1 diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go index 63f8eb323..1e4e08fdf 100644 --- a/api/http/handler/stacks/stack_update.go +++ b/api/http/handler/stacks/stack_update.go @@ -4,7 +4,6 @@ import ( "net/http" "strconv" - "github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/security" "github.com/asaskevich/govalidator" @@ -76,8 +75,8 @@ func (handler *Handler) stackUpdate(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} } @@ -86,10 +85,12 @@ func (handler *Handler) stackUpdate(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} } updateError := handler.updateAndDeployStack(r, stack, endpoint) diff --git a/api/http/proxy/access_control.go b/api/http/proxy/access_control.go deleted file mode 100644 index fea0014ec..000000000 --- a/api/http/proxy/access_control.go +++ /dev/null @@ -1,166 +0,0 @@ -package proxy - -import ( - "github.com/portainer/portainer/api" -) - -type ( - // ExtendedStack represents a stack combined with its associated access control - ExtendedStack struct { - portainer.Stack - ResourceControl portainer.ResourceControl `json:"ResourceControl"` - } -) - -// applyResourceAccessControlFromLabel returns an optionally decorated object as the first return value and the -// access level for the user (granted or denied) as the second return value. -// It will retrieve an identifier from the labels object. If an identifier exists, it will check for -// an existing resource control associated to it. -// Returns a decorated object and authorized access (true) when a resource control is found and the user can access the resource. -// Returns the original object and denied access (false) when no resource control is found. -// Returns the original object and denied access (false) when a resource control is found and the user cannot access the resource. -func applyResourceAccessControlFromLabel(labelsObject, resourceObject map[string]interface{}, labelIdentifier string, - context *restrictedDockerOperationContext) (map[string]interface{}, bool) { - - if labelsObject != nil && labelsObject[labelIdentifier] != nil { - resourceIdentifier := labelsObject[labelIdentifier].(string) - return applyResourceAccessControl(resourceObject, resourceIdentifier, context) - } - return resourceObject, false -} - -// applyResourceAccessControl returns an optionally decorated object as the first return value and the -// access level for the user (granted or denied) as the second return value. -// Returns a decorated object and authorized access (true) when a resource control is found to the specified resource -// identifier and the user can access the resource. -// Returns the original object and authorized access (false) when no resource control is found for the specified -// resource identifier. -// Returns the original object and denied access (false) when a resource control is associated to the resource -// and the user cannot access the resource. -func applyResourceAccessControl(resourceObject map[string]interface{}, resourceIdentifier string, - context *restrictedDockerOperationContext) (map[string]interface{}, bool) { - - resourceControl := getResourceControlByResourceID(resourceIdentifier, context.resourceControls) - if resourceControl == nil { - return resourceObject, context.isAdmin || context.endpointResourceAccess - } - - if context.isAdmin || context.endpointResourceAccess || resourceControl.Public || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) { - resourceObject = decorateObject(resourceObject, resourceControl) - return resourceObject, true - } - - return resourceObject, false -} - -// decorateResourceWithAccessControlFromLabel will retrieve an identifier from the labels object. If an identifier exists, -// it will check for an existing resource control associated to it. If a resource control is found, the resource object will be -// decorated. If no identifier can be found in the labels or no resource control is associated to the identifier, the resource -// object will not be changed. -func decorateResourceWithAccessControlFromLabel(labelsObject, resourceObject map[string]interface{}, labelIdentifier string, - resourceControls []portainer.ResourceControl) map[string]interface{} { - - if labelsObject != nil && labelsObject[labelIdentifier] != nil { - resourceIdentifier := labelsObject[labelIdentifier].(string) - resourceObject = decorateResourceWithAccessControl(resourceObject, resourceIdentifier, resourceControls) - } - - return resourceObject -} - -// decorateResourceWithAccessControl will check if a resource control is associated to the specified resource identifier. -// If a resource control is found, the resource object will be decorated, otherwise it will not be changed. -func decorateResourceWithAccessControl(resourceObject map[string]interface{}, resourceIdentifier string, - resourceControls []portainer.ResourceControl) map[string]interface{} { - - resourceControl := getResourceControlByResourceID(resourceIdentifier, resourceControls) - if resourceControl != nil { - return decorateObject(resourceObject, resourceControl) - } - return resourceObject -} - -func canUserAccessResource(userID portainer.UserID, userTeamIDs []portainer.TeamID, resourceControl *portainer.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 -} - -func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} { - if object["Portainer"] == nil { - object["Portainer"] = make(map[string]interface{}) - } - - portainerMetadata := object["Portainer"].(map[string]interface{}) - portainerMetadata["ResourceControl"] = resourceControl - return object -} - -func getResourceControlByResourceID(resourceID string, resourceControls []portainer.ResourceControl) *portainer.ResourceControl { - for _, resourceControl := range resourceControls { - if resourceID == resourceControl.ResourceID { - return &resourceControl - } - for _, subResourceID := range resourceControl.SubResourceIDs { - if resourceID == subResourceID { - return &resourceControl - } - } - } - return nil -} - -// CanAccessStack checks if a user can access a stack -func CanAccessStack(stack *portainer.Stack, resourceControl *portainer.ResourceControl, userID portainer.UserID, memberships []portainer.TeamMembership) bool { - if resourceControl == nil { - return false - } - - userTeamIDs := make([]portainer.TeamID, 0) - for _, membership := range memberships { - userTeamIDs = append(userTeamIDs, membership.TeamID) - } - - if canUserAccessResource(userID, userTeamIDs, resourceControl) { - return true - } - - return resourceControl.Public -} - -// FilterStacks filters stacks based on user role and resource controls. -func FilterStacks(stacks []portainer.Stack, resourceControls []portainer.ResourceControl, isAdmin bool, - userID portainer.UserID, memberships []portainer.TeamMembership) []ExtendedStack { - - filteredStacks := make([]ExtendedStack, 0) - - userTeamIDs := make([]portainer.TeamID, 0) - for _, membership := range memberships { - userTeamIDs = append(userTeamIDs, membership.TeamID) - } - - for _, stack := range stacks { - extendedStack := ExtendedStack{stack, portainer.ResourceControl{}} - resourceControl := getResourceControlByResourceID(stack.Name, resourceControls) - if resourceControl == nil && isAdmin { - filteredStacks = append(filteredStacks, extendedStack) - } else if resourceControl != nil && (isAdmin || resourceControl.Public || canUserAccessResource(userID, userTeamIDs, resourceControl)) { - extendedStack.ResourceControl = *resourceControl - filteredStacks = append(filteredStacks, extendedStack) - } - } - - return filteredStacks -} diff --git a/api/http/proxy/configs.go b/api/http/proxy/configs.go deleted file mode 100644 index 6863b5971..000000000 --- a/api/http/proxy/configs.go +++ /dev/null @@ -1,107 +0,0 @@ -package proxy - -import ( - "net/http" - - "github.com/portainer/portainer/api" -) - -const ( - // ErrDockerConfigIdentifierNotFound defines an error raised when Portainer is unable to find a config identifier - ErrDockerConfigIdentifierNotFound = portainer.Error("Docker config identifier not found") - configIdentifier = "ID" -) - -// configListOperation extracts the response as a JSON object, loop through the configs array -// decorate and/or filter the configs based on resource controls before rewriting the response -func configListOperation(response *http.Response, executor *operationExecutor) error { - var err error - - // ConfigList response is a JSON array - // https://docs.docker.com/engine/api/v1.30/#operation/ConfigList - responseArray, err := getResponseAsJSONArray(response) - if err != nil { - return err - } - - if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { - responseArray, err = decorateConfigList(responseArray, executor.operationContext.resourceControls) - } else { - responseArray, err = filterConfigList(responseArray, executor.operationContext) - } - if err != nil { - return err - } - - return rewriteResponse(response, responseArray, http.StatusOK) -} - -// configInspectOperation extracts the response as a JSON object, verify that the user -// has access to the config based on resource control (check are done based on the configID and optional Swarm service ID) -// and either rewrite an access denied response or a decorated config. -func configInspectOperation(response *http.Response, executor *operationExecutor) error { - // ConfigInspect response is a JSON object - // https://docs.docker.com/engine/api/v1.30/#operation/ConfigInspect - responseObject, err := getResponseAsJSONOBject(response) - if err != nil { - return err - } - - if responseObject[configIdentifier] == nil { - return ErrDockerConfigIdentifierNotFound - } - - configID := responseObject[configIdentifier].(string) - responseObject, access := applyResourceAccessControl(responseObject, configID, executor.operationContext) - if !access { - return rewriteAccessDeniedResponse(response) - } - - return rewriteResponse(response, responseObject, http.StatusOK) -} - -// decorateConfigList loops through all configs and decorates any config with an existing resource control. -// Resource controls checks are based on: resource identifier. -// Config object schema reference: https://docs.docker.com/engine/api/v1.30/#operation/ConfigList -func decorateConfigList(configData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) { - decoratedConfigData := make([]interface{}, 0) - - for _, config := range configData { - - configObject := config.(map[string]interface{}) - if configObject[configIdentifier] == nil { - return nil, ErrDockerConfigIdentifierNotFound - } - - configID := configObject[configIdentifier].(string) - configObject = decorateResourceWithAccessControl(configObject, configID, resourceControls) - - decoratedConfigData = append(decoratedConfigData, configObject) - } - - return decoratedConfigData, nil -} - -// filterConfigList loops through all configs and filters public configs (no associated resource control) -// as well as authorized configs (access granted to the user based on existing resource control). -// Authorized configs are decorated during the process. -// Resource controls checks are based on: resource identifier. -// Config object schema reference: https://docs.docker.com/engine/api/v1.30/#operation/ConfigList -func filterConfigList(configData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { - filteredConfigData := make([]interface{}, 0) - - for _, config := range configData { - configObject := config.(map[string]interface{}) - if configObject[configIdentifier] == nil { - return nil, ErrDockerConfigIdentifierNotFound - } - - configID := configObject[configIdentifier].(string) - configObject, access := applyResourceAccessControl(configObject, configID, context) - if access { - filteredConfigData = append(filteredConfigData, configObject) - } - } - - return filteredConfigData, nil -} diff --git a/api/http/proxy/containers.go b/api/http/proxy/containers.go deleted file mode 100644 index 81bde4c4f..000000000 --- a/api/http/proxy/containers.go +++ /dev/null @@ -1,204 +0,0 @@ -package proxy - -import ( - "net/http" - - "github.com/portainer/portainer/api" -) - -const ( - // ErrDockerContainerIdentifierNotFound defines an error raised when Portainer is unable to find a container identifier - ErrDockerContainerIdentifierNotFound = portainer.Error("Docker container identifier not found") - containerIdentifier = "Id" - containerLabelForServiceIdentifier = "com.docker.swarm.service.id" - containerLabelForSwarmStackIdentifier = "com.docker.stack.namespace" - containerLabelForComposeStackIdentifier = "com.docker.compose.project" -) - -// containerListOperation extracts the response as a JSON object, loop through the containers array -// decorate and/or filter the containers based on resource controls before rewriting the response -func containerListOperation(response *http.Response, executor *operationExecutor) error { - var err error - // ContainerList response is a JSON array - // https://docs.docker.com/engine/api/v1.28/#operation/ContainerList - responseArray, err := getResponseAsJSONArray(response) - if err != nil { - return err - } - - if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { - responseArray, err = decorateContainerList(responseArray, executor.operationContext.resourceControls) - } else { - responseArray, err = filterContainerList(responseArray, executor.operationContext) - } - if err != nil { - return err - } - - if executor.labelBlackList != nil { - responseArray, err = filterContainersWithBlackListedLabels(responseArray, executor.labelBlackList) - if err != nil { - return err - } - } - - return rewriteResponse(response, responseArray, http.StatusOK) -} - -// containerInspectOperation extracts the response as a JSON object, verify that the user -// has access to the container based on resource control (check are done based on the containerID and optional Swarm service ID) -// and either rewrite an access denied response or a decorated container. -func containerInspectOperation(response *http.Response, executor *operationExecutor) error { - // ContainerInspect response is a JSON object - // https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect - responseObject, err := getResponseAsJSONOBject(response) - if err != nil { - return err - } - - if responseObject[containerIdentifier] == nil { - return ErrDockerContainerIdentifierNotFound - } - - containerID := responseObject[containerIdentifier].(string) - responseObject, access := applyResourceAccessControl(responseObject, containerID, executor.operationContext) - if access { - return rewriteResponse(response, responseObject, http.StatusOK) - } - - containerLabels := extractContainerLabelsFromContainerInspectObject(responseObject) - responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForServiceIdentifier, executor.operationContext) - if access { - return rewriteResponse(response, responseObject, http.StatusOK) - } - - responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForSwarmStackIdentifier, executor.operationContext) - if access { - return rewriteResponse(response, responseObject, http.StatusOK) - } - - responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForComposeStackIdentifier, executor.operationContext) - if access { - return rewriteResponse(response, responseObject, http.StatusOK) - } - - return rewriteAccessDeniedResponse(response) -} - -// extractContainerLabelsFromContainerInspectObject retrieve the Labels of the container if present. -// Container schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect -func extractContainerLabelsFromContainerInspectObject(responseObject map[string]interface{}) map[string]interface{} { - // Labels are stored under Config.Labels - containerConfigObject := extractJSONField(responseObject, "Config") - if containerConfigObject != nil { - containerLabelsObject := extractJSONField(containerConfigObject, "Labels") - return containerLabelsObject - } - return nil -} - -// extractContainerLabelsFromContainerListObject retrieve the Labels of the container if present. -// Container schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList -func extractContainerLabelsFromContainerListObject(responseObject map[string]interface{}) map[string]interface{} { - // Labels are stored under Labels - containerLabelsObject := extractJSONField(responseObject, "Labels") - return containerLabelsObject -} - -// decorateContainerList loops through all containers and decorates any container with an existing resource control. -// Resource controls checks are based on: resource identifier, service identifier (from label), stack identifier (from label). -// Container object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList -func decorateContainerList(containerData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) { - decoratedContainerData := make([]interface{}, 0) - - for _, container := range containerData { - - containerObject := container.(map[string]interface{}) - if containerObject[containerIdentifier] == nil { - return nil, ErrDockerContainerIdentifierNotFound - } - - containerID := containerObject[containerIdentifier].(string) - containerObject = decorateResourceWithAccessControl(containerObject, containerID, resourceControls) - - containerLabels := extractContainerLabelsFromContainerListObject(containerObject) - containerObject = decorateResourceWithAccessControlFromLabel(containerLabels, containerObject, containerLabelForServiceIdentifier, resourceControls) - containerObject = decorateResourceWithAccessControlFromLabel(containerLabels, containerObject, containerLabelForSwarmStackIdentifier, resourceControls) - containerObject = decorateResourceWithAccessControlFromLabel(containerLabels, containerObject, containerLabelForComposeStackIdentifier, resourceControls) - - decoratedContainerData = append(decoratedContainerData, containerObject) - } - - return decoratedContainerData, nil -} - -// filterContainerList loops through all containers and filters public containers (no associated resource control) -// as well as authorized containers (access granted to the user based on existing resource control). -// Authorized containers are decorated during the process. -// Resource controls checks are based on: resource identifier, service identifier (from label), stack identifier (from label). -// Container object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList -func filterContainerList(containerData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { - filteredContainerData := make([]interface{}, 0) - - for _, container := range containerData { - containerObject := container.(map[string]interface{}) - if containerObject[containerIdentifier] == nil { - return nil, ErrDockerContainerIdentifierNotFound - } - - containerID := containerObject[containerIdentifier].(string) - containerObject, access := applyResourceAccessControl(containerObject, containerID, context) - if !access { - containerLabels := extractContainerLabelsFromContainerListObject(containerObject) - containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForComposeStackIdentifier, context) - if !access { - containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForServiceIdentifier, context) - if !access { - containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForSwarmStackIdentifier, context) - } - } - } - - if access { - filteredContainerData = append(filteredContainerData, containerObject) - } - } - - return filteredContainerData, nil -} - -// filterContainersWithLabels loops through a list of containers, and filters containers that do not contains -// any labels in the labels black list. -func filterContainersWithBlackListedLabels(containerData []interface{}, labelBlackList []portainer.Pair) ([]interface{}, error) { - filteredContainerData := make([]interface{}, 0) - - for _, container := range containerData { - containerObject := container.(map[string]interface{}) - - containerLabels := extractContainerLabelsFromContainerListObject(containerObject) - if containerLabels != nil { - if !containerHasBlackListedLabel(containerLabels, labelBlackList) { - filteredContainerData = append(filteredContainerData, containerObject) - } - } else { - filteredContainerData = append(filteredContainerData, containerObject) - } - } - - return filteredContainerData, nil -} - -func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelBlackList []portainer.Pair) bool { - for key, value := range containerLabels { - labelName := key - labelValue := value.(string) - - for _, blackListedLabel := range labelBlackList { - if blackListedLabel.Name == labelName && blackListedLabel.Value == labelValue { - return true - } - } - } - - return false -} diff --git a/api/http/proxy/docker_transport.go b/api/http/proxy/docker_transport.go deleted file mode 100644 index 81515ce5a..000000000 --- a/api/http/proxy/docker_transport.go +++ /dev/null @@ -1,600 +0,0 @@ -package proxy - -import ( - "encoding/base64" - "encoding/json" - "net/http" - "path" - "regexp" - "strings" - - "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/http/security" -) - -var apiVersionRe = regexp.MustCompile(`(/v[0-9]\.[0-9]*)?`) - -type ( - proxyTransport struct { - dockerTransport *http.Transport - enableSignature bool - ResourceControlService portainer.ResourceControlService - UserService portainer.UserService - TeamMembershipService portainer.TeamMembershipService - RegistryService portainer.RegistryService - DockerHubService portainer.DockerHubService - SettingsService portainer.SettingsService - SignatureService portainer.DigitalSignatureService - ReverseTunnelService portainer.ReverseTunnelService - ExtensionService portainer.ExtensionService - endpointIdentifier portainer.EndpointID - endpointType portainer.EndpointType - } - restrictedDockerOperationContext struct { - isAdmin bool - endpointResourceAccess bool - userID portainer.UserID - userTeamIDs []portainer.TeamID - resourceControls []portainer.ResourceControl - } - registryAccessContext struct { - isAdmin bool - userID portainer.UserID - teamMemberships []portainer.TeamMembership - registries []portainer.Registry - dockerHub *portainer.DockerHub - } - registryAuthenticationHeader struct { - Username string `json:"username"` - Password string `json:"password"` - Serveraddress string `json:"serveraddress"` - } - operationExecutor struct { - operationContext *restrictedDockerOperationContext - labelBlackList []portainer.Pair - } - restrictedOperationRequest func(*http.Response, *operationExecutor) error - operationRequest func(*http.Request) error -) - -func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error) { - return p.proxyDockerRequest(request) -} - -func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Response, error) { - response, err := p.dockerTransport.RoundTrip(request) - - if p.endpointType != portainer.EdgeAgentEnvironment { - return response, err - } - - if err == nil { - p.ReverseTunnelService.SetTunnelStatusToActive(p.endpointIdentifier) - } else { - p.ReverseTunnelService.SetTunnelStatusToIdle(p.endpointIdentifier) - } - - return response, err -} - -func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) { - path := apiVersionRe.ReplaceAllString(request.URL.Path, "") - request.URL.Path = path - - if p.enableSignature { - signature, err := p.SignatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) - if err != nil { - return nil, err - } - - request.Header.Set(portainer.PortainerAgentPublicKeyHeader, p.SignatureService.EncodedPublicKey()) - request.Header.Set(portainer.PortainerAgentSignatureHeader, signature) - } - - switch { - case strings.HasPrefix(path, "/configs"): - return p.proxyConfigRequest(request) - case strings.HasPrefix(path, "/containers"): - return p.proxyContainerRequest(request) - case strings.HasPrefix(path, "/services"): - return p.proxyServiceRequest(request) - case strings.HasPrefix(path, "/volumes"): - return p.proxyVolumeRequest(request) - case strings.HasPrefix(path, "/networks"): - return p.proxyNetworkRequest(request) - case strings.HasPrefix(path, "/secrets"): - return p.proxySecretRequest(request) - case strings.HasPrefix(path, "/swarm"): - return p.proxySwarmRequest(request) - case strings.HasPrefix(path, "/nodes"): - return p.proxyNodeRequest(request) - case strings.HasPrefix(path, "/tasks"): - return p.proxyTaskRequest(request) - case strings.HasPrefix(path, "/build"): - return p.proxyBuildRequest(request) - case strings.HasPrefix(path, "/images"): - return p.proxyImageRequest(request) - case strings.HasPrefix(path, "/v2"): - return p.proxyAgentRequest(request) - default: - return p.executeDockerRequest(request) - } -} - -func (p *proxyTransport) proxyAgentRequest(r *http.Request) (*http.Response, error) { - requestPath := strings.TrimPrefix(r.URL.Path, "/v2") - - switch { - case strings.HasPrefix(requestPath, "/browse"): - volumeIDParameter, found := r.URL.Query()["volumeID"] - if !found || len(volumeIDParameter) < 1 { - return p.administratorOperation(r) - } - - return p.restrictedVolumeBrowserOperation(r, volumeIDParameter[0]) - } - - return p.executeDockerRequest(r) -} - -func (p *proxyTransport) proxyConfigRequest(request *http.Request) (*http.Response, error) { - switch requestPath := request.URL.Path; requestPath { - case "/configs/create": - return p.executeDockerRequest(request) - - case "/configs": - return p.rewriteOperation(request, configListOperation) - - default: - // assume /configs/{id} - if request.Method == http.MethodGet { - return p.rewriteOperation(request, configInspectOperation) - } - configID := path.Base(requestPath) - return p.restrictedOperation(request, configID) - } -} - -func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) { - switch requestPath := request.URL.Path; requestPath { - case "/containers/create": - return p.executeDockerRequest(request) - - case "/containers/prune": - return p.administratorOperation(request) - - case "/containers/json": - return p.rewriteOperationWithLabelFiltering(request, containerListOperation) - - default: - // This section assumes /containers/** - if match, _ := path.Match("/containers/*/*", requestPath); match { - // Handle /containers/{id}/{action} requests - containerID := path.Base(path.Dir(requestPath)) - action := path.Base(requestPath) - - if action == "json" { - return p.rewriteOperation(request, containerInspectOperation) - } - return p.restrictedOperation(request, containerID) - } else if match, _ := path.Match("/containers/*", requestPath); match { - // Handle /containers/{id} requests - containerID := path.Base(requestPath) - return p.restrictedOperation(request, containerID) - } - return p.executeDockerRequest(request) - } -} - -func (p *proxyTransport) proxyServiceRequest(request *http.Request) (*http.Response, error) { - switch requestPath := request.URL.Path; requestPath { - case "/services/create": - return p.replaceRegistryAuthenticationHeader(request) - - case "/services": - return p.rewriteOperation(request, serviceListOperation) - - default: - // This section assumes /services/** - if match, _ := path.Match("/services/*/*", requestPath); match { - // Handle /services/{id}/{action} requests - serviceID := path.Base(path.Dir(requestPath)) - return p.restrictedOperation(request, serviceID) - } else if match, _ := path.Match("/services/*", requestPath); match { - // Handle /services/{id} requests - serviceID := path.Base(requestPath) - - if request.Method == http.MethodGet { - return p.rewriteOperation(request, serviceInspectOperation) - } - return p.restrictedOperation(request, serviceID) - } - return p.executeDockerRequest(request) - } -} - -func (p *proxyTransport) proxyVolumeRequest(request *http.Request) (*http.Response, error) { - switch requestPath := request.URL.Path; requestPath { - case "/volumes/create": - return p.executeDockerRequest(request) - - case "/volumes/prune": - return p.administratorOperation(request) - - case "/volumes": - return p.rewriteOperation(request, volumeListOperation) - - default: - // assume /volumes/{name} - if request.Method == http.MethodGet { - return p.rewriteOperation(request, volumeInspectOperation) - } - volumeID := path.Base(requestPath) - return p.restrictedOperation(request, volumeID) - } -} - -func (p *proxyTransport) proxyNetworkRequest(request *http.Request) (*http.Response, error) { - switch requestPath := request.URL.Path; requestPath { - case "/networks/create": - return p.executeDockerRequest(request) - - case "/networks": - return p.rewriteOperation(request, networkListOperation) - - default: - // assume /networks/{id} - if request.Method == http.MethodGet { - return p.rewriteOperation(request, networkInspectOperation) - } - networkID := path.Base(requestPath) - return p.restrictedOperation(request, networkID) - } -} - -func (p *proxyTransport) proxySecretRequest(request *http.Request) (*http.Response, error) { - switch requestPath := request.URL.Path; requestPath { - case "/secrets/create": - return p.executeDockerRequest(request) - - case "/secrets": - return p.rewriteOperation(request, secretListOperation) - - default: - // assume /secrets/{id} - if request.Method == http.MethodGet { - return p.rewriteOperation(request, secretInspectOperation) - } - secretID := path.Base(requestPath) - return p.restrictedOperation(request, secretID) - } -} - -func (p *proxyTransport) proxyNodeRequest(request *http.Request) (*http.Response, error) { - requestPath := request.URL.Path - - // assume /nodes/{id} - if path.Base(requestPath) != "nodes" { - return p.administratorOperation(request) - } - - return p.executeDockerRequest(request) -} - -func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Response, error) { - switch requestPath := request.URL.Path; requestPath { - case "/swarm": - return p.rewriteOperation(request, swarmInspectOperation) - default: - // assume /swarm/{action} - return p.administratorOperation(request) - } -} - -func (p *proxyTransport) proxyTaskRequest(request *http.Request) (*http.Response, error) { - switch requestPath := request.URL.Path; requestPath { - case "/tasks": - return p.rewriteOperation(request, taskListOperation) - default: - // assume /tasks/{id} - return p.executeDockerRequest(request) - } -} - -func (p *proxyTransport) proxyBuildRequest(request *http.Request) (*http.Response, error) { - return p.interceptAndRewriteRequest(request, buildOperation) -} - -func (p *proxyTransport) proxyImageRequest(request *http.Request) (*http.Response, error) { - switch requestPath := request.URL.Path; requestPath { - case "/images/create": - return p.replaceRegistryAuthenticationHeader(request) - default: - if path.Base(requestPath) == "push" && request.Method == http.MethodPost { - return p.replaceRegistryAuthenticationHeader(request) - } - return p.executeDockerRequest(request) - } -} - -func (p *proxyTransport) replaceRegistryAuthenticationHeader(request *http.Request) (*http.Response, error) { - accessContext, err := p.createRegistryAccessContext(request) - if err != nil { - return nil, err - } - - originalHeader := request.Header.Get("X-Registry-Auth") - - if originalHeader != "" { - - decodedHeaderData, err := base64.StdEncoding.DecodeString(originalHeader) - if err != nil { - return nil, err - } - - var originalHeaderData registryAuthenticationHeader - err = json.Unmarshal(decodedHeaderData, &originalHeaderData) - if err != nil { - return nil, err - } - - authenticationHeader := createRegistryAuthenticationHeader(originalHeaderData.Serveraddress, accessContext) - - headerData, err := json.Marshal(authenticationHeader) - if err != nil { - return nil, err - } - - header := base64.StdEncoding.EncodeToString(headerData) - - request.Header.Set("X-Registry-Auth", header) - } - - return p.executeDockerRequest(request) -} - -// restrictedOperation ensures that the current user has the required authorizations -// before executing the original request. -func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) { - var err error - tokenData, err := security.RetrieveTokenData(request) - if err != nil { - return nil, err - } - - if tokenData.Role != portainer.AdministratorRole { - - teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID) - if err != nil { - return nil, err - } - - userTeamIDs := make([]portainer.TeamID, 0) - for _, membership := range teamMemberships { - userTeamIDs = append(userTeamIDs, membership.TeamID) - } - - resourceControls, err := p.ResourceControlService.ResourceControls() - if err != nil { - return nil, err - } - - resourceControl := getResourceControlByResourceID(resourceID, resourceControls) - if resourceControl != nil && !canUserAccessResource(tokenData.ID, userTeamIDs, resourceControl) { - return writeAccessDeniedResponse() - } - } - - return p.executeDockerRequest(request) -} - -// restrictedVolumeBrowserOperation is similar to restrictedOperation but adds an extra check on a specific setting -func (p *proxyTransport) restrictedVolumeBrowserOperation(request *http.Request, resourceID string) (*http.Response, error) { - var err error - tokenData, err := security.RetrieveTokenData(request) - if err != nil { - return nil, err - } - - if tokenData.Role != portainer.AdministratorRole { - settings, err := p.SettingsService.Settings() - if err != nil { - return nil, err - } - - _, err = p.ExtensionService.Extension(portainer.RBACExtension) - if err == portainer.ErrObjectNotFound && !settings.AllowVolumeBrowserForRegularUsers { - return writeAccessDeniedResponse() - } else if err != nil && err != portainer.ErrObjectNotFound { - return nil, err - } - - user, err := p.UserService.User(tokenData.ID) - if err != nil { - return nil, err - } - - endpointResourceAccess := false - _, ok := user.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess] - if ok { - endpointResourceAccess = true - } - - teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID) - if err != nil { - return nil, err - } - - userTeamIDs := make([]portainer.TeamID, 0) - for _, membership := range teamMemberships { - userTeamIDs = append(userTeamIDs, membership.TeamID) - } - - resourceControls, err := p.ResourceControlService.ResourceControls() - if err != nil { - return nil, err - } - - resourceControl := getResourceControlByResourceID(resourceID, resourceControls) - if !endpointResourceAccess && (resourceControl == nil || !canUserAccessResource(tokenData.ID, userTeamIDs, resourceControl)) { - return writeAccessDeniedResponse() - } - } - - return p.executeDockerRequest(request) -} - -// rewriteOperationWithLabelFiltering will create a new operation context with data that will be used -// to decorate the original request's response as well as retrieve all the black listed labels -// to filter the resources. -func (p *proxyTransport) rewriteOperationWithLabelFiltering(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) { - operationContext, err := p.createOperationContext(request) - if err != nil { - return nil, err - } - - settings, err := p.SettingsService.Settings() - if err != nil { - return nil, err - } - - executor := &operationExecutor{ - operationContext: operationContext, - labelBlackList: settings.BlackListedLabels, - } - - return p.executeRequestAndRewriteResponse(request, operation, executor) -} - -// rewriteOperation will create a new operation context with data that will be used -// to decorate the original request's response. -func (p *proxyTransport) rewriteOperation(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) { - operationContext, err := p.createOperationContext(request) - if err != nil { - return nil, err - } - - executor := &operationExecutor{ - operationContext: operationContext, - } - - return p.executeRequestAndRewriteResponse(request, operation, executor) -} - -func (p *proxyTransport) interceptAndRewriteRequest(request *http.Request, operation operationRequest) (*http.Response, error) { - err := operation(request) - if err != nil { - return nil, err - } - - return p.executeDockerRequest(request) -} - -func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) { - response, err := p.executeDockerRequest(request) - if err != nil { - return response, err - } - - err = operation(response, executor) - return response, err -} - -// administratorOperation ensures that the user has administrator privileges -// before executing the original request. -func (p *proxyTransport) administratorOperation(request *http.Request) (*http.Response, error) { - tokenData, err := security.RetrieveTokenData(request) - if err != nil { - return nil, err - } - - if tokenData.Role != portainer.AdministratorRole { - return writeAccessDeniedResponse() - } - - return p.executeDockerRequest(request) -} - -func (p *proxyTransport) createRegistryAccessContext(request *http.Request) (*registryAccessContext, error) { - tokenData, err := security.RetrieveTokenData(request) - if err != nil { - return nil, err - } - - accessContext := ®istryAccessContext{ - isAdmin: true, - userID: tokenData.ID, - } - - hub, err := p.DockerHubService.DockerHub() - if err != nil { - return nil, err - } - accessContext.dockerHub = hub - - registries, err := p.RegistryService.Registries() - if err != nil { - return nil, err - } - accessContext.registries = registries - - if tokenData.Role != portainer.AdministratorRole { - accessContext.isAdmin = false - - teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID) - if err != nil { - return nil, err - } - - accessContext.teamMemberships = teamMemberships - } - - return accessContext, nil -} - -func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedDockerOperationContext, error) { - var err error - tokenData, err := security.RetrieveTokenData(request) - if err != nil { - return nil, err - } - - resourceControls, err := p.ResourceControlService.ResourceControls() - if err != nil { - return nil, err - } - - operationContext := &restrictedDockerOperationContext{ - isAdmin: true, - userID: tokenData.ID, - resourceControls: resourceControls, - endpointResourceAccess: false, - } - - if tokenData.Role != portainer.AdministratorRole { - operationContext.isAdmin = false - - user, err := p.UserService.User(operationContext.userID) - if err != nil { - return nil, err - } - - _, ok := user.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess] - if ok { - operationContext.endpointResourceAccess = true - } - - teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID) - if err != nil { - return nil, err - } - - userTeamIDs := make([]portainer.TeamID, 0) - for _, membership := range teamMemberships { - userTeamIDs = append(userTeamIDs, membership.TeamID) - } - operationContext.userTeamIDs = userTeamIDs - } - - return operationContext, nil -} diff --git a/api/http/proxy/factory.go b/api/http/proxy/factory.go deleted file mode 100644 index b9416707d..000000000 --- a/api/http/proxy/factory.go +++ /dev/null @@ -1,101 +0,0 @@ -package proxy - -import ( - "net" - "net/http" - "net/http/httputil" - "net/url" - - "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/crypto" -) - -// AzureAPIBaseURL is the URL where Azure API requests will be proxied. -const AzureAPIBaseURL = "https://management.azure.com" - -// proxyFactory is a factory to create reverse proxies to Docker endpoints -type proxyFactory struct { - ResourceControlService portainer.ResourceControlService - UserService portainer.UserService - TeamMembershipService portainer.TeamMembershipService - SettingsService portainer.SettingsService - RegistryService portainer.RegistryService - DockerHubService portainer.DockerHubService - SignatureService portainer.DigitalSignatureService - ReverseTunnelService portainer.ReverseTunnelService - ExtensionService portainer.ExtensionService -} - -func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler { - u.Scheme = "http" - return httputil.NewSingleHostReverseProxy(u) -} - -func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error) { - remoteURL, err := url.Parse(AzureAPIBaseURL) - if err != nil { - return nil, err - } - - proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) - proxy.Transport = NewAzureTransport(credentials) - - return proxy, nil -} - -func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, endpoint *portainer.Endpoint) (http.Handler, error) { - u.Scheme = "https" - - proxy := factory.createDockerReverseProxy(u, endpoint) - config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify) - if err != nil { - return nil, err - } - - proxy.Transport.(*proxyTransport).dockerTransport.TLSClientConfig = config - return proxy, nil -} - -func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, endpoint *portainer.Endpoint) http.Handler { - u.Scheme = "http" - return factory.createDockerReverseProxy(u, endpoint) -} - -func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, endpoint *portainer.Endpoint) *httputil.ReverseProxy { - proxy := newSingleHostReverseProxyWithHostHeader(u) - - enableSignature := false - if endpoint.Type == portainer.AgentOnDockerEnvironment { - enableSignature = true - } - - transport := &proxyTransport{ - enableSignature: enableSignature, - ResourceControlService: factory.ResourceControlService, - UserService: factory.UserService, - TeamMembershipService: factory.TeamMembershipService, - SettingsService: factory.SettingsService, - RegistryService: factory.RegistryService, - DockerHubService: factory.DockerHubService, - ReverseTunnelService: factory.ReverseTunnelService, - ExtensionService: factory.ExtensionService, - dockerTransport: &http.Transport{}, - endpointIdentifier: endpoint.ID, - endpointType: endpoint.Type, - } - - if enableSignature { - transport.SignatureService = factory.SignatureService - } - - proxy.Transport = transport - return proxy -} - -func newSocketTransport(socketPath string) *http.Transport { - return &http.Transport{ - Dial: func(proto, addr string) (conn net.Conn, err error) { - return net.Dial("unix", socketPath) - }, - } -} diff --git a/api/http/proxy/factory/azure.go b/api/http/proxy/factory/azure.go new file mode 100644 index 000000000..27b8a26f8 --- /dev/null +++ b/api/http/proxy/factory/azure.go @@ -0,0 +1,20 @@ +package factory + +import ( + "net/http" + "net/url" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/azure" +) + +func newAzureProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + remoteURL, err := url.Parse(azureAPIBaseURL) + if err != nil { + return nil, err + } + + proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) + proxy.Transport = azure.NewTransport(&endpoint.AzureCredentials) + return proxy, nil +} diff --git a/api/http/proxy/azure_transport.go b/api/http/proxy/factory/azure/transport.go similarity index 71% rename from api/http/proxy/azure_transport.go rename to api/http/proxy/factory/azure/transport.go index f0752b925..8ea0e0760 100644 --- a/api/http/proxy/azure_transport.go +++ b/api/http/proxy/factory/azure/transport.go @@ -1,4 +1,4 @@ -package proxy +package azure import ( "net/http" @@ -16,9 +16,7 @@ type ( expirationTime time.Time } - // AzureTransport represents a transport used when executing HTTP requests - // against the Azure API. - AzureTransport struct { + Transport struct { credentials *portainer.AzureCredentials client *client.HTTPClient token *azureAPIToken @@ -26,15 +24,27 @@ type ( } ) -// NewAzureTransport returns a pointer to an AzureTransport instance. -func NewAzureTransport(credentials *portainer.AzureCredentials) *AzureTransport { - return &AzureTransport{ +// NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport +// interface for proxying requests to the Azure API. +func NewTransport(credentials *portainer.AzureCredentials) *Transport { + return &Transport{ credentials: credentials, client: client.NewHTTPClient(), } } -func (transport *AzureTransport) authenticate() error { +// RoundTrip is the implementation of the Transport interface. +func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) { + err := transport.retrieveAuthenticationToken() + if err != nil { + return nil, err + } + + request.Header.Set("Authorization", "Bearer "+transport.token.value) + return http.DefaultTransport.RoundTrip(request) +} + +func (transport *Transport) authenticate() error { token, err := transport.client.ExecuteAzureAuthenticationRequest(transport.credentials) if err != nil { return err @@ -53,7 +63,7 @@ func (transport *AzureTransport) authenticate() error { return nil } -func (transport *AzureTransport) retrieveAuthenticationToken() error { +func (transport *Transport) retrieveAuthenticationToken() error { transport.mutex.Lock() defer transport.mutex.Unlock() @@ -68,14 +78,3 @@ func (transport *AzureTransport) retrieveAuthenticationToken() error { return nil } - -// RoundTrip is the implementation of the Transport interface. -func (transport *AzureTransport) RoundTrip(request *http.Request) (*http.Response, error) { - err := transport.retrieveAuthenticationToken() - if err != nil { - return nil, err - } - - request.Header.Set("Authorization", "Bearer "+transport.token.value) - return http.DefaultTransport.RoundTrip(request) -} diff --git a/api/http/proxy/factory/docker.go b/api/http/proxy/factory/docker.go new file mode 100644 index 000000000..365abb2fb --- /dev/null +++ b/api/http/proxy/factory/docker.go @@ -0,0 +1,118 @@ +package factory + +import ( + "fmt" + "io" + "log" + "net/http" + "net/url" + "strings" + + httperror "github.com/portainer/libhttp/error" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" + "github.com/portainer/portainer/api/http/proxy/factory/docker" +) + +func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") { + return factory.newDockerLocalProxy(endpoint) + } + + return factory.newDockerHTTPProxy(endpoint) +} + +func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + endpointURL, err := url.Parse(endpoint.URL) + if err != nil { + return nil, err + } + + return factory.newOSBasedLocalProxy(endpointURL.Path, endpoint) +} + +func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + if endpoint.Type == portainer.EdgeAgentEnvironment { + tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID) + endpoint.URL = fmt.Sprintf("http://localhost:%d", tunnel.Port) + } + + endpointURL, err := url.Parse(endpoint.URL) + if err != nil { + return nil, err + } + + endpointURL.Scheme = "http" + httpTransport := &http.Transport{} + + if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify { + config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify) + if err != nil { + return nil, err + } + + httpTransport.TLSClientConfig = config + endpointURL.Scheme = "https" + } + + transportParameters := &docker.TransportParameters{ + Endpoint: endpoint, + ResourceControlService: factory.resourceControlService, + UserService: factory.userService, + TeamService: factory.teamService, + TeamMembershipService: factory.teamMembershipService, + RegistryService: factory.registryService, + DockerHubService: factory.dockerHubService, + SettingsService: factory.settingsService, + ReverseTunnelService: factory.reverseTunnelService, + ExtensionService: factory.extensionService, + SignatureService: factory.signatureService, + DockerClientFactory: factory.dockerClientFactory, + } + + dockerTransport, err := docker.NewTransport(transportParameters, httpTransport) + if err != nil { + return nil, err + } + + proxy := newSingleHostReverseProxyWithHostHeader(endpointURL) + proxy.Transport = dockerTransport + return proxy, nil +} + +type dockerLocalProxy struct { + transport *docker.Transport +} + +// ServeHTTP is the http.Handler interface implementation +// for a local (Unix socket or Windows named pipe) Docker proxy. +func (proxy *dockerLocalProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Force URL/domain to http/unixsocket to be able to + // use http.transport RoundTrip to do the requests via the socket + r.URL.Scheme = "http" + r.URL.Host = "unixsocket" + + res, err := proxy.transport.ProxyDockerRequest(r) + if err != nil { + code := http.StatusInternalServerError + if res != nil && res.StatusCode != 0 { + code = res.StatusCode + } + + httperror.WriteError(w, code, "Unable to proxy the request via the Docker socket", err) + return + } + defer res.Body.Close() + + for k, vv := range res.Header { + for _, v := range vv { + w.Header().Add(k, v) + } + } + + w.WriteHeader(res.StatusCode) + + if _, err := io.Copy(w, res.Body); err != nil { + log.Printf("proxy error: %s\n", err) + } +} diff --git a/api/http/proxy/factory/docker/access_control.go b/api/http/proxy/factory/docker/access_control.go new file mode 100644 index 000000000..83050f79e --- /dev/null +++ b/api/http/proxy/factory/docker/access_control.go @@ -0,0 +1,306 @@ +package docker + +import ( + "log" + "net/http" + "strings" + + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" + + "github.com/portainer/portainer/api" +) + +const ( + resourceLabelForPortainerTeamResourceControl = "io.portainer.accesscontrol.teams" + resourceLabelForPortainerUserResourceControl = "io.portainer.accesscontrol.users" + resourceLabelForPortainerPublicResourceControl = "io.portainer.accesscontrol.public" + resourceLabelForDockerSwarmStackName = "com.docker.stack.namespace" + resourceLabelForDockerServiceID = "com.docker.swarm.service.id" + resourceLabelForDockerComposeStackName = "com.docker.compose.project" +) + +type ( + resourceLabelsObjectSelector func(map[string]interface{}) map[string]interface{} + resourceOperationParameters struct { + resourceIdentifierAttribute string + resourceType portainer.ResourceControlType + labelsObjectSelector resourceLabelsObjectSelector + } +) + +func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject map[string]interface{}, resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) { + if labelsObject[resourceLabelForPortainerPublicResourceControl] != nil { + resourceControl := portainer.NewPublicResourceControl(resourceID, resourceType) + + err := transport.resourceControlService.CreateResourceControl(resourceControl) + if err != nil { + return nil, err + } + + return resourceControl, nil + } + + teamNames := make([]string, 0) + userNames := make([]string, 0) + if labelsObject[resourceLabelForPortainerTeamResourceControl] != nil { + concatenatedTeamNames := labelsObject[resourceLabelForPortainerTeamResourceControl].(string) + teamNames = strings.Split(concatenatedTeamNames, ",") + } + + if labelsObject[resourceLabelForPortainerUserResourceControl] != nil { + concatenatedUserNames := labelsObject[resourceLabelForPortainerUserResourceControl].(string) + userNames = strings.Split(concatenatedUserNames, ",") + } + + if len(teamNames) > 0 || len(userNames) > 0 { + teamIDs := make([]portainer.TeamID, 0) + userIDs := make([]portainer.UserID, 0) + + for _, name := range teamNames { + team, err := transport.teamService.TeamByName(name) + if err != nil { + log.Printf("[WARN] [http,proxy,docker] [message: unknown team name in access control label, ignoring access control rule for this team] [name: %s] [resource_id: %s]", name, resourceID) + continue + } + + teamIDs = append(teamIDs, team.ID) + } + + for _, name := range userNames { + user, err := transport.userService.UserByUsername(name) + if err != nil { + log.Printf("[WARN] [http,proxy,docker] [message: unknown user name in access control label, ignoring access control rule for this user] [name: %s] [resource_id: %s]", name, resourceID) + continue + } + + userIDs = append(userIDs, user.ID) + } + + resourceControl := portainer.NewRestrictedResourceControl(resourceID, resourceType, userIDs, teamIDs) + + err := transport.resourceControlService.CreateResourceControl(resourceControl) + if err != nil { + return nil, err + } + + return resourceControl, nil + } + + return nil, nil +} + +func (transport *Transport) createPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) (*portainer.ResourceControl, error) { + resourceControl := portainer.NewPrivateResourceControl(resourceIdentifier, resourceType, userID) + + err := transport.resourceControlService.CreateResourceControl(resourceControl) + if err != nil { + log.Printf("[ERROR] [http,proxy,docker,transport] [message: unable to persist resource control] [resource: %s] [err: %s]", resourceIdentifier, err) + return nil, err + } + + return resourceControl, nil +} + +func (transport *Transport) getInheritedResourceControlFromServiceOrStack(resourceIdentifier, nodeName string, resourceType portainer.ResourceControlType, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { + client := transport.dockerClient + + if nodeName != "" { + dockerClient, err := transport.dockerClientFactory.CreateClient(transport.endpoint, nodeName) + if err != nil { + return nil, err + } + defer dockerClient.Close() + + client = dockerClient + } + + switch resourceType { + case portainer.ContainerResourceControl: + return getInheritedResourceControlFromContainerLabels(client, resourceIdentifier, resourceControls) + case portainer.NetworkResourceControl: + return getInheritedResourceControlFromNetworkLabels(client, resourceIdentifier, resourceControls) + case portainer.VolumeResourceControl: + return getInheritedResourceControlFromVolumeLabels(client, resourceIdentifier, resourceControls) + case portainer.ServiceResourceControl: + return getInheritedResourceControlFromServiceLabels(client, resourceIdentifier, resourceControls) + case portainer.ConfigResourceControl: + return getInheritedResourceControlFromConfigLabels(client, resourceIdentifier, resourceControls) + case portainer.SecretResourceControl: + return getInheritedResourceControlFromSecretLabels(client, resourceIdentifier, resourceControls) + } + + return nil, nil +} + +func (transport *Transport) applyAccessControlOnResource(parameters *resourceOperationParameters, responseObject map[string]interface{}, response *http.Response, executor *operationExecutor) error { + if responseObject[parameters.resourceIdentifierAttribute] == nil { + log.Printf("[WARN] [message: unable to find resource identifier property in resource object] [identifier_attribute: %s]", parameters.resourceIdentifierAttribute) + return nil + } + + if parameters.resourceType == portainer.NetworkResourceControl { + systemResourceControl := findSystemNetworkResourceControl(responseObject) + if systemResourceControl != nil { + responseObject = decorateObject(responseObject, systemResourceControl) + return responseutils.RewriteResponse(response, responseObject, http.StatusOK) + } + } + + resourceIdentifier := responseObject[parameters.resourceIdentifierAttribute].(string) + resourceLabelsObject := parameters.labelsObjectSelector(responseObject) + + resourceControl, err := transport.findResourceControl(resourceIdentifier, parameters.resourceType, resourceLabelsObject, executor.operationContext.resourceControls) + if err != nil { + return err + } + + if resourceControl == nil && (executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess) { + return responseutils.RewriteResponse(response, responseObject, http.StatusOK) + } + + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess || portainer.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl) { + responseObject = decorateObject(responseObject, resourceControl) + return responseutils.RewriteResponse(response, responseObject, http.StatusOK) + } + + return responseutils.RewriteAccessDeniedResponse(response) +} + +func (transport *Transport) applyAccessControlOnResourceList(parameters *resourceOperationParameters, resourceData []interface{}, executor *operationExecutor) ([]interface{}, error) { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { + return transport.decorateResourceList(parameters, resourceData, executor.operationContext.resourceControls) + } + + return transport.filterResourceList(parameters, resourceData, executor.operationContext) +} + +func (transport *Transport) decorateResourceList(parameters *resourceOperationParameters, resourceData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) { + decoratedResourceData := make([]interface{}, 0) + + for _, resource := range resourceData { + resourceObject := resource.(map[string]interface{}) + + if resourceObject[parameters.resourceIdentifierAttribute] == nil { + log.Printf("[WARN] [http,proxy,docker,decorate] [message: unable to find resource identifier property in resource list element] [identifier_attribute: %s]", parameters.resourceIdentifierAttribute) + continue + } + + if parameters.resourceType == portainer.NetworkResourceControl { + systemResourceControl := findSystemNetworkResourceControl(resourceObject) + if systemResourceControl != nil { + resourceObject = decorateObject(resourceObject, systemResourceControl) + decoratedResourceData = append(decoratedResourceData, resourceObject) + continue + } + } + + resourceIdentifier := resourceObject[parameters.resourceIdentifierAttribute].(string) + resourceLabelsObject := parameters.labelsObjectSelector(resourceObject) + + resourceControl, err := transport.findResourceControl(resourceIdentifier, parameters.resourceType, resourceLabelsObject, resourceControls) + if err != nil { + return nil, err + } + + if resourceControl != nil { + resourceObject = decorateObject(resourceObject, resourceControl) + } + + decoratedResourceData = append(decoratedResourceData, resourceObject) + } + + return decoratedResourceData, nil +} + +func (transport *Transport) filterResourceList(parameters *resourceOperationParameters, resourceData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { + filteredResourceData := make([]interface{}, 0) + + for _, resource := range resourceData { + resourceObject := resource.(map[string]interface{}) + if resourceObject[parameters.resourceIdentifierAttribute] == nil { + log.Printf("[WARN] [http,proxy,docker,filter] [message: unable to find resource identifier property in resource list element] [identifier_attribute: %s]", parameters.resourceIdentifierAttribute) + continue + } + + resourceIdentifier := resourceObject[parameters.resourceIdentifierAttribute].(string) + resourceLabelsObject := parameters.labelsObjectSelector(resourceObject) + + if parameters.resourceType == portainer.NetworkResourceControl { + systemResourceControl := findSystemNetworkResourceControl(resourceObject) + if systemResourceControl != nil { + resourceObject = decorateObject(resourceObject, systemResourceControl) + filteredResourceData = append(filteredResourceData, resourceObject) + continue + } + } + + resourceControl, err := transport.findResourceControl(resourceIdentifier, parameters.resourceType, resourceLabelsObject, context.resourceControls) + if err != nil { + return nil, err + } + + if resourceControl == nil { + if context.isAdmin || context.endpointResourceAccess { + filteredResourceData = append(filteredResourceData, resourceObject) + } + continue + } + + if context.isAdmin || context.endpointResourceAccess || portainer.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) { + resourceObject = decorateObject(resourceObject, resourceControl) + filteredResourceData = append(filteredResourceData, resourceObject) + } + } + + return filteredResourceData, nil +} + +func (transport *Transport) findResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, resourceLabelsObject map[string]interface{}, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { + resourceControl := portainer.GetResourceControlByResourceIDAndType(resourceIdentifier, resourceType, resourceControls) + if resourceControl != nil { + return resourceControl, nil + } + + if resourceLabelsObject != nil { + if resourceLabelsObject[resourceLabelForDockerServiceID] != nil { + inheritedServiceIdentifier := resourceLabelsObject[resourceLabelForDockerServiceID].(string) + resourceControl = portainer.GetResourceControlByResourceIDAndType(inheritedServiceIdentifier, portainer.ServiceResourceControl, resourceControls) + + if resourceControl != nil { + return resourceControl, nil + } + } + + if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != nil { + inheritedSwarmStackIdentifier := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string) + resourceControl = portainer.GetResourceControlByResourceIDAndType(inheritedSwarmStackIdentifier, portainer.StackResourceControl, resourceControls) + + if resourceControl != nil { + return resourceControl, nil + } + } + + if resourceLabelsObject[resourceLabelForDockerComposeStackName] != nil { + inheritedComposeStackIdentifier := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string) + resourceControl = portainer.GetResourceControlByResourceIDAndType(inheritedComposeStackIdentifier, portainer.StackResourceControl, resourceControls) + + if resourceControl != nil { + return resourceControl, nil + } + } + + return transport.newResourceControlFromPortainerLabels(resourceLabelsObject, resourceIdentifier, resourceType) + } + + return nil, nil +} + +func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} { + if object["Portainer"] == nil { + object["Portainer"] = make(map[string]interface{}) + } + + portainerMetadata := object["Portainer"].(map[string]interface{}) + portainerMetadata["ResourceControl"] = resourceControl + return object +} diff --git a/api/http/proxy/build.go b/api/http/proxy/factory/docker/build.go similarity index 99% rename from api/http/proxy/build.go rename to api/http/proxy/factory/docker/build.go index 93f0d4a68..4bec82ec0 100644 --- a/api/http/proxy/build.go +++ b/api/http/proxy/factory/docker/build.go @@ -1,4 +1,4 @@ -package proxy +package docker import ( "bytes" diff --git a/api/http/proxy/factory/docker/configs.go b/api/http/proxy/factory/docker/configs.go new file mode 100644 index 000000000..e9092dc3f --- /dev/null +++ b/api/http/proxy/factory/docker/configs.go @@ -0,0 +1,86 @@ +package docker + +import ( + "context" + "net/http" + + "github.com/docker/docker/client" + + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" +) + +const ( + configObjectIdentifier = "ID" +) + +func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, configID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { + config, _, err := dockerClient.ConfigInspectWithRaw(context.Background(), configID) + if err != nil { + return nil, err + } + + swarmStackName := config.Spec.Labels[resourceLabelForDockerSwarmStackName] + if swarmStackName != "" { + return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + } + + return nil, nil +} + +// configListOperation extracts the response as a JSON object, loop through the configs array +// decorate and/or filter the configs based on resource controls before rewriting the response. +func (transport *Transport) configListOperation(response *http.Response, executor *operationExecutor) error { + // ConfigList response is a JSON array + // https://docs.docker.com/engine/api/v1.30/#operation/ConfigList + responseArray, err := responseutils.GetResponseAsJSONArray(response) + if err != nil { + return err + } + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: configObjectIdentifier, + resourceType: portainer.ConfigResourceControl, + labelsObjectSelector: selectorConfigLabels, + } + + responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor) + if err != nil { + return err + } + + return responseutils.RewriteResponse(response, responseArray, http.StatusOK) +} + +// configInspectOperation extracts the response as a JSON object, verify that the user +// has access to the config based on resource control and either rewrite an access denied response or a decorated config. +func (transport *Transport) configInspectOperation(response *http.Response, executor *operationExecutor) error { + // ConfigInspect response is a JSON object + // https://docs.docker.com/engine/api/v1.30/#operation/ConfigInspect + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return err + } + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: configObjectIdentifier, + resourceType: portainer.ConfigResourceControl, + labelsObjectSelector: selectorConfigLabels, + } + + return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor) +} + +// selectorConfigLabels retrieve the labels object associated to the config object. +// Labels are available under the "Spec.Labels" property. +// API schema references: +// https://docs.docker.com/engine/api/v1.40/#operation/ConfigList +// https://docs.docker.com/engine/api/v1.40/#operation/ConfigInspect +func selectorConfigLabels(responseObject map[string]interface{}) map[string]interface{} { + secretSpec := responseutils.GetJSONObject(responseObject, "Spec") + if secretSpec != nil { + secretLabelsObject := responseutils.GetJSONObject(secretSpec, "Labels") + return secretLabelsObject + } + return nil +} diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go new file mode 100644 index 000000000..8952b1bf6 --- /dev/null +++ b/api/http/proxy/factory/docker/containers.go @@ -0,0 +1,146 @@ +package docker + +import ( + "context" + "net/http" + + "github.com/docker/docker/client" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" +) + +const ( + containerObjectIdentifier = "Id" +) + +func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { + container, err := dockerClient.ContainerInspect(context.Background(), containerID) + if err != nil { + return nil, err + } + + swarmStackName := container.Config.Labels[resourceLabelForDockerSwarmStackName] + if swarmStackName != "" { + return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + } + + serviceName := container.Config.Labels[resourceLabelForDockerServiceID] + if serviceName != "" { + return portainer.GetResourceControlByResourceIDAndType(serviceName, portainer.ServiceResourceControl, resourceControls), nil + } + + composeStackName := container.Config.Labels[resourceLabelForDockerComposeStackName] + if composeStackName != "" { + return portainer.GetResourceControlByResourceIDAndType(composeStackName, portainer.StackResourceControl, resourceControls), nil + } + + return nil, nil +} + +// containerListOperation extracts the response as a JSON array, loop through the containers array +// decorate and/or filter the containers based on resource controls before rewriting the response. +func (transport *Transport) containerListOperation(response *http.Response, executor *operationExecutor) error { + // ContainerList response is a JSON array + // https://docs.docker.com/engine/api/v1.28/#operation/ContainerList + responseArray, err := responseutils.GetResponseAsJSONArray(response) + if err != nil { + return err + } + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: containerObjectIdentifier, + resourceType: portainer.ContainerResourceControl, + labelsObjectSelector: selectorContainerLabelsFromContainerListOperation, + } + + responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor) + if err != nil { + return err + } + + if executor.labelBlackList != nil { + responseArray, err = filterContainersWithBlackListedLabels(responseArray, executor.labelBlackList) + if err != nil { + return err + } + } + + return responseutils.RewriteResponse(response, responseArray, http.StatusOK) +} + +// containerInspectOperation extracts the response as a JSON object, verify that the user +// has access to the container based on resource control and either rewrite an access denied response or a decorated container. +func (transport *Transport) containerInspectOperation(response *http.Response, executor *operationExecutor) error { + //ContainerInspect response is a JSON object + // https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return err + } + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: containerObjectIdentifier, + resourceType: portainer.ContainerResourceControl, + labelsObjectSelector: selectorContainerLabelsFromContainerInspectOperation, + } + + return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor) +} + +// selectorContainerLabelsFromContainerInspectOperation retrieve the labels object associated to the container object. +// This selector is specific to the containerInspect Docker operation. +// Labels are available under the "Config.Labels" property. +// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect +func selectorContainerLabelsFromContainerInspectOperation(responseObject map[string]interface{}) map[string]interface{} { + containerConfigObject := responseutils.GetJSONObject(responseObject, "Config") + if containerConfigObject != nil { + containerLabelsObject := responseutils.GetJSONObject(containerConfigObject, "Labels") + return containerLabelsObject + } + return nil +} + +// selectorContainerLabelsFromContainerListOperation retrieve the labels object associated to the container object. +// This selector is specific to the containerList Docker operation. +// Labels are available under the "Labels" property. +// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList +func selectorContainerLabelsFromContainerListOperation(responseObject map[string]interface{}) map[string]interface{} { + containerLabelsObject := responseutils.GetJSONObject(responseObject, "Labels") + return containerLabelsObject +} + +// filterContainersWithLabels loops through a list of containers, and filters containers that do not contains +// any labels in the labels black list. +func filterContainersWithBlackListedLabels(containerData []interface{}, labelBlackList []portainer.Pair) ([]interface{}, error) { + filteredContainerData := make([]interface{}, 0) + + for _, container := range containerData { + containerObject := container.(map[string]interface{}) + + containerLabels := selectorContainerLabelsFromContainerListOperation(containerObject) + if containerLabels != nil { + if !containerHasBlackListedLabel(containerLabels, labelBlackList) { + filteredContainerData = append(filteredContainerData, containerObject) + } + } else { + filteredContainerData = append(filteredContainerData, containerObject) + } + } + + return filteredContainerData, nil +} + +func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelBlackList []portainer.Pair) bool { + for key, value := range containerLabels { + labelName := key + labelValue := value.(string) + + for _, blackListedLabel := range labelBlackList { + if blackListedLabel.Name == labelName && blackListedLabel.Value == labelValue { + return true + } + } + } + + return false +} diff --git a/api/http/proxy/factory/docker/networks.go b/api/http/proxy/factory/docker/networks.go new file mode 100644 index 000000000..c50b716e2 --- /dev/null +++ b/api/http/proxy/factory/docker/networks.go @@ -0,0 +1,101 @@ +package docker + +import ( + "context" + "net/http" + + "github.com/docker/docker/api/types" + + "github.com/docker/docker/client" + + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" +) + +const ( + networkObjectIdentifier = "Id" + networkObjectName = "Name" +) + +func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, networkID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { + network, err := dockerClient.NetworkInspect(context.Background(), networkID, types.NetworkInspectOptions{}) + if err != nil { + return nil, err + } + + swarmStackName := network.Labels[resourceLabelForDockerSwarmStackName] + if swarmStackName != "" { + return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + } + + return nil, nil +} + +// networkListOperation extracts the response as a JSON object, loop through the networks array +// decorate and/or filter the networks based on resource controls before rewriting the response. +func (transport *Transport) networkListOperation(response *http.Response, executor *operationExecutor) error { + // NetworkList response is a JSON array + // https://docs.docker.com/engine/api/v1.28/#operation/NetworkList + responseArray, err := responseutils.GetResponseAsJSONArray(response) + if err != nil { + return err + } + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: networkObjectIdentifier, + resourceType: portainer.NetworkResourceControl, + labelsObjectSelector: selectorNetworkLabels, + } + + responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor) + if err != nil { + return err + } + + return responseutils.RewriteResponse(response, responseArray, http.StatusOK) +} + +// networkInspectOperation extracts the response as a JSON object, verify that the user +// has access to the network based on resource control and either rewrite an access denied response or a decorated network. +func (transport *Transport) networkInspectOperation(response *http.Response, executor *operationExecutor) error { + // NetworkInspect response is a JSON object + // https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return err + } + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: networkObjectIdentifier, + resourceType: portainer.NetworkResourceControl, + labelsObjectSelector: selectorNetworkLabels, + } + + return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor) +} + +// findSystemNetworkResourceControl will check if the network object is a system network +// and will return a system resource control if that's the case. +func findSystemNetworkResourceControl(networkObject map[string]interface{}) *portainer.ResourceControl { + if networkObject[networkObjectName] == nil { + return nil + } + + networkID := networkObject[networkObjectIdentifier].(string) + networkName := networkObject[networkObjectName].(string) + + if networkName == "bridge" || networkName == "host" || networkName == "none" { + return portainer.NewSystemResourceControl(networkID, portainer.NetworkResourceControl) + } + + return nil +} + +// selectorNetworkLabels retrieve the labels object associated to the network object. +// Labels are available under the "Labels" property. +// API schema references: +// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect +// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList +func selectorNetworkLabels(responseObject map[string]interface{}) map[string]interface{} { + return responseutils.GetJSONObject(responseObject, "Labels") +} diff --git a/api/http/proxy/registry.go b/api/http/proxy/factory/docker/registry.go similarity index 73% rename from api/http/proxy/registry.go rename to api/http/proxy/factory/docker/registry.go index 5f614f29d..c07ebae3d 100644 --- a/api/http/proxy/registry.go +++ b/api/http/proxy/factory/docker/registry.go @@ -1,10 +1,25 @@ -package proxy +package docker import ( "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" ) +type ( + registryAccessContext struct { + isAdmin bool + userID portainer.UserID + teamMemberships []portainer.TeamMembership + registries []portainer.Registry + dockerHub *portainer.DockerHub + } + registryAuthenticationHeader struct { + Username string `json:"username"` + Password string `json:"password"` + Serveraddress string `json:"serveraddress"` + } +) + func createRegistryAuthenticationHeader(serverAddress string, accessContext *registryAccessContext) *registryAuthenticationHeader { var authenticationHeader *registryAuthenticationHeader diff --git a/api/http/proxy/factory/docker/secrets.go b/api/http/proxy/factory/docker/secrets.go new file mode 100644 index 000000000..08a4045cf --- /dev/null +++ b/api/http/proxy/factory/docker/secrets.go @@ -0,0 +1,87 @@ +package docker + +import ( + "context" + "net/http" + + "github.com/docker/docker/client" + + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" + + "github.com/portainer/portainer/api" +) + +const ( + secretObjectIdentifier = "ID" +) + +func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, secretID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { + secret, _, err := dockerClient.SecretInspectWithRaw(context.Background(), secretID) + if err != nil { + return nil, err + } + + swarmStackName := secret.Spec.Labels[resourceLabelForDockerSwarmStackName] + if swarmStackName != "" { + return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + } + + return nil, nil +} + +// secretListOperation extracts the response as a JSON object, loop through the secrets array +// decorate and/or filter the secrets based on resource controls before rewriting the response. +func (transport *Transport) secretListOperation(response *http.Response, executor *operationExecutor) error { + // SecretList response is a JSON array + // https://docs.docker.com/engine/api/v1.28/#operation/SecretList + responseArray, err := responseutils.GetResponseAsJSONArray(response) + if err != nil { + return err + } + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: secretObjectIdentifier, + resourceType: portainer.SecretResourceControl, + labelsObjectSelector: selectorSecretLabels, + } + + responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor) + if err != nil { + return err + } + + return responseutils.RewriteResponse(response, responseArray, http.StatusOK) +} + +// secretInspectOperation extracts the response as a JSON object, verify that the user +// has access to the secret based on resource control and either rewrite an access denied response or a decorated secret. +func (transport *Transport) secretInspectOperation(response *http.Response, executor *operationExecutor) error { + // SecretInspect response is a JSON object + // https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return err + } + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: secretObjectIdentifier, + resourceType: portainer.SecretResourceControl, + labelsObjectSelector: selectorSecretLabels, + } + + return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor) +} + +// selectorSecretLabels retrieve the labels object associated to the secret object. +// Labels are available under the "Spec.Labels" property. +// API schema references: +// https://docs.docker.com/engine/api/v1.40/#operation/SecretList +// https://docs.docker.com/engine/api/v1.40/#operation/SecretInspect +func selectorSecretLabels(responseObject map[string]interface{}) map[string]interface{} { + secretSpec := responseutils.GetJSONObject(responseObject, "Spec") + if secretSpec != nil { + secretLabelsObject := responseutils.GetJSONObject(secretSpec, "Labels") + return secretLabelsObject + } + return nil +} diff --git a/api/http/proxy/factory/docker/services.go b/api/http/proxy/factory/docker/services.go new file mode 100644 index 000000000..eb1aa6e80 --- /dev/null +++ b/api/http/proxy/factory/docker/services.go @@ -0,0 +1,86 @@ +package docker + +import ( + "context" + "net/http" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" +) + +const ( + serviceObjectIdentifier = "ID" +) + +func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { + service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{}) + if err != nil { + return nil, err + } + + swarmStackName := service.Spec.Labels[resourceLabelForDockerSwarmStackName] + if swarmStackName != "" { + return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + } + + return nil, nil +} + +// serviceListOperation extracts the response as a JSON array, loop through the service array +// decorate and/or filter the services based on resource controls before rewriting the response. +func (transport *Transport) serviceListOperation(response *http.Response, executor *operationExecutor) error { + // ServiceList response is a JSON array + // https://docs.docker.com/engine/api/v1.28/#operation/ServiceList + responseArray, err := responseutils.GetResponseAsJSONArray(response) + if err != nil { + return err + } + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: serviceObjectIdentifier, + resourceType: portainer.ServiceResourceControl, + labelsObjectSelector: selectorServiceLabels, + } + + responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor) + if err != nil { + return err + } + + return responseutils.RewriteResponse(response, responseArray, http.StatusOK) +} + +// serviceInspectOperation extracts the response as a JSON object, verify that the user +// has access to the service based on resource control and either rewrite an access denied response or a decorated service. +func (transport *Transport) serviceInspectOperation(response *http.Response, executor *operationExecutor) error { + //ServiceInspect response is a JSON object + //https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return err + } + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: serviceObjectIdentifier, + resourceType: portainer.ServiceResourceControl, + labelsObjectSelector: selectorServiceLabels, + } + + return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor) +} + +// selectorServiceLabels retrieve the labels object associated to the service object. +// Labels are available under the "Spec.Labels" property. +// API schema references: +// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect +// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList +func selectorServiceLabels(responseObject map[string]interface{}) map[string]interface{} { + serviceSpecObject := responseutils.GetJSONObject(responseObject, "Spec") + if serviceSpecObject != nil { + return responseutils.GetJSONObject(serviceSpecObject, "Labels") + } + return nil +} diff --git a/api/http/proxy/swarm.go b/api/http/proxy/factory/docker/swarm.go similarity index 71% rename from api/http/proxy/swarm.go rename to api/http/proxy/factory/docker/swarm.go index 0048b3a38..57d27f8cc 100644 --- a/api/http/proxy/swarm.go +++ b/api/http/proxy/factory/docker/swarm.go @@ -1,7 +1,9 @@ -package proxy +package docker import ( "net/http" + + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" ) // swarmInspectOperation extracts the response as a JSON object and rewrites the response based @@ -9,7 +11,7 @@ import ( func swarmInspectOperation(response *http.Response, executor *operationExecutor) error { // SwarmInspect response is a JSON object // https://docs.docker.com/engine/api/v1.30/#operation/SwarmInspect - responseObject, err := getResponseAsJSONOBject(response) + responseObject, err := responseutils.GetResponseAsJSONOBject(response) if err != nil { return err } @@ -19,5 +21,5 @@ func swarmInspectOperation(response *http.Response, executor *operationExecutor) delete(responseObject, "TLSInfo") } - return rewriteResponse(response, responseObject, http.StatusOK) + return responseutils.RewriteResponse(response, responseObject, http.StatusOK) } diff --git a/api/http/proxy/factory/docker/tasks.go b/api/http/proxy/factory/docker/tasks.go new file mode 100644 index 000000000..ad13398fd --- /dev/null +++ b/api/http/proxy/factory/docker/tasks.go @@ -0,0 +1,50 @@ +package docker + +import ( + "net/http" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" +) + +const ( + taskServiceObjectIdentifier = "ServiceID" +) + +// taskListOperation extracts the response as a JSON array, loop through the tasks array +// and filter the containers based on resource controls before rewriting the response. +func (transport *Transport) taskListOperation(response *http.Response, executor *operationExecutor) error { + // TaskList response is a JSON array + // https://docs.docker.com/engine/api/v1.28/#operation/TaskList + responseArray, err := responseutils.GetResponseAsJSONArray(response) + if err != nil { + return err + } + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: taskServiceObjectIdentifier, + resourceType: portainer.ServiceResourceControl, + labelsObjectSelector: selectorTaskLabels, + } + + responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor) + if err != nil { + return err + } + + return responseutils.RewriteResponse(response, responseArray, http.StatusOK) +} + +// selectorServiceLabels retrieve the labels object associated to the task object. +// Labels are available under the "Spec.ContainerSpec.Labels" property. +// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList +func selectorTaskLabels(responseObject map[string]interface{}) map[string]interface{} { + taskSpecObject := responseutils.GetJSONObject(responseObject, "Spec") + if taskSpecObject != nil { + containerSpecObject := responseutils.GetJSONObject(taskSpecObject, "ContainerSpec") + if containerSpecObject != nil { + return responseutils.GetJSONObject(containerSpecObject, "Labels") + } + } + return nil +} diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go new file mode 100644 index 000000000..c4bc33897 --- /dev/null +++ b/api/http/proxy/factory/docker/transport.go @@ -0,0 +1,725 @@ +package docker + +import ( + "encoding/base64" + "encoding/json" + "errors" + "log" + "net/http" + "path" + "regexp" + "strings" + + "github.com/portainer/portainer/api/docker" + + "github.com/docker/docker/client" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" + "github.com/portainer/portainer/api/http/security" +) + +var apiVersionRe = regexp.MustCompile(`(/v[0-9]\.[0-9]*)?`) + +type ( + // Transport is a custom transport for Docker API reverse proxy. It allows + // interception of requests and rewriting of responses. + Transport struct { + HTTPTransport *http.Transport + endpoint *portainer.Endpoint + resourceControlService portainer.ResourceControlService + userService portainer.UserService + teamService portainer.TeamService + teamMembershipService portainer.TeamMembershipService + registryService portainer.RegistryService + dockerHubService portainer.DockerHubService + settingsService portainer.SettingsService + signatureService portainer.DigitalSignatureService + reverseTunnelService portainer.ReverseTunnelService + extensionService portainer.ExtensionService + dockerClient *client.Client + dockerClientFactory *docker.ClientFactory + } + + // TransportParameters is used to create a new Transport + TransportParameters struct { + Endpoint *portainer.Endpoint + ResourceControlService portainer.ResourceControlService + UserService portainer.UserService + TeamService portainer.TeamService + TeamMembershipService portainer.TeamMembershipService + RegistryService portainer.RegistryService + DockerHubService portainer.DockerHubService + SettingsService portainer.SettingsService + SignatureService portainer.DigitalSignatureService + ReverseTunnelService portainer.ReverseTunnelService + ExtensionService portainer.ExtensionService + DockerClientFactory *docker.ClientFactory + } + + restrictedDockerOperationContext struct { + isAdmin bool + endpointResourceAccess bool + userID portainer.UserID + userTeamIDs []portainer.TeamID + resourceControls []portainer.ResourceControl + } + + operationExecutor struct { + operationContext *restrictedDockerOperationContext + labelBlackList []portainer.Pair + } + restrictedOperationRequest func(*http.Response, *operationExecutor) error + operationRequest func(*http.Request) error +) + +// NewTransport returns a pointer to a new Transport instance. +func NewTransport(parameters *TransportParameters, httpTransport *http.Transport) (*Transport, error) { + dockerClient, err := parameters.DockerClientFactory.CreateClient(parameters.Endpoint, "") + if err != nil { + return nil, err + } + + transport := &Transport{ + endpoint: parameters.Endpoint, + resourceControlService: parameters.ResourceControlService, + userService: parameters.UserService, + teamService: parameters.TeamService, + teamMembershipService: parameters.TeamMembershipService, + registryService: parameters.RegistryService, + dockerHubService: parameters.DockerHubService, + settingsService: parameters.SettingsService, + signatureService: parameters.SignatureService, + reverseTunnelService: parameters.ReverseTunnelService, + extensionService: parameters.ExtensionService, + dockerClientFactory: parameters.DockerClientFactory, + HTTPTransport: httpTransport, + dockerClient: dockerClient, + } + + return transport, nil +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) { + return transport.ProxyDockerRequest(request) +} + +// ProxyDockerRequest intercepts a Docker API request and apply logic based +// on the requested operation. +func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Response, error) { + requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "") + request.URL.Path = requestPath + + if transport.endpoint.Type == portainer.AgentOnDockerEnvironment { + signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) + if err != nil { + return nil, err + } + + request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey()) + request.Header.Set(portainer.PortainerAgentSignatureHeader, signature) + } + + switch { + case strings.HasPrefix(requestPath, "/configs"): + return transport.proxyConfigRequest(request) + case strings.HasPrefix(requestPath, "/containers"): + return transport.proxyContainerRequest(request) + case strings.HasPrefix(requestPath, "/services"): + return transport.proxyServiceRequest(request) + case strings.HasPrefix(requestPath, "/volumes"): + return transport.proxyVolumeRequest(request) + case strings.HasPrefix(requestPath, "/networks"): + return transport.proxyNetworkRequest(request) + case strings.HasPrefix(requestPath, "/secrets"): + return transport.proxySecretRequest(request) + case strings.HasPrefix(requestPath, "/swarm"): + return transport.proxySwarmRequest(request) + case strings.HasPrefix(requestPath, "/nodes"): + return transport.proxyNodeRequest(request) + case strings.HasPrefix(requestPath, "/tasks"): + return transport.proxyTaskRequest(request) + case strings.HasPrefix(requestPath, "/build"): + return transport.proxyBuildRequest(request) + case strings.HasPrefix(requestPath, "/images"): + return transport.proxyImageRequest(request) + case strings.HasPrefix(requestPath, "/v2"): + return transport.proxyAgentRequest(request) + default: + return transport.executeDockerRequest(request) + } +} + +func (transport *Transport) executeDockerRequest(request *http.Request) (*http.Response, error) { + response, err := transport.HTTPTransport.RoundTrip(request) + + if transport.endpoint.Type != portainer.EdgeAgentEnvironment { + return response, err + } + + if err == nil { + transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpoint.ID) + } else { + transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpoint.ID) + } + + return response, err +} + +func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response, error) { + requestPath := strings.TrimPrefix(r.URL.Path, "/v2") + + switch { + case strings.HasPrefix(requestPath, "/browse"): + volumeIDParameter, found := r.URL.Query()["volumeID"] + if !found || len(volumeIDParameter) < 1 { + return transport.administratorOperation(r) + } + + return transport.restrictedResourceOperation(r, volumeIDParameter[0], portainer.VolumeResourceControl, true) + } + + return transport.executeDockerRequest(r) +} + +func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Response, error) { + switch requestPath := request.URL.Path; requestPath { + case "/configs/create": + return transport.decorateGenericResourceCreationOperation(request, configObjectIdentifier, portainer.ConfigResourceControl) + + case "/configs": + return transport.rewriteOperation(request, transport.configListOperation) + + default: + // assume /configs/{id} + configID := path.Base(requestPath) + + if request.Method == http.MethodGet { + return transport.rewriteOperation(request, transport.configInspectOperation) + } else if request.Method == http.MethodDelete { + return transport.executeGenericResourceDeletionOperation(request, configID, portainer.ConfigResourceControl) + } + + return transport.restrictedResourceOperation(request, configID, portainer.ConfigResourceControl, false) + } +} + +func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.Response, error) { + switch requestPath := request.URL.Path; requestPath { + case "/containers/create": + return transport.decorateGenericResourceCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl) + + case "/containers/prune": + return transport.administratorOperation(request) + + case "/containers/json": + return transport.rewriteOperationWithLabelFiltering(request, transport.containerListOperation) + + default: + // This section assumes /containers/** + if match, _ := path.Match("/containers/*/*", requestPath); match { + // Handle /containers/{id}/{action} requests + containerID := path.Base(path.Dir(requestPath)) + action := path.Base(requestPath) + + if action == "json" { + return transport.rewriteOperation(request, transport.containerInspectOperation) + } + return transport.restrictedResourceOperation(request, containerID, portainer.ContainerResourceControl, false) + } else if match, _ := path.Match("/containers/*", requestPath); match { + // Handle /containers/{id} requests + containerID := path.Base(requestPath) + + if request.Method == http.MethodDelete { + return transport.executeGenericResourceDeletionOperation(request, containerID, portainer.ContainerResourceControl) + } + + return transport.restrictedResourceOperation(request, containerID, portainer.ContainerResourceControl, false) + } + return transport.executeDockerRequest(request) + } +} + +func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Response, error) { + switch requestPath := request.URL.Path; requestPath { + case "/services/create": + return transport.replaceRegistryAuthenticationHeader(request) + + case "/services": + return transport.rewriteOperation(request, transport.serviceListOperation) + + default: + // This section assumes /services/** + if match, _ := path.Match("/services/*/*", requestPath); match { + // Handle /services/{id}/{action} requests + serviceID := path.Base(path.Dir(requestPath)) + return transport.restrictedResourceOperation(request, serviceID, portainer.ServiceResourceControl, false) + } else if match, _ := path.Match("/services/*", requestPath); match { + // Handle /services/{id} requests + serviceID := path.Base(requestPath) + + switch request.Method { + case http.MethodGet: + return transport.rewriteOperation(request, transport.serviceInspectOperation) + case http.MethodDelete: + return transport.executeGenericResourceDeletionOperation(request, serviceID, portainer.ServiceResourceControl) + } + return transport.restrictedResourceOperation(request, serviceID, portainer.ServiceResourceControl, false) + } + return transport.executeDockerRequest(request) + } +} + +func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Response, error) { + switch requestPath := request.URL.Path; requestPath { + case "/volumes/create": + return transport.decorateGenericResourceCreationOperation(request, volumeObjectIdentifier, portainer.VolumeResourceControl) + + case "/volumes/prune": + return transport.administratorOperation(request) + + case "/volumes": + return transport.rewriteOperation(request, transport.volumeListOperation) + + default: + // assume /volumes/{name} + volumeID := path.Base(requestPath) + + if request.Method == http.MethodGet { + return transport.rewriteOperation(request, transport.volumeInspectOperation) + } else if request.Method == http.MethodDelete { + return transport.executeGenericResourceDeletionOperation(request, volumeID, portainer.VolumeResourceControl) + } + return transport.restrictedResourceOperation(request, volumeID, portainer.VolumeResourceControl, false) + } +} + +func (transport *Transport) proxyNetworkRequest(request *http.Request) (*http.Response, error) { + switch requestPath := request.URL.Path; requestPath { + case "/networks/create": + return transport.decorateGenericResourceCreationOperation(request, networkObjectIdentifier, portainer.NetworkResourceControl) + + case "/networks": + return transport.rewriteOperation(request, transport.networkListOperation) + + default: + // assume /networks/{id} + networkID := path.Base(requestPath) + + if request.Method == http.MethodGet { + return transport.rewriteOperation(request, transport.networkInspectOperation) + } else if request.Method == http.MethodDelete { + return transport.executeGenericResourceDeletionOperation(request, networkID, portainer.NetworkResourceControl) + } + return transport.restrictedResourceOperation(request, networkID, portainer.NetworkResourceControl, false) + } +} + +func (transport *Transport) proxySecretRequest(request *http.Request) (*http.Response, error) { + switch requestPath := request.URL.Path; requestPath { + case "/secrets/create": + return transport.decorateGenericResourceCreationOperation(request, secretObjectIdentifier, portainer.SecretResourceControl) + + case "/secrets": + return transport.rewriteOperation(request, transport.secretListOperation) + + default: + // assume /secrets/{id} + secretID := path.Base(requestPath) + + if request.Method == http.MethodGet { + return transport.rewriteOperation(request, transport.secretInspectOperation) + } else if request.Method == http.MethodDelete { + return transport.executeGenericResourceDeletionOperation(request, secretID, portainer.SecretResourceControl) + } + return transport.restrictedResourceOperation(request, secretID, portainer.SecretResourceControl, false) + } +} + +func (transport *Transport) proxyNodeRequest(request *http.Request) (*http.Response, error) { + requestPath := request.URL.Path + + // assume /nodes/{id} + if path.Base(requestPath) != "nodes" { + return transport.administratorOperation(request) + } + + return transport.executeDockerRequest(request) +} + +func (transport *Transport) proxySwarmRequest(request *http.Request) (*http.Response, error) { + switch requestPath := request.URL.Path; requestPath { + case "/swarm": + return transport.rewriteOperation(request, swarmInspectOperation) + default: + // assume /swarm/{action} + return transport.administratorOperation(request) + } +} + +func (transport *Transport) proxyTaskRequest(request *http.Request) (*http.Response, error) { + switch requestPath := request.URL.Path; requestPath { + case "/tasks": + return transport.rewriteOperation(request, transport.taskListOperation) + default: + // assume /tasks/{id} + return transport.executeDockerRequest(request) + } +} + +func (transport *Transport) proxyBuildRequest(request *http.Request) (*http.Response, error) { + return transport.interceptAndRewriteRequest(request, buildOperation) +} + +func (transport *Transport) proxyImageRequest(request *http.Request) (*http.Response, error) { + switch requestPath := request.URL.Path; requestPath { + case "/images/create": + return transport.replaceRegistryAuthenticationHeader(request) + default: + if path.Base(requestPath) == "push" && request.Method == http.MethodPost { + return transport.replaceRegistryAuthenticationHeader(request) + } + return transport.executeDockerRequest(request) + } +} + +func (transport *Transport) replaceRegistryAuthenticationHeader(request *http.Request) (*http.Response, error) { + accessContext, err := transport.createRegistryAccessContext(request) + if err != nil { + return nil, err + } + + originalHeader := request.Header.Get("X-Registry-Auth") + + if originalHeader != "" { + + decodedHeaderData, err := base64.StdEncoding.DecodeString(originalHeader) + if err != nil { + return nil, err + } + + var originalHeaderData registryAuthenticationHeader + err = json.Unmarshal(decodedHeaderData, &originalHeaderData) + if err != nil { + return nil, err + } + + authenticationHeader := createRegistryAuthenticationHeader(originalHeaderData.Serveraddress, accessContext) + + headerData, err := json.Marshal(authenticationHeader) + if err != nil { + return nil, err + } + + header := base64.StdEncoding.EncodeToString(headerData) + + request.Header.Set("X-Registry-Auth", header) + } + + return transport.decorateGenericResourceCreationOperation(request, serviceObjectIdentifier, portainer.ServiceResourceControl) +} + +func (transport *Transport) restrictedResourceOperation(request *http.Request, resourceID string, resourceType portainer.ResourceControlType, volumeBrowseRestrictionCheck bool) (*http.Response, error) { + var err error + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + if tokenData.Role != portainer.AdministratorRole { + rbacExtension, err := transport.extensionService.Extension(portainer.RBACExtension) + if err != nil && err != portainer.ErrObjectNotFound { + return nil, err + } + + if volumeBrowseRestrictionCheck { + settings, err := transport.settingsService.Settings() + if err != nil { + return nil, err + } + + if rbacExtension != nil && !settings.AllowVolumeBrowserForRegularUsers { + return responseutils.WriteAccessDeniedResponse() + } + } + + user, err := transport.userService.User(tokenData.ID) + if err != nil { + return nil, err + } + + endpointResourceAccess := false + _, ok := user.EndpointAuthorizations[transport.endpoint.ID][portainer.EndpointResourcesAccess] + if ok { + endpointResourceAccess = true + } + + if rbacExtension != nil && endpointResourceAccess { + return transport.executeDockerRequest(request) + } + + teamMemberships, err := transport.teamMembershipService.TeamMembershipsByUserID(tokenData.ID) + if err != nil { + return nil, err + } + + userTeamIDs := make([]portainer.TeamID, 0) + for _, membership := range teamMemberships { + userTeamIDs = append(userTeamIDs, membership.TeamID) + } + + resourceControls, err := transport.resourceControlService.ResourceControls() + if err != nil { + return nil, err + } + + resourceControl := portainer.GetResourceControlByResourceIDAndType(resourceID, resourceType, resourceControls) + if resourceControl == nil { + agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader) + + // This resource was created outside of portainer, + // is part of a Docker service or part of a Docker Swarm/Compose stack. + inheritedResourceControl, err := transport.getInheritedResourceControlFromServiceOrStack(resourceID, agentTargetHeader, resourceType, resourceControls) + if err != nil { + return nil, err + } + + if inheritedResourceControl == nil || !portainer.UserCanAccessResource(tokenData.ID, userTeamIDs, inheritedResourceControl) { + return responseutils.WriteAccessDeniedResponse() + } + } + + if resourceControl != nil && !portainer.UserCanAccessResource(tokenData.ID, userTeamIDs, resourceControl) { + return responseutils.WriteAccessDeniedResponse() + } + } + + return transport.executeDockerRequest(request) +} + +// rewriteOperationWithLabelFiltering will create a new operation context with data that will be used +// to decorate the original request's response as well as retrieve all the black listed labels +// to filter the resources. +func (transport *Transport) rewriteOperationWithLabelFiltering(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) { + operationContext, err := transport.createOperationContext(request) + if err != nil { + return nil, err + } + + settings, err := transport.settingsService.Settings() + if err != nil { + return nil, err + } + + executor := &operationExecutor{ + operationContext: operationContext, + labelBlackList: settings.BlackListedLabels, + } + + return transport.executeRequestAndRewriteResponse(request, operation, executor) +} + +// rewriteOperation will create a new operation context with data that will be used +// to decorate the original request's response. +func (transport *Transport) rewriteOperation(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) { + operationContext, err := transport.createOperationContext(request) + if err != nil { + return nil, err + } + + executor := &operationExecutor{ + operationContext: operationContext, + } + + return transport.executeRequestAndRewriteResponse(request, operation, executor) +} + +func (transport *Transport) interceptAndRewriteRequest(request *http.Request, operation operationRequest) (*http.Response, error) { + err := operation(request) + if err != nil { + return nil, err + } + + return transport.executeDockerRequest(request) +} + +// decorateGenericResourceCreationResponse extracts the response as a JSON object, extracts the resource identifier from that object based +// on the resourceIdentifierAttribute parameter then generate a new resource control associated to that resource +// with a random token and rewrites the response by decorating the original response with a ResourceControl object. +// The generic Docker API response format is JSON object: +// https://docs.docker.com/engine/api/v1.40/#operation/ContainerCreate +// https://docs.docker.com/engine/api/v1.40/#operation/NetworkCreate +// https://docs.docker.com/engine/api/v1.40/#operation/VolumeCreate +// https://docs.docker.com/engine/api/v1.40/#operation/ServiceCreate +// https://docs.docker.com/engine/api/v1.40/#operation/SecretCreate +// https://docs.docker.com/engine/api/v1.40/#operation/ConfigCreate +func (transport *Transport) decorateGenericResourceCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error { + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return err + } + + if responseObject[resourceIdentifierAttribute] == nil { + log.Printf("[ERROR] [proxy,docker]") + return errors.New("missing identifier in Docker resource creation response") + } + + resourceID := responseObject[resourceIdentifierAttribute].(string) + + resourceControl, err := transport.createPrivateResourceControl(resourceID, resourceType, userID) + if err != nil { + return err + } + + responseObject = decorateObject(responseObject, resourceControl) + + return responseutils.RewriteResponse(response, responseObject, http.StatusOK) +} + +func (transport *Transport) decorateGenericResourceCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + response, err := transport.executeDockerRequest(request) + if err != nil { + return response, err + } + + if response.StatusCode == http.StatusCreated { + err = transport.decorateGenericResourceCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID) + } + + return response, err +} + +func (transport *Transport) executeGenericResourceDeletionOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) { + response, err := transport.restrictedResourceOperation(request, resourceIdentifierAttribute, resourceType, false) + if err != nil { + return response, err + } + + resourceControl, err := transport.resourceControlService.ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType) + if err != nil { + return response, err + } + + if resourceControl != nil { + err = transport.resourceControlService.DeleteResourceControl(resourceControl.ID) + if err != nil { + return response, err + } + } + + return response, err +} + +func (transport *Transport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) { + response, err := transport.executeDockerRequest(request) + if err != nil { + return response, err + } + + err = operation(response, executor) + return response, err +} + +// administratorOperation ensures that the user has administrator privileges +// before executing the original request. +func (transport *Transport) administratorOperation(request *http.Request) (*http.Response, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + if tokenData.Role != portainer.AdministratorRole { + return responseutils.WriteAccessDeniedResponse() + } + + return transport.executeDockerRequest(request) +} + +func (transport *Transport) createRegistryAccessContext(request *http.Request) (*registryAccessContext, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + accessContext := ®istryAccessContext{ + isAdmin: true, + userID: tokenData.ID, + } + + hub, err := transport.dockerHubService.DockerHub() + if err != nil { + return nil, err + } + accessContext.dockerHub = hub + + registries, err := transport.registryService.Registries() + if err != nil { + return nil, err + } + accessContext.registries = registries + + if tokenData.Role != portainer.AdministratorRole { + accessContext.isAdmin = false + + teamMemberships, err := transport.teamMembershipService.TeamMembershipsByUserID(tokenData.ID) + if err != nil { + return nil, err + } + + accessContext.teamMemberships = teamMemberships + } + + return accessContext, nil +} + +func (transport *Transport) createOperationContext(request *http.Request) (*restrictedDockerOperationContext, error) { + var err error + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + resourceControls, err := transport.resourceControlService.ResourceControls() + if err != nil { + return nil, err + } + + operationContext := &restrictedDockerOperationContext{ + isAdmin: true, + userID: tokenData.ID, + resourceControls: resourceControls, + endpointResourceAccess: false, + } + + if tokenData.Role != portainer.AdministratorRole { + operationContext.isAdmin = false + + user, err := transport.userService.User(operationContext.userID) + if err != nil { + return nil, err + } + + _, ok := user.EndpointAuthorizations[transport.endpoint.ID][portainer.EndpointResourcesAccess] + if ok { + operationContext.endpointResourceAccess = true + } + + teamMemberships, err := transport.teamMembershipService.TeamMembershipsByUserID(tokenData.ID) + if err != nil { + return nil, err + } + + userTeamIDs := make([]portainer.TeamID, 0) + for _, membership := range teamMemberships { + userTeamIDs = append(userTeamIDs, membership.TeamID) + } + operationContext.userTeamIDs = userTeamIDs + } + + return operationContext, nil +} diff --git a/api/http/proxy/factory/docker/volumes.go b/api/http/proxy/factory/docker/volumes.go new file mode 100644 index 000000000..49512852e --- /dev/null +++ b/api/http/proxy/factory/docker/volumes.go @@ -0,0 +1,89 @@ +package docker + +import ( + "context" + "net/http" + + "github.com/docker/docker/client" + + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/responseutils" +) + +const ( + volumeObjectIdentifier = "Name" +) + +func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { + volume, err := dockerClient.VolumeInspect(context.Background(), volumeID) + if err != nil { + return nil, err + } + + swarmStackName := volume.Labels[resourceLabelForDockerSwarmStackName] + if swarmStackName != "" { + return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil + } + + return nil, nil +} + +// volumeListOperation extracts the response as a JSON object, loop through the volume array +// decorate and/or filter the volumes based on resource controls before rewriting the response. +func (transport *Transport) volumeListOperation(response *http.Response, executor *operationExecutor) error { + // VolumeList response is a JSON object + // https://docs.docker.com/engine/api/v1.28/#operation/VolumeList + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return err + } + + // The "Volumes" field contains the list of volumes as an array of JSON objects + if responseObject["Volumes"] != nil { + volumeData := responseObject["Volumes"].([]interface{}) + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: volumeObjectIdentifier, + resourceType: portainer.VolumeResourceControl, + labelsObjectSelector: selectorVolumeLabels, + } + + volumeData, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, volumeData, executor) + if err != nil { + return err + } + + // Overwrite the original volume list + responseObject["Volumes"] = volumeData + } + + return responseutils.RewriteResponse(response, responseObject, http.StatusOK) +} + +// volumeInspectOperation extracts the response as a JSON object, verify that the user +// has access to the volume based on any existing resource control and either rewrite an access denied response or a decorated volume. +func (transport *Transport) volumeInspectOperation(response *http.Response, executor *operationExecutor) error { + // VolumeInspect response is a JSON object + // https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect + responseObject, err := responseutils.GetResponseAsJSONOBject(response) + if err != nil { + return err + } + + resourceOperationParameters := &resourceOperationParameters{ + resourceIdentifierAttribute: volumeObjectIdentifier, + resourceType: portainer.VolumeResourceControl, + labelsObjectSelector: selectorVolumeLabels, + } + + return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor) +} + +// selectorVolumeLabels retrieve the labels object associated to the volume object. +// Labels are available under the "Labels" property. +// API schema references: +// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect +// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList +func selectorVolumeLabels(responseObject map[string]interface{}) map[string]interface{} { + return responseutils.GetJSONObject(responseObject, "Labels") +} diff --git a/api/http/proxy/factory/docker_unix.go b/api/http/proxy/factory/docker_unix.go new file mode 100644 index 000000000..214b50747 --- /dev/null +++ b/api/http/proxy/factory/docker_unix.go @@ -0,0 +1,46 @@ +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris + +package factory + +import ( + "net" + "net/http" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/docker" +) + +func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portainer.Endpoint) (http.Handler, error) { + transportParameters := &docker.TransportParameters{ + Endpoint: endpoint, + ResourceControlService: factory.resourceControlService, + UserService: factory.userService, + TeamService: factory.teamService, + TeamMembershipService: factory.teamMembershipService, + RegistryService: factory.registryService, + DockerHubService: factory.dockerHubService, + SettingsService: factory.settingsService, + ReverseTunnelService: factory.reverseTunnelService, + ExtensionService: factory.extensionService, + SignatureService: factory.signatureService, + DockerClientFactory: factory.dockerClientFactory, + } + + proxy := &dockerLocalProxy{} + + dockerTransport, err := docker.NewTransport(transportParameters, newSocketTransport(path)) + if err != nil { + return nil, err + } + + proxy.transport = dockerTransport + return proxy, nil +} + +func newSocketTransport(socketPath string) *http.Transport { + return &http.Transport{ + Dial: func(proto, addr string) (conn net.Conn, err error) { + return net.Dial("unix", socketPath) + }, + } +} diff --git a/api/http/proxy/factory/docker_windows.go b/api/http/proxy/factory/docker_windows.go new file mode 100644 index 000000000..c965b8981 --- /dev/null +++ b/api/http/proxy/factory/docker_windows.go @@ -0,0 +1,47 @@ +// +build windows + +package factory + +import ( + "net" + "net/http" + + "github.com/Microsoft/go-winio" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/docker" +) + +func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portainer.Endpoint) (http.Handler, error) { + transportParameters := &docker.TransportParameters{ + Endpoint: endpoint, + ResourceControlService: factory.resourceControlService, + UserService: factory.userService, + TeamService: factory.teamService, + TeamMembershipService: factory.teamMembershipService, + RegistryService: factory.registryService, + DockerHubService: factory.dockerHubService, + SettingsService: factory.settingsService, + ReverseTunnelService: factory.reverseTunnelService, + ExtensionService: factory.extensionService, + SignatureService: factory.signatureService, + DockerClientFactory: factory.dockerClientFactory, + } + + proxy := &dockerLocalProxy{} + + dockerTransport, err := docker.NewTransport(transportParameters, newSocketTransport(path)) + if err != nil { + return nil, err + } + + proxy.transport = dockerTransport + return proxy, nil +} + +func newNamedPipeTransport(namedPipePath string) *http.Transport { + return &http.Transport{ + Dial: func(proto, addr string) (conn net.Conn, err error) { + return winio.DialPipe(namedPipePath, nil) + }, + } +} diff --git a/api/http/proxy/factory/factory.go b/api/http/proxy/factory/factory.go new file mode 100644 index 000000000..2440928cb --- /dev/null +++ b/api/http/proxy/factory/factory.go @@ -0,0 +1,109 @@ +package factory + +import ( + "fmt" + "net/http" + "net/http/httputil" + "net/url" + + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/docker" +) + +const azureAPIBaseURL = "https://management.azure.com" + +var extensionPorts = map[portainer.ExtensionID]string{ + portainer.RegistryManagementExtension: "7001", + portainer.OAuthAuthenticationExtension: "7002", + portainer.RBACExtension: "7003", +} + +type ( + // ProxyFactory is a factory to create reverse proxies to Docker endpoints and extensions + ProxyFactory struct { + resourceControlService portainer.ResourceControlService + userService portainer.UserService + teamService portainer.TeamService + teamMembershipService portainer.TeamMembershipService + settingsService portainer.SettingsService + registryService portainer.RegistryService + dockerHubService portainer.DockerHubService + signatureService portainer.DigitalSignatureService + reverseTunnelService portainer.ReverseTunnelService + extensionService portainer.ExtensionService + dockerClientFactory *docker.ClientFactory + } + + // ProxyFactoryParameters is used to create a new ProxyFactory + ProxyFactoryParameters struct { + ResourceControlService portainer.ResourceControlService + UserService portainer.UserService + TeamService portainer.TeamService + TeamMembershipService portainer.TeamMembershipService + SettingsService portainer.SettingsService + RegistryService portainer.RegistryService + DockerHubService portainer.DockerHubService + SignatureService portainer.DigitalSignatureService + ReverseTunnelService portainer.ReverseTunnelService + ExtensionService portainer.ExtensionService + DockerClientFactory *docker.ClientFactory + } +) + +// NewProxyFactory returns a pointer to a new instance of a ProxyFactory +func NewProxyFactory(parameters *ProxyFactoryParameters) *ProxyFactory { + return &ProxyFactory{ + resourceControlService: parameters.ResourceControlService, + userService: parameters.UserService, + teamService: parameters.TeamService, + teamMembershipService: parameters.TeamMembershipService, + settingsService: parameters.SettingsService, + registryService: parameters.RegistryService, + dockerHubService: parameters.DockerHubService, + signatureService: parameters.SignatureService, + reverseTunnelService: parameters.ReverseTunnelService, + extensionService: parameters.ExtensionService, + dockerClientFactory: parameters.DockerClientFactory, + } +} + +// BuildExtensionURL returns the URL to an extension server +func BuildExtensionURL(extensionID portainer.ExtensionID) string { + return fmt.Sprintf("http://%s:%s", portainer.ExtensionServer, extensionPorts[extensionID]) +} + +// NewExtensionProxy returns a new HTTP proxy to an extension server +func (factory *ProxyFactory) NewExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) { + address := "http://" + portainer.ExtensionServer + ":" + extensionPorts[extensionID] + + extensionURL, err := url.Parse(address) + if err != nil { + return nil, err + } + + extensionURL.Scheme = "http" + proxy := httputil.NewSingleHostReverseProxy(extensionURL) + return proxy, nil +} + +// NewLegacyExtensionProxy returns a new HTTP proxy to a legacy extension server (Storidge) +func (factory *ProxyFactory) NewLegacyExtensionProxy(extensionAPIURL string) (http.Handler, error) { + extensionURL, err := url.Parse(extensionAPIURL) + if err != nil { + return nil, err + } + + extensionURL.Scheme = "http" + proxy := httputil.NewSingleHostReverseProxy(extensionURL) + return proxy, nil +} + +// NewEndpointProxy returns a new reverse proxy (filesystem based or HTTP) to an endpoint API server +func (factory *ProxyFactory) NewEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + switch endpoint.Type { + case portainer.AzureEnvironment: + return newAzureProxy(endpoint) + } + + return factory.newDockerProxy(endpoint) +} diff --git a/api/http/proxy/factory/responseutils/json.go b/api/http/proxy/factory/responseutils/json.go new file mode 100644 index 000000000..15af94c60 --- /dev/null +++ b/api/http/proxy/factory/responseutils/json.go @@ -0,0 +1,11 @@ +package responseutils + +// GetJSONObject will extract an object from a specific property of another JSON object. +// Returns nil if nothing is associated to the specified key. +func GetJSONObject(jsonObject map[string]interface{}, property string) map[string]interface{} { + object := jsonObject[property] + if object != nil { + return object.(map[string]interface{}) + } + return nil +} diff --git a/api/http/proxy/factory/responseutils/response.go b/api/http/proxy/factory/responseutils/response.go new file mode 100644 index 000000000..1ce1d39e9 --- /dev/null +++ b/api/http/proxy/factory/responseutils/response.go @@ -0,0 +1,106 @@ +package responseutils + +import ( + "bytes" + "encoding/json" + "errors" + "io/ioutil" + "log" + "net/http" + "strconv" +) + +// GetResponseAsJSONOBject returns the response content as a generic JSON object +func GetResponseAsJSONOBject(response *http.Response) (map[string]interface{}, error) { + responseData, err := getResponseBodyAsGenericJSON(response) + if err != nil { + return nil, err + } + + responseObject := responseData.(map[string]interface{}) + return responseObject, nil +} + +// GetResponseAsJSONArray returns the response content as an array of generic JSON object +func GetResponseAsJSONArray(response *http.Response) ([]interface{}, error) { + responseData, err := getResponseBodyAsGenericJSON(response) + if err != nil { + return nil, err + } + + switch responseObject := responseData.(type) { + case []interface{}: + return responseObject, nil + case map[string]interface{}: + if responseObject["message"] != nil { + return nil, errors.New(responseObject["message"].(string)) + } + log.Printf("[ERROR] [http,proxy,response] [message: invalid response format, expecting JSON array] [response: %+v]", responseObject) + return nil, errors.New("unable to parse response: expected JSON array, got JSON object") + default: + log.Printf("[ERROR] [http,proxy,response] [message: invalid response format, expecting JSON array] [response: %+v]", responseObject) + return nil, errors.New("unable to parse response: expected JSON array") + } +} + +func getResponseBodyAsGenericJSON(response *http.Response) (interface{}, error) { + if response.Body == nil { + return nil, errors.New("unable to parse response: empty response body") + } + + var data interface{} + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + + err = response.Body.Close() + if err != nil { + return nil, err + } + + err = json.Unmarshal(body, &data) + if err != nil { + return nil, err + } + + return data, nil +} + +type dockerErrorResponse struct { + Message string `json:"message,omitempty"` +} + +// WriteAccessDeniedResponse will create a new access denied response +func WriteAccessDeniedResponse() (*http.Response, error) { + response := &http.Response{} + err := RewriteResponse(response, dockerErrorResponse{Message: "access denied to resource"}, http.StatusForbidden) + return response, err +} + +// RewriteAccessDeniedResponse will overwrite the existing response with an access denied response +func RewriteAccessDeniedResponse(response *http.Response) error { + return RewriteResponse(response, dockerErrorResponse{Message: "access denied to resource"}, http.StatusForbidden) +} + +// RewriteResponse will replace the existing response body and status code with the one specified +// in parameters +func RewriteResponse(response *http.Response, newResponseData interface{}, statusCode int) error { + jsonData, err := json.Marshal(newResponseData) + if err != nil { + return err + } + + body := ioutil.NopCloser(bytes.NewReader(jsonData)) + + response.StatusCode = statusCode + response.Body = body + response.ContentLength = int64(len(jsonData)) + + if response.Header == nil { + response.Header = make(http.Header) + } + response.Header.Set("Content-Length", strconv.Itoa(len(jsonData))) + + return nil +} diff --git a/api/http/proxy/reverse_proxy.go b/api/http/proxy/factory/reverse_proxy.go similarity index 98% rename from api/http/proxy/reverse_proxy.go rename to api/http/proxy/factory/reverse_proxy.go index 47e71b63e..93d22f94d 100644 --- a/api/http/proxy/reverse_proxy.go +++ b/api/http/proxy/factory/reverse_proxy.go @@ -1,4 +1,4 @@ -package proxy +package factory import ( "net/http" diff --git a/api/http/proxy/factory_local.go b/api/http/proxy/factory_local.go deleted file mode 100644 index c9ab44b81..000000000 --- a/api/http/proxy/factory_local.go +++ /dev/null @@ -1,29 +0,0 @@ -// +build !windows - -package proxy - -import ( - "net/http" - - portainer "github.com/portainer/portainer/api" -) - -func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endpoint) http.Handler { - proxy := &localProxy{} - transport := &proxyTransport{ - enableSignature: false, - ResourceControlService: factory.ResourceControlService, - UserService: factory.UserService, - TeamMembershipService: factory.TeamMembershipService, - SettingsService: factory.SettingsService, - RegistryService: factory.RegistryService, - DockerHubService: factory.DockerHubService, - ExtensionService: factory.ExtensionService, - dockerTransport: newSocketTransport(path), - ReverseTunnelService: factory.ReverseTunnelService, - endpointIdentifier: endpoint.ID, - endpointType: endpoint.Type, - } - proxy.Transport = transport - return proxy -} diff --git a/api/http/proxy/factory_local_windows.go b/api/http/proxy/factory_local_windows.go deleted file mode 100644 index 3f1d860d7..000000000 --- a/api/http/proxy/factory_local_windows.go +++ /dev/null @@ -1,40 +0,0 @@ -// +build windows - -package proxy - -import ( - "net" - "net/http" - - "github.com/Microsoft/go-winio" - - portainer "github.com/portainer/portainer/api" -) - -func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endpoint) http.Handler { - proxy := &localProxy{} - transport := &proxyTransport{ - enableSignature: false, - ResourceControlService: factory.ResourceControlService, - UserService: factory.UserService, - TeamMembershipService: factory.TeamMembershipService, - SettingsService: factory.SettingsService, - RegistryService: factory.RegistryService, - DockerHubService: factory.DockerHubService, - ReverseTunnelService: factory.ReverseTunnelService, - ExtensionService: factory.ExtensionService, - dockerTransport: newNamedPipeTransport(path), - endpointIdentifier: endpoint.ID, - endpointType: endpoint.Type, - } - proxy.Transport = transport - return proxy -} - -func newNamedPipeTransport(namedPipePath string) *http.Transport { - return &http.Transport{ - Dial: func(proto, addr string) (conn net.Conn, err error) { - return winio.DialPipe(namedPipePath, nil) - }, - } -} diff --git a/api/http/proxy/local.go b/api/http/proxy/local.go deleted file mode 100644 index 7686768ad..000000000 --- a/api/http/proxy/local.go +++ /dev/null @@ -1,43 +0,0 @@ -package proxy - -import ( - "io" - "log" - "net/http" - - httperror "github.com/portainer/libhttp/error" -) - -type localProxy struct { - Transport *proxyTransport -} - -func (proxy *localProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Force URL/domain to http/unixsocket to be able to - // use http.Transport RoundTrip to do the requests via the socket - r.URL.Scheme = "http" - r.URL.Host = "unixsocket" - - res, err := proxy.Transport.proxyDockerRequest(r) - if err != nil { - code := http.StatusInternalServerError - if res != nil && res.StatusCode != 0 { - code = res.StatusCode - } - httperror.WriteError(w, code, "Unable to proxy the request via the Docker socket", err) - return - } - defer res.Body.Close() - - for k, vv := range res.Header { - for _, v := range vv { - w.Header().Add(k, v) - } - } - - w.WriteHeader(res.StatusCode) - - if _, err := io.Copy(w, res.Body); err != nil { - log.Printf("proxy error: %s\n", err) - } -} diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index aa4a68bef..d692d4efa 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -1,29 +1,22 @@ package proxy import ( - "fmt" "net/http" - "net/url" "strconv" "github.com/orcaman/concurrent-map" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/docker" + "github.com/portainer/portainer/api/http/proxy/factory" ) // TODO: contain code related to legacy extension management -var extensionPorts = map[portainer.ExtensionID]string{ - portainer.RegistryManagementExtension: "7001", - portainer.OAuthAuthenticationExtension: "7002", - portainer.RBACExtension: "7003", -} - type ( - // Manager represents a service used to manage Docker proxies. + // Manager represents a service used to manage proxies to endpoints and extensions. Manager struct { - proxyFactory *proxyFactory - reverseTunnelService portainer.ReverseTunnelService - proxies cmap.ConcurrentMap + proxyFactory *factory.ProxyFactory + endpointProxies cmap.ConcurrentMap extensionProxies cmap.ConcurrentMap legacyExtensionProxies cmap.ConcurrentMap } @@ -32,6 +25,7 @@ type ( ManagerParams struct { ResourceControlService portainer.ResourceControlService UserService portainer.UserService + TeamService portainer.TeamService TeamMembershipService portainer.TeamMembershipService SettingsService portainer.SettingsService RegistryService portainer.RegistryService @@ -39,54 +33,71 @@ type ( SignatureService portainer.DigitalSignatureService ReverseTunnelService portainer.ReverseTunnelService ExtensionService portainer.ExtensionService + DockerClientFactory *docker.ClientFactory } ) // NewManager initializes a new proxy Service func NewManager(parameters *ManagerParams) *Manager { + proxyFactoryParameters := &factory.ProxyFactoryParameters{ + ResourceControlService: parameters.ResourceControlService, + UserService: parameters.UserService, + TeamService: parameters.TeamService, + TeamMembershipService: parameters.TeamMembershipService, + SettingsService: parameters.SettingsService, + RegistryService: parameters.RegistryService, + DockerHubService: parameters.DockerHubService, + SignatureService: parameters.SignatureService, + ReverseTunnelService: parameters.ReverseTunnelService, + ExtensionService: parameters.ExtensionService, + DockerClientFactory: parameters.DockerClientFactory, + } + return &Manager{ - proxies: cmap.New(), + endpointProxies: cmap.New(), extensionProxies: cmap.New(), legacyExtensionProxies: cmap.New(), - proxyFactory: &proxyFactory{ - ResourceControlService: parameters.ResourceControlService, - UserService: parameters.UserService, - TeamMembershipService: parameters.TeamMembershipService, - SettingsService: parameters.SettingsService, - RegistryService: parameters.RegistryService, - DockerHubService: parameters.DockerHubService, - SignatureService: parameters.SignatureService, - ReverseTunnelService: parameters.ReverseTunnelService, - ExtensionService: parameters.ExtensionService, - }, - reverseTunnelService: parameters.ReverseTunnelService, + proxyFactory: factory.NewProxyFactory(proxyFactoryParameters), } } -// GetProxy returns the proxy associated to a key -func (manager *Manager) GetProxy(endpoint *portainer.Endpoint) http.Handler { - proxy, ok := manager.proxies.Get(string(endpoint.ID)) - if !ok { - return nil - } - return proxy.(http.Handler) -} - -// CreateAndRegisterProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies. +// CreateAndRegisterEndpointProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies. // It can also be used to create a new HTTP reverse proxy and replace an already registered proxy. -func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (http.Handler, error) { - proxy, err := manager.createProxy(endpoint) +func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) { + proxy, err := manager.proxyFactory.NewEndpointProxy(endpoint) if err != nil { return nil, err } - manager.proxies.Set(string(endpoint.ID), proxy) + manager.endpointProxies.Set(string(endpoint.ID), proxy) return proxy, nil } -// DeleteProxy deletes the proxy associated to a key -func (manager *Manager) DeleteProxy(endpoint *portainer.Endpoint) { - manager.proxies.Remove(string(endpoint.ID)) +// GetEndpointProxy returns the proxy associated to a key +func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Handler { + proxy, ok := manager.endpointProxies.Get(string(endpoint.ID)) + if !ok { + return nil + } + + return proxy.(http.Handler) +} + +// DeleteEndpointProxy deletes the proxy associated to a key +func (manager *Manager) DeleteEndpointProxy(endpoint *portainer.Endpoint) { + manager.endpointProxies.Remove(string(endpoint.ID)) +} + +// CreateExtensionProxy creates a new HTTP reverse proxy for an extension and +// registers it in the extension map associated to the specified extension identifier +func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) { + proxy, err := manager.proxyFactory.NewExtensionProxy(extensionID) + if err != nil { + return nil, err + } + + manager.extensionProxies.Set(strconv.Itoa(int(extensionID)), proxy) + return proxy, nil } // GetExtensionProxy returns an extension proxy associated to an extension identifier @@ -95,28 +106,13 @@ func (manager *Manager) GetExtensionProxy(extensionID portainer.ExtensionID) htt if !ok { return nil } + return proxy.(http.Handler) } -// CreateExtensionProxy creates a new HTTP reverse proxy for an extension and -// registers it in the extension map associated to the specified extension identifier -func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) { - address := "http://" + portainer.ExtensionServer + ":" + extensionPorts[extensionID] - - extensionURL, err := url.Parse(address) - if err != nil { - return nil, err - } - - proxy := manager.proxyFactory.newHTTPProxy(extensionURL) - manager.extensionProxies.Set(strconv.Itoa(int(extensionID)), proxy) - - return proxy, nil -} - // GetExtensionURL retrieves the URL of an extension running locally based on the extension port table func (manager *Manager) GetExtensionURL(extensionID portainer.ExtensionID) string { - return "http://localhost:" + extensionPorts[extensionID] + return factory.BuildExtensionURL(extensionID) } // DeleteExtensionProxy deletes the extension proxy associated to an extension identifier @@ -124,6 +120,17 @@ func (manager *Manager) DeleteExtensionProxy(extensionID portainer.ExtensionID) manager.extensionProxies.Remove(strconv.Itoa(int(extensionID))) } +// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies. +func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) (http.Handler, error) { + proxy, err := manager.proxyFactory.NewLegacyExtensionProxy(extensionAPIURL) + if err != nil { + return nil, err + } + + manager.legacyExtensionProxies.Set(key, proxy) + return proxy, nil +} + // GetLegacyExtensionProxy returns a legacy extension proxy associated to a key func (manager *Manager) GetLegacyExtensionProxy(key string) http.Handler { proxy, ok := manager.legacyExtensionProxies.Get(key) @@ -133,56 +140,6 @@ func (manager *Manager) GetLegacyExtensionProxy(key string) http.Handler { return proxy.(http.Handler) } -// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies. -func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) (http.Handler, error) { - extensionURL, err := url.Parse(extensionAPIURL) - if err != nil { - return nil, err - } - - proxy := manager.proxyFactory.newHTTPProxy(extensionURL) - manager.legacyExtensionProxies.Set(key, proxy) - return proxy, nil -} - -func (manager *Manager) createDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) { - baseURL := endpoint.URL - if endpoint.Type == portainer.EdgeAgentEnvironment { - tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID) - baseURL = fmt.Sprintf("http://localhost:%d", tunnel.Port) - } - - endpointURL, err := url.Parse(baseURL) - if err != nil { - return nil, err - } - - switch endpoint.Type { - case portainer.AgentOnDockerEnvironment: - return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, endpoint) - case portainer.EdgeAgentEnvironment: - return manager.proxyFactory.newDockerHTTPProxy(endpointURL, endpoint), nil - } - - if endpointURL.Scheme == "tcp" { - if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify { - return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, endpoint) - } - - return manager.proxyFactory.newDockerHTTPProxy(endpointURL, endpoint), nil - } - - return manager.proxyFactory.newLocalProxy(endpointURL.Path, endpoint), nil -} - -func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) { - if endpoint.Type == portainer.AzureEnvironment { - return newAzureProxy(&endpoint.AzureCredentials) - } - - return manager.createDockerProxy(endpoint) -} - // CreateGitlabProxy creates a new HTTP reverse proxy that can be used to send requests to the Gitlab API.. func (manager *Manager) CreateGitlabProxy(url string) (http.Handler, error) { return newGitlabProxy(url) diff --git a/api/http/proxy/networks.go b/api/http/proxy/networks.go deleted file mode 100644 index 084f8604a..000000000 --- a/api/http/proxy/networks.go +++ /dev/null @@ -1,135 +0,0 @@ -package proxy - -import ( - "net/http" - - "github.com/portainer/portainer/api" -) - -const ( - // ErrDockerNetworkIdentifierNotFound defines an error raised when Portainer is unable to find a network identifier - ErrDockerNetworkIdentifierNotFound = portainer.Error("Docker network identifier not found") - networkIdentifier = "Id" - networkLabelForStackIdentifier = "com.docker.stack.namespace" -) - -// networkListOperation extracts the response as a JSON object, loop through the networks array -// decorate and/or filter the networks based on resource controls before rewriting the response -func networkListOperation(response *http.Response, executor *operationExecutor) error { - var err error - // NetworkList response is a JSON array - // https://docs.docker.com/engine/api/v1.28/#operation/NetworkList - responseArray, err := getResponseAsJSONArray(response) - if err != nil { - return err - } - - if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { - responseArray, err = decorateNetworkList(responseArray, executor.operationContext.resourceControls) - } else { - responseArray, err = filterNetworkList(responseArray, executor.operationContext) - } - if err != nil { - return err - } - - return rewriteResponse(response, responseArray, http.StatusOK) -} - -// networkInspectOperation extracts the response as a JSON object, verify that the user -// has access to the network based on resource control and either rewrite an access denied response -// or a decorated network. -func networkInspectOperation(response *http.Response, executor *operationExecutor) error { - // NetworkInspect response is a JSON object - // https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect - responseObject, err := getResponseAsJSONOBject(response) - if err != nil { - return err - } - - if responseObject[networkIdentifier] == nil { - return ErrDockerNetworkIdentifierNotFound - } - - networkID := responseObject[networkIdentifier].(string) - responseObject, access := applyResourceAccessControl(responseObject, networkID, executor.operationContext) - if access { - return rewriteResponse(response, responseObject, http.StatusOK) - } - - networkLabels := extractNetworkLabelsFromNetworkInspectObject(responseObject) - responseObject, access = applyResourceAccessControlFromLabel(networkLabels, responseObject, networkLabelForStackIdentifier, executor.operationContext) - if access { - return rewriteResponse(response, responseObject, http.StatusOK) - } - - return rewriteAccessDeniedResponse(response) -} - -// extractNetworkLabelsFromNetworkInspectObject retrieve the Labels of the network if present. -// Container schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect -func extractNetworkLabelsFromNetworkInspectObject(responseObject map[string]interface{}) map[string]interface{} { - // Labels are stored under Labels - return extractJSONField(responseObject, "Labels") -} - -// extractNetworkLabelsFromNetworkListObject retrieve the Labels of the network if present. -// Network schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList -func extractNetworkLabelsFromNetworkListObject(responseObject map[string]interface{}) map[string]interface{} { - // Labels are stored under Labels - return extractJSONField(responseObject, "Labels") -} - -// decorateNetworkList loops through all networks and decorates any network with an existing resource control. -// Resource controls checks are based on: resource identifier, stack identifier (from label). -// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList -func decorateNetworkList(networkData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) { - decoratedNetworkData := make([]interface{}, 0) - - for _, network := range networkData { - - networkObject := network.(map[string]interface{}) - if networkObject[networkIdentifier] == nil { - return nil, ErrDockerNetworkIdentifierNotFound - } - - networkID := networkObject[networkIdentifier].(string) - networkObject = decorateResourceWithAccessControl(networkObject, networkID, resourceControls) - - networkLabels := extractNetworkLabelsFromNetworkListObject(networkObject) - networkObject = decorateResourceWithAccessControlFromLabel(networkLabels, networkObject, networkLabelForStackIdentifier, resourceControls) - - decoratedNetworkData = append(decoratedNetworkData, networkObject) - } - - return decoratedNetworkData, nil -} - -// filterNetworkList loops through all networks and filters public networks (no associated resource control) -// as well as authorized networks (access granted to the user based on existing resource control). -// Authorized networks are decorated during the process. -// Resource controls checks are based on: resource identifier, stack identifier (from label). -// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList -func filterNetworkList(networkData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { - filteredNetworkData := make([]interface{}, 0) - - for _, network := range networkData { - networkObject := network.(map[string]interface{}) - if networkObject[networkIdentifier] == nil { - return nil, ErrDockerNetworkIdentifierNotFound - } - - networkID := networkObject[networkIdentifier].(string) - networkObject, access := applyResourceAccessControl(networkObject, networkID, context) - if !access { - networkLabels := extractNetworkLabelsFromNetworkListObject(networkObject) - networkObject, access = applyResourceAccessControlFromLabel(networkLabels, networkObject, networkLabelForStackIdentifier, context) - } - - if access { - filteredNetworkData = append(filteredNetworkData, networkObject) - } - } - - return filteredNetworkData, nil -} diff --git a/api/http/proxy/response.go b/api/http/proxy/response.go deleted file mode 100644 index a974d8f25..000000000 --- a/api/http/proxy/response.go +++ /dev/null @@ -1,109 +0,0 @@ -package proxy - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "log" - "net/http" - "strconv" - - "github.com/portainer/portainer/api" -) - -const ( - // ErrEmptyResponseBody defines an error raised when portainer excepts to parse the body of a HTTP response and there is nothing to parse - ErrEmptyResponseBody = portainer.Error("Empty response body") - // ErrInvalidResponseContent defines an error raised when Portainer excepts a JSON array and get something else. - ErrInvalidResponseContent = portainer.Error("Invalid Docker response") -) - -func extractJSONField(jsonObject map[string]interface{}, key string) map[string]interface{} { - object := jsonObject[key] - if object != nil { - return object.(map[string]interface{}) - } - return nil -} - -func getResponseAsJSONOBject(response *http.Response) (map[string]interface{}, error) { - responseData, err := getResponseBodyAsGenericJSON(response) - if err != nil { - return nil, err - } - - responseObject := responseData.(map[string]interface{}) - return responseObject, nil -} - -func getResponseAsJSONArray(response *http.Response) ([]interface{}, error) { - responseData, err := getResponseBodyAsGenericJSON(response) - if err != nil { - return nil, err - } - - switch responseObject := responseData.(type) { - case []interface{}: - return responseObject, nil - case map[string]interface{}: - if responseObject["message"] != nil { - return nil, portainer.Error(responseObject["message"].(string)) - } - log.Printf("Response: %+v\n", responseObject) - return nil, ErrInvalidResponseContent - default: - log.Printf("Response: %+v\n", responseObject) - return nil, ErrInvalidResponseContent - } -} - -func getResponseBodyAsGenericJSON(response *http.Response) (interface{}, error) { - var data interface{} - if response.Body != nil { - body, err := ioutil.ReadAll(response.Body) - if err != nil { - return nil, err - } - - err = response.Body.Close() - if err != nil { - return nil, err - } - - err = json.Unmarshal(body, &data) - if err != nil { - return nil, err - } - - return data, nil - } - return nil, ErrEmptyResponseBody -} - -func writeAccessDeniedResponse() (*http.Response, error) { - response := &http.Response{} - err := rewriteResponse(response, portainer.ErrResourceAccessDenied, http.StatusForbidden) - return response, err -} - -func rewriteAccessDeniedResponse(response *http.Response) error { - return rewriteResponse(response, portainer.ErrResourceAccessDenied, http.StatusForbidden) -} - -func rewriteResponse(response *http.Response, newResponseData interface{}, statusCode int) error { - jsonData, err := json.Marshal(newResponseData) - if err != nil { - return err - } - body := ioutil.NopCloser(bytes.NewReader(jsonData)) - response.StatusCode = statusCode - response.Body = body - response.ContentLength = int64(len(jsonData)) - - if response.Header == nil { - response.Header = make(http.Header) - } - response.Header.Set("Content-Length", strconv.Itoa(len(jsonData))) - - return nil -} diff --git a/api/http/proxy/secrets.go b/api/http/proxy/secrets.go deleted file mode 100644 index 67af61936..000000000 --- a/api/http/proxy/secrets.go +++ /dev/null @@ -1,107 +0,0 @@ -package proxy - -import ( - "net/http" - - "github.com/portainer/portainer/api" -) - -const ( - // ErrDockerSecretIdentifierNotFound defines an error raised when Portainer is unable to find a secret identifier - ErrDockerSecretIdentifierNotFound = portainer.Error("Docker secret identifier not found") - secretIdentifier = "ID" -) - -// secretListOperation extracts the response as a JSON object, loop through the secrets array -// decorate and/or filter the secrets based on resource controls before rewriting the response -func secretListOperation(response *http.Response, executor *operationExecutor) error { - var err error - - // SecretList response is a JSON array - // https://docs.docker.com/engine/api/v1.28/#operation/SecretList - responseArray, err := getResponseAsJSONArray(response) - if err != nil { - return err - } - - if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { - responseArray, err = decorateSecretList(responseArray, executor.operationContext.resourceControls) - } else { - responseArray, err = filterSecretList(responseArray, executor.operationContext) - } - if err != nil { - return err - } - - return rewriteResponse(response, responseArray, http.StatusOK) -} - -// secretInspectOperation extracts the response as a JSON object, verify that the user -// has access to the secret based on resource control (check are done based on the secretID and optional Swarm service ID) -// and either rewrite an access denied response or a decorated secret. -func secretInspectOperation(response *http.Response, executor *operationExecutor) error { - // SecretInspect response is a JSON object - // https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect - responseObject, err := getResponseAsJSONOBject(response) - if err != nil { - return err - } - - if responseObject[secretIdentifier] == nil { - return ErrDockerSecretIdentifierNotFound - } - - secretID := responseObject[secretIdentifier].(string) - responseObject, access := applyResourceAccessControl(responseObject, secretID, executor.operationContext) - if !access { - return rewriteAccessDeniedResponse(response) - } - - return rewriteResponse(response, responseObject, http.StatusOK) -} - -// decorateSecretList loops through all secrets and decorates any secret with an existing resource control. -// Resource controls checks are based on: resource identifier. -// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList -func decorateSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) { - decoratedSecretData := make([]interface{}, 0) - - for _, secret := range secretData { - - secretObject := secret.(map[string]interface{}) - if secretObject[secretIdentifier] == nil { - return nil, ErrDockerSecretIdentifierNotFound - } - - secretID := secretObject[secretIdentifier].(string) - secretObject = decorateResourceWithAccessControl(secretObject, secretID, resourceControls) - - decoratedSecretData = append(decoratedSecretData, secretObject) - } - - return decoratedSecretData, nil -} - -// filterSecretList loops through all secrets and filters public secrets (no associated resource control) -// as well as authorized secrets (access granted to the user based on existing resource control). -// Authorized secrets are decorated during the process. -// Resource controls checks are based on: resource identifier. -// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList -func filterSecretList(secretData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { - filteredSecretData := make([]interface{}, 0) - - for _, secret := range secretData { - secretObject := secret.(map[string]interface{}) - if secretObject[secretIdentifier] == nil { - return nil, ErrDockerSecretIdentifierNotFound - } - - secretID := secretObject[secretIdentifier].(string) - secretObject, access := applyResourceAccessControl(secretObject, secretID, context) - if access { - filteredSecretData = append(filteredSecretData, secretObject) - } - } - - return filteredSecretData, nil -} diff --git a/api/http/proxy/services.go b/api/http/proxy/services.go deleted file mode 100644 index b14b50a47..000000000 --- a/api/http/proxy/services.go +++ /dev/null @@ -1,143 +0,0 @@ -package proxy - -import ( - "net/http" - - "github.com/portainer/portainer/api" -) - -const ( - // ErrDockerServiceIdentifierNotFound defines an error raised when Portainer is unable to find a service identifier - ErrDockerServiceIdentifierNotFound = portainer.Error("Docker service identifier not found") - serviceIdentifier = "ID" - serviceLabelForStackIdentifier = "com.docker.stack.namespace" -) - -// serviceListOperation extracts the response as a JSON array, loop through the service array -// decorate and/or filter the services based on resource controls before rewriting the response -func serviceListOperation(response *http.Response, executor *operationExecutor) error { - var err error - // ServiceList response is a JSON array - // https://docs.docker.com/engine/api/v1.28/#operation/ServiceList - responseArray, err := getResponseAsJSONArray(response) - if err != nil { - return err - } - - if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { - responseArray, err = decorateServiceList(responseArray, executor.operationContext.resourceControls) - } else { - responseArray, err = filterServiceList(responseArray, executor.operationContext) - } - if err != nil { - return err - } - - return rewriteResponse(response, responseArray, http.StatusOK) -} - -// serviceInspectOperation extracts the response as a JSON object, verify that the user -// has access to the service based on resource control and either rewrite an access denied response -// or a decorated service. -func serviceInspectOperation(response *http.Response, executor *operationExecutor) error { - // ServiceInspect response is a JSON object - // https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect - responseObject, err := getResponseAsJSONOBject(response) - if err != nil { - return err - } - - if responseObject[serviceIdentifier] == nil { - return ErrDockerServiceIdentifierNotFound - } - - serviceID := responseObject[serviceIdentifier].(string) - responseObject, access := applyResourceAccessControl(responseObject, serviceID, executor.operationContext) - if access { - return rewriteResponse(response, responseObject, http.StatusOK) - } - - serviceLabels := extractServiceLabelsFromServiceInspectObject(responseObject) - responseObject, access = applyResourceAccessControlFromLabel(serviceLabels, responseObject, serviceLabelForStackIdentifier, executor.operationContext) - if access { - return rewriteResponse(response, responseObject, http.StatusOK) - } - - return rewriteAccessDeniedResponse(response) -} - -// extractServiceLabelsFromServiceInspectObject retrieve the Labels of the service if present. -// Service schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect -func extractServiceLabelsFromServiceInspectObject(responseObject map[string]interface{}) map[string]interface{} { - // Labels are stored under Spec.Labels - serviceSpecObject := extractJSONField(responseObject, "Spec") - if serviceSpecObject != nil { - return extractJSONField(serviceSpecObject, "Labels") - } - return nil -} - -// extractServiceLabelsFromServiceListObject retrieve the Labels of the service if present. -// Service schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList -func extractServiceLabelsFromServiceListObject(responseObject map[string]interface{}) map[string]interface{} { - // Labels are stored under Spec.Labels - serviceSpecObject := extractJSONField(responseObject, "Spec") - if serviceSpecObject != nil { - return extractJSONField(serviceSpecObject, "Labels") - } - return nil -} - -// decorateServiceList loops through all services and decorates any service with an existing resource control. -// Resource controls checks are based on: resource identifier, stack identifier (from label). -// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList -func decorateServiceList(serviceData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) { - decoratedServiceData := make([]interface{}, 0) - - for _, service := range serviceData { - - serviceObject := service.(map[string]interface{}) - if serviceObject[serviceIdentifier] == nil { - return nil, ErrDockerServiceIdentifierNotFound - } - - serviceID := serviceObject[serviceIdentifier].(string) - serviceObject = decorateResourceWithAccessControl(serviceObject, serviceID, resourceControls) - - serviceLabels := extractServiceLabelsFromServiceListObject(serviceObject) - serviceObject = decorateResourceWithAccessControlFromLabel(serviceLabels, serviceObject, serviceLabelForStackIdentifier, resourceControls) - - decoratedServiceData = append(decoratedServiceData, serviceObject) - } - - return decoratedServiceData, nil -} - -// filterServiceList loops through all services and filters public services (no associated resource control) -// as well as authorized services (access granted to the user based on existing resource control). -// Authorized services are decorated during the process. -// Resource controls checks are based on: resource identifier, stack identifier (from label). -// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList -func filterServiceList(serviceData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { - filteredServiceData := make([]interface{}, 0) - - for _, service := range serviceData { - serviceObject := service.(map[string]interface{}) - if serviceObject[serviceIdentifier] == nil { - return nil, ErrDockerServiceIdentifierNotFound - } - - serviceID := serviceObject[serviceIdentifier].(string) - serviceObject, access := applyResourceAccessControl(serviceObject, serviceID, context) - if !access { - serviceLabels := extractServiceLabelsFromServiceListObject(serviceObject) - serviceObject, access = applyResourceAccessControlFromLabel(serviceLabels, serviceObject, serviceLabelForStackIdentifier, context) - } - - if access { - filteredServiceData = append(filteredServiceData, serviceObject) - } - } - - return filteredServiceData, nil -} diff --git a/api/http/proxy/tasks.go b/api/http/proxy/tasks.go deleted file mode 100644 index 1da630ff7..000000000 --- a/api/http/proxy/tasks.go +++ /dev/null @@ -1,79 +0,0 @@ -package proxy - -import ( - "net/http" - - "github.com/portainer/portainer/api" -) - -const ( - // ErrDockerTaskServiceIdentifierNotFound defines an error raised when Portainer is unable to find the service identifier associated to a task - ErrDockerTaskServiceIdentifierNotFound = portainer.Error("Docker task service identifier not found") - taskServiceIdentifier = "ServiceID" - taskLabelForStackIdentifier = "com.docker.stack.namespace" -) - -// taskListOperation extracts the response as a JSON object, loop through the tasks array -// and filter the tasks based on resource controls before rewriting the response -func taskListOperation(response *http.Response, executor *operationExecutor) error { - var err error - - // TaskList response is a JSON array - // https://docs.docker.com/engine/api/v1.28/#operation/TaskList - responseArray, err := getResponseAsJSONArray(response) - if err != nil { - return err - } - - if !executor.operationContext.isAdmin && !executor.operationContext.endpointResourceAccess { - responseArray, err = filterTaskList(responseArray, executor.operationContext) - if err != nil { - return err - } - } - - return rewriteResponse(response, responseArray, http.StatusOK) -} - -// extractTaskLabelsFromTaskListObject retrieve the Labels of the task if present. -// Task schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList -func extractTaskLabelsFromTaskListObject(responseObject map[string]interface{}) map[string]interface{} { - // Labels are stored under Spec.ContainerSpec.Labels - taskSpecObject := extractJSONField(responseObject, "Spec") - if taskSpecObject != nil { - containerSpecObject := extractJSONField(taskSpecObject, "ContainerSpec") - if containerSpecObject != nil { - return extractJSONField(containerSpecObject, "Labels") - } - } - return nil -} - -// filterTaskList loops through all tasks and filters public tasks (no associated resource control) -// as well as authorized tasks (access granted to the user based on existing resource control). -// Resource controls checks are based on: service identifier, stack identifier (from label). -// Task object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList -// any resource control giving access to the user based on the associated service identifier. -func filterTaskList(taskData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { - filteredTaskData := make([]interface{}, 0) - - for _, task := range taskData { - taskObject := task.(map[string]interface{}) - if taskObject[taskServiceIdentifier] == nil { - return nil, ErrDockerTaskServiceIdentifierNotFound - } - - serviceID := taskObject[taskServiceIdentifier].(string) - taskObject, access := applyResourceAccessControl(taskObject, serviceID, context) - if !access { - taskLabels := extractTaskLabelsFromTaskListObject(taskObject) - taskObject, access = applyResourceAccessControlFromLabel(taskLabels, taskObject, taskLabelForStackIdentifier, context) - } - - if access { - filteredTaskData = append(filteredTaskData, taskObject) - } - } - - return filteredTaskData, nil -} diff --git a/api/http/proxy/volumes.go b/api/http/proxy/volumes.go deleted file mode 100644 index 59099d9ed..000000000 --- a/api/http/proxy/volumes.go +++ /dev/null @@ -1,144 +0,0 @@ -package proxy - -import ( - "net/http" - - "github.com/portainer/portainer/api" -) - -const ( - // ErrDockerVolumeIdentifierNotFound defines an error raised when Portainer is unable to find a volume identifier - ErrDockerVolumeIdentifierNotFound = portainer.Error("Docker volume identifier not found") - volumeIdentifier = "Name" - volumeLabelForStackIdentifier = "com.docker.stack.namespace" -) - -// volumeListOperation extracts the response as a JSON object, loop through the volume array -// decorate and/or filter the volumes based on resource controls before rewriting the response -func volumeListOperation(response *http.Response, executor *operationExecutor) error { - var err error - // VolumeList response is a JSON object - // https://docs.docker.com/engine/api/v1.28/#operation/VolumeList - responseObject, err := getResponseAsJSONOBject(response) - if err != nil { - return err - } - - // The "Volumes" field contains the list of volumes as an array of JSON objects - // Response schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList - if responseObject["Volumes"] != nil { - volumeData := responseObject["Volumes"].([]interface{}) - - if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { - volumeData, err = decorateVolumeList(volumeData, executor.operationContext.resourceControls) - } else { - volumeData, err = filterVolumeList(volumeData, executor.operationContext) - } - if err != nil { - return err - } - - // Overwrite the original volume list - responseObject["Volumes"] = volumeData - } - - return rewriteResponse(response, responseObject, http.StatusOK) -} - -// volumeInspectOperation extracts the response as a JSON object, verify that the user -// has access to the volume based on any existing resource control and either rewrite an access denied response -// or a decorated volume. -func volumeInspectOperation(response *http.Response, executor *operationExecutor) error { - // VolumeInspect response is a JSON object - // https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect - responseObject, err := getResponseAsJSONOBject(response) - if err != nil { - return err - } - - if responseObject[volumeIdentifier] == nil { - return ErrDockerVolumeIdentifierNotFound - } - - volumeID := responseObject[volumeIdentifier].(string) - responseObject, access := applyResourceAccessControl(responseObject, volumeID, executor.operationContext) - if access { - return rewriteResponse(response, responseObject, http.StatusOK) - } - - volumeLabels := extractVolumeLabelsFromVolumeInspectObject(responseObject) - responseObject, access = applyResourceAccessControlFromLabel(volumeLabels, responseObject, volumeLabelForStackIdentifier, executor.operationContext) - if access { - return rewriteResponse(response, responseObject, http.StatusOK) - } - - return rewriteAccessDeniedResponse(response) -} - -// extractVolumeLabelsFromVolumeInspectObject retrieve the Labels of the volume if present. -// Volume schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect -func extractVolumeLabelsFromVolumeInspectObject(responseObject map[string]interface{}) map[string]interface{} { - // Labels are stored under Labels - return extractJSONField(responseObject, "Labels") -} - -// extractVolumeLabelsFromVolumeListObject retrieve the Labels of the volume if present. -// Volume schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList -func extractVolumeLabelsFromVolumeListObject(responseObject map[string]interface{}) map[string]interface{} { - // Labels are stored under Labels - return extractJSONField(responseObject, "Labels") -} - -// decorateVolumeList loops through all volumes and decorates any volume with an existing resource control. -// Resource controls checks are based on: resource identifier, stack identifier (from label). -// Volume object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList -func decorateVolumeList(volumeData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) { - decoratedVolumeData := make([]interface{}, 0) - - for _, volume := range volumeData { - - volumeObject := volume.(map[string]interface{}) - if volumeObject[volumeIdentifier] == nil { - return nil, ErrDockerVolumeIdentifierNotFound - } - - volumeID := volumeObject[volumeIdentifier].(string) - volumeObject = decorateResourceWithAccessControl(volumeObject, volumeID, resourceControls) - - volumeLabels := extractVolumeLabelsFromVolumeListObject(volumeObject) - volumeObject = decorateResourceWithAccessControlFromLabel(volumeLabels, volumeObject, volumeLabelForStackIdentifier, resourceControls) - - decoratedVolumeData = append(decoratedVolumeData, volumeObject) - } - - return decoratedVolumeData, nil -} - -// filterVolumeList loops through all volumes and filters public volumes (no associated resource control) -// as well as authorized volumes (access granted to the user based on existing resource control). -// Authorized volumes are decorated during the process. -// Resource controls checks are based on: resource identifier, stack identifier (from label). -// Volume object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList -func filterVolumeList(volumeData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { - filteredVolumeData := make([]interface{}, 0) - - for _, volume := range volumeData { - volumeObject := volume.(map[string]interface{}) - if volumeObject[volumeIdentifier] == nil { - return nil, ErrDockerVolumeIdentifierNotFound - } - - volumeID := volumeObject[volumeIdentifier].(string) - volumeObject, access := applyResourceAccessControl(volumeObject, volumeID, context) - if !access { - volumeLabels := extractVolumeLabelsFromVolumeListObject(volumeObject) - volumeObject, access = applyResourceAccessControlFromLabel(volumeLabels, volumeObject, volumeLabelForStackIdentifier, context) - } - - if access { - filteredVolumeData = append(filteredVolumeData, volumeObject) - } - } - - return filteredVolumeData, nil -} diff --git a/api/http/security/authorization.go b/api/http/security/authorization.go index d2a0550c6..2ba3e8743 100644 --- a/api/http/security/authorization.go +++ b/api/http/security/authorization.go @@ -4,40 +4,6 @@ import ( "github.com/portainer/portainer/api" ) -// AuthorizedResourceControlDeletion ensure that the user can delete a resource control object. -// A non-administrator user cannot delete a resource control where: -// * the Public flag is false -// * he is not one of the users in the user accesses -// * he is not a member of any team within the team accesses -func AuthorizedResourceControlDeletion(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool { - if context.IsAdmin || resourceControl.Public { - return true - } - - userAccessesCount := len(resourceControl.UserAccesses) - teamAccessesCount := len(resourceControl.TeamAccesses) - - if teamAccessesCount > 0 { - for _, access := range resourceControl.TeamAccesses { - for _, membership := range context.UserMemberships { - if membership.TeamID == access.TeamID { - return true - } - } - } - } - - if userAccessesCount > 0 { - for _, access := range resourceControl.UserAccesses { - if access.UserID == context.UserID { - return true - } - } - } - - return false -} - // AuthorizedResourceControlAccess checks whether the user can alter an existing resource control. func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool { if context.IsAdmin || resourceControl.Public { @@ -62,30 +28,22 @@ func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl, } // AuthorizedResourceControlUpdate ensure that the user can update a resource control object. -// It reuses the creation restrictions and adds extra checks. -// A non-administrator user cannot update a resource control where: -// * he wants to put one or more user in the user accesses -func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool { - userAccessesCount := len(resourceControl.UserAccesses) - if !context.IsAdmin && userAccessesCount > 0 { - return false - } - - return AuthorizedResourceControlCreation(resourceControl, context) -} - -// AuthorizedResourceControlCreation ensure that the user can create a resource control object. // A non-administrator user cannot create a resource control where: // * the Public flag is set false +// * the AdministatorsOnly flag is set to true // * he wants to create a resource control without any user/team accesses // * he wants to add more than one user in the user accesses // * he wants to add a user in the user accesses that is not corresponding to its id // * he wants to add a team he is not a member of -func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool { +func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool { if context.IsAdmin || resourceControl.Public { return true } + if resourceControl.AdministratorsOnly { + return false + } + userAccessesCount := len(resourceControl.UserAccesses) teamAccessesCount := len(resourceControl.TeamAccesses) @@ -133,15 +91,6 @@ func AuthorizedTeamManagement(teamID portainer.TeamID, context *RestrictedReques return false } -// AuthorizedUserManagement ensure that access to the management of the specified user is granted. -// It will check if the user is either administrator or the owner of the user account. -func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedRequestContext) bool { - if context.IsAdmin || context.UserID == userID { - return true - } - return false -} - // authorizedEndpointAccess ensure that the user can access the specified endpoint. // It will check if the user is part of the authorized users or part of a team that is // listed in the authorized teams of the endpoint and the associated group. diff --git a/api/http/server.go b/api/http/server.go index 40c11ac2e..90a7abd68 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -87,6 +87,7 @@ func (server *Server) Start() error { proxyManagerParameters := &proxy.ManagerParams{ ResourceControlService: server.ResourceControlService, UserService: server.UserService, + TeamService: server.TeamService, TeamMembershipService: server.TeamMembershipService, SettingsService: server.SettingsService, RegistryService: server.RegistryService, @@ -94,6 +95,7 @@ func (server *Server) Start() error { SignatureService: server.SignatureService, ReverseTunnelService: server.ReverseTunnelService, ExtensionService: server.ExtensionService, + DockerClientFactory: server.DockerClientFactory, } proxyManager := proxy.NewManager(proxyManagerParameters) @@ -214,6 +216,8 @@ func (server *Server) Start() error { stackHandler.RegistryService = server.RegistryService stackHandler.DockerHubService = server.DockerHubService stackHandler.SettingsService = server.SettingsService + stackHandler.UserService = server.UserService + stackHandler.ExtensionService = server.ExtensionService var tagHandler = tags.NewHandler(requestBouncer) tagHandler.TagService = server.TagService diff --git a/api/libcompose/compose_stack.go b/api/libcompose/compose_stack.go index 01de1cd20..30322d7d8 100644 --- a/api/libcompose/compose_stack.go +++ b/api/libcompose/compose_stack.go @@ -16,6 +16,10 @@ import ( "github.com/portainer/portainer/api" ) +const ( + dockerClientVersion = "1.24" +) + // ComposeStackManager represents a service for managing compose stacks. type ComposeStackManager struct { dataPath string @@ -40,7 +44,7 @@ func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) ( clientOpts := client.Options{ Host: endpointURL, - APIVersion: portainer.SupportedDockerAPIVersion, + APIVersion: dockerClientVersion, } if endpoint.TLSConfig.TLS { diff --git a/api/portainer.go b/api/portainer.go index 078fde6ce..761e60d7e 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -175,14 +175,15 @@ type ( // Stack represents a Docker stack created via docker stack deploy Stack struct { - ID StackID `json:"Id"` - Name string `json:"Name"` - Type StackType `json:"Type"` - EndpointID EndpointID `json:"EndpointId"` - SwarmID string `json:"SwarmId"` - EntryPoint string `json:"EntryPoint"` - Env []Pair `json:"Env"` - ProjectPath string + ID StackID `json:"Id"` + Name string `json:"Name"` + Type StackType `json:"Type"` + EndpointID EndpointID `json:"EndpointId"` + SwarmID string `json:"SwarmId"` + EntryPoint string `json:"EntryPoint"` + Env []Pair `json:"Env"` + ResourceControl *ResourceControl `json:"ResourceControl"` + ProjectPath string } // RegistryID represents a registry identifier @@ -446,21 +447,20 @@ type ( // ResourceControl represent a reference to a Docker resource with specific access controls ResourceControl struct { - ID ResourceControlID `json:"Id"` - ResourceID string `json:"ResourceId"` - SubResourceIDs []string `json:"SubResourceIds"` - Type ResourceControlType `json:"Type"` - UserAccesses []UserResourceAccess `json:"UserAccesses"` - TeamAccesses []TeamResourceAccess `json:"TeamAccesses"` - Public bool `json:"Public"` + ID ResourceControlID `json:"Id"` + ResourceID string `json:"ResourceId"` + SubResourceIDs []string `json:"SubResourceIds"` + Type ResourceControlType `json:"Type"` + UserAccesses []UserResourceAccess `json:"UserAccesses"` + TeamAccesses []TeamResourceAccess `json:"TeamAccesses"` + Public bool `json:"Public"` + AdministratorsOnly bool `json:"AdministratorsOnly"` + System bool `json:"System"` // Deprecated fields // Deprecated in DBVersion == 2 OwnerID UserID `json:"OwnerId,omitempty"` AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"` - - // Deprecated in DBVersion == 14 - AdministratorsOnly bool `json:"AdministratorsOnly,omitempty"` } // ResourceControlType represents the type of resource associated to the resource control (volume, container, service...) @@ -749,7 +749,7 @@ type ( // ResourceControlService represents a service for managing resource control data ResourceControlService interface { ResourceControl(ID ResourceControlID) (*ResourceControl, error) - ResourceControlByResourceID(resourceID string) (*ResourceControl, error) + ResourceControlByResourceIDAndType(resourceID string, resourceType ResourceControlType) (*ResourceControl, error) ResourceControls() ([]ResourceControl, error) CreateResourceControl(rc *ResourceControl) error UpdateResourceControl(ID ResourceControlID, resourceControl *ResourceControl) error @@ -912,7 +912,7 @@ const ( // APIVersion is the version number of the Portainer API APIVersion = "1.23.0-dev" // DBVersion is the version number of the Portainer database - DBVersion = 21 + DBVersion = 22 // AssetsServerURL represents the URL of the Portainer asset server AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved @@ -936,8 +936,6 @@ const ( // PortainerAgentSignatureMessage represents the message used to create a digital signature // to be used when communicating with an agent PortainerAgentSignatureMessage = "Portainer-App" - // SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer - SupportedDockerAPIVersion = "1.24" // ExtensionServer represents the server used by Portainer to communicate with extensions ExtensionServer = "localhost" // DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance diff --git a/api/swagger.yaml b/api/swagger.yaml index 54f437420..584648175 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1235,7 +1235,7 @@ paths: summary: "Create a new resource control" description: | Create a new resource control to restrict access to a Docker resource. - **Access policy**: restricted + **Access policy**: administrator operationId: "ResourceControlCreate" consumes: - "application/json" @@ -1343,7 +1343,7 @@ paths: summary: "Remove a resource control" description: | Remove a resource control. - **Access policy**: restricted + **Access policy**: administrator operationId: "ResourceControlDelete" security: - jwt: [] diff --git a/app/docker/components/datatables/configs-datatable/configsDatatable.html b/app/docker/components/datatables/configs-datatable/configsDatatable.html index 31fd0cc67..d6a1e386d 100644 --- a/app/docker/components/datatables/configs-datatable/configsDatatable.html +++ b/app/docker/components/datatables/configs-datatable/configsDatatable.html @@ -101,7 +101,7 @@ - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }} + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }} diff --git a/app/docker/components/datatables/containers-datatable/containersDatatable.html b/app/docker/components/datatables/containers-datatable/containersDatatable.html index fb2a24cd8..b8bdc3a4b 100644 --- a/app/docker/components/datatables/containers-datatable/containersDatatable.html +++ b/app/docker/components/datatables/containers-datatable/containersDatatable.html @@ -272,7 +272,7 @@ - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }} + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }} diff --git a/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html b/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html index 691569467..7d6e4ef65 100644 --- a/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html +++ b/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.html @@ -9,6 +9,7 @@ {{ item.Name | truncate:40 }} {{ item.Name | truncate:40 }} + System {{ item.StackName ? item.StackName : '-' }} {{ item.Scope }} @@ -22,6 +23,6 @@ - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }} + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = RCO.ADMINISTRATORS }} \ No newline at end of file diff --git a/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js b/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js index 90dc60c57..41cabc224 100644 --- a/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js +++ b/app/docker/components/datatables/networks-datatable/network-row-content/networkRowContent.js @@ -1,3 +1,5 @@ +import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership'; + angular.module('portainer.docker') .directive('networkRowContent', [function networkRowContent() { var directive = { @@ -9,6 +11,9 @@ angular.module('portainer.docker') parentCtrl: '<', allowCheckbox: '<', allowExpand: '<' + }, + controller: ($scope) => { + $scope.RCO = RCO; } }; return directive; diff --git a/app/docker/components/datatables/networks-datatable/networksDatatableController.js b/app/docker/components/datatables/networks-datatable/networksDatatableController.js index 7550111b4..eefb9d153 100644 --- a/app/docker/components/datatables/networks-datatable/networksDatatableController.js +++ b/app/docker/components/datatables/networks-datatable/networksDatatableController.js @@ -1,13 +1,13 @@ import _ from 'lodash-es'; angular.module('portainer.docker') - .controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS', 'DatatableService', - function ($scope, $controller, PREDEFINED_NETWORKS, DatatableService) { + .controller('NetworksDatatableController', ['$scope', '$controller', 'NetworkHelper', 'DatatableService', + function ($scope, $controller, NetworkHelper, DatatableService) { angular.extend(this, $controller('GenericDatatableController', {$scope: $scope})); this.disableRemove = function(item) { - return PREDEFINED_NETWORKS.includes(item.Name); + return NetworkHelper.isSystemNetwork(item); }; this.state = Object.assign(this.state, { @@ -15,7 +15,7 @@ angular.module('portainer.docker') }) /** - * Do not allow PREDEFINED_NETWORKS to be selected + * Do not allow system networks to be selected */ this.allowSelection = function(item) { return !this.disableRemove(item); diff --git a/app/docker/components/datatables/secrets-datatable/secretsDatatable.html b/app/docker/components/datatables/secrets-datatable/secretsDatatable.html index 4f9916f53..28e560a36 100644 --- a/app/docker/components/datatables/secrets-datatable/secretsDatatable.html +++ b/app/docker/components/datatables/secrets-datatable/secretsDatatable.html @@ -101,7 +101,7 @@ - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }} + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }} diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.html b/app/docker/components/datatables/services-datatable/servicesDatatable.html index 0952d091f..ce551f6c0 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatable.html +++ b/app/docker/components/datatables/services-datatable/servicesDatatable.html @@ -156,7 +156,7 @@ - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }} + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }} diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html index 1266ea5be..1c8f66f09 100644 --- a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html +++ b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html @@ -164,7 +164,7 @@ - {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }} + {{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }} diff --git a/app/docker/models/config.js b/app/docker/models/config.js index 8f1d83873..56301040c 100644 --- a/app/docker/models/config.js +++ b/app/docker/models/config.js @@ -1,4 +1,4 @@ -import { ResourceControlViewModel } from '../../portainer/models/resourceControl'; +import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; export function ConfigViewModel(data) { this.Id = data.ID; @@ -9,9 +9,7 @@ export function ConfigViewModel(data) { this.Labels = data.Spec.Labels; this.Data = atob(data.Spec.Data); - if (data.Portainer) { - if (data.Portainer.ResourceControl) { - this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl); - } + if (data.Portainer && data.Portainer.ResourceControl) { + this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl); } } diff --git a/app/docker/models/container.js b/app/docker/models/container.js index c23b7b891..c09ad7748 100644 --- a/app/docker/models/container.js +++ b/app/docker/models/container.js @@ -1,5 +1,5 @@ import _ from 'lodash-es'; -import { ResourceControlViewModel } from '../../portainer/models/resourceControl'; +import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; export function createStatus(statusText) { var status = _.toLower(statusText); @@ -103,9 +103,7 @@ export function ContainerDetailsViewModel(data) { this.Config = data.Config; this.HostConfig = data.HostConfig; this.Mounts = data.Mounts; - if (data.Portainer) { - if (data.Portainer.ResourceControl) { - this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl); - } + if (data.Portainer && data.Portainer.ResourceControl) { + this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl); } } diff --git a/app/docker/models/network.js b/app/docker/models/network.js index 35e419850..a629eb0d3 100644 --- a/app/docker/models/network.js +++ b/app/docker/models/network.js @@ -1,4 +1,4 @@ -import { ResourceControlViewModel } from "../../portainer/models/resourceControl"; +import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; export function NetworkViewModel(data) { this.Id = data.Id; diff --git a/app/docker/models/secret.js b/app/docker/models/secret.js index ccf098420..ecca5f077 100644 --- a/app/docker/models/secret.js +++ b/app/docker/models/secret.js @@ -1,4 +1,4 @@ -import { ResourceControlViewModel } from '../../portainer/models/resourceControl' +import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; export function SecretViewModel(data) { this.Id = data.ID; diff --git a/app/docker/models/service.js b/app/docker/models/service.js index 7bfd68d6c..84f58eed0 100644 --- a/app/docker/models/service.js +++ b/app/docker/models/service.js @@ -1,4 +1,4 @@ -import { ResourceControlViewModel } from '../../portainer/models/resourceControl'; +import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; export function ServiceViewModel(data, runningTasks, allTasks) { this.Model = data; diff --git a/app/docker/models/volume.js b/app/docker/models/volume.js index ae2ac1904..0de34bbc5 100644 --- a/app/docker/models/volume.js +++ b/app/docker/models/volume.js @@ -1,4 +1,4 @@ -import { ResourceControlViewModel } from "../../portainer/models/resourceControl"; +import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl'; export function VolumeViewModel(data) { this.Id = data.Name; diff --git a/app/docker/services/containerService.js b/app/docker/services/containerService.js index cc4cbea87..300a09f91 100644 --- a/app/docker/services/containerService.js +++ b/app/docker/services/containerService.js @@ -107,14 +107,14 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe service.createAndStartContainer = function(configuration) { var deferred = $q.defer(); - var containerID; + var container; service.createContainer(configuration) .then(function success(data) { - containerID = data.Id; - return service.startContainer(containerID); + container = data; + return service.startContainer(container.Id); }) .then(function success() { - deferred.resolve({ Id: containerID }); + deferred.resolve(container); }) .catch(function error(err) { deferred.reject(err); @@ -129,13 +129,9 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe .then(function success(data) { if (data.message) { deferred.reject({ msg: data.message, err: data.message }); + } else { + deferred.resolve(); } - if (container.ResourceControl && container.ResourceControl.Type === 1) { - return ResourceControlService.deleteResourceControl(container.ResourceControl.Id); - } - }) - .then(function success() { - deferred.resolve(); }) .catch(function error(err) { deferred.reject({ msg: 'Unable to remove container', err: err }); diff --git a/app/docker/services/serviceService.js b/app/docker/services/serviceService.js index d3db5f048..95f16d214 100644 --- a/app/docker/services/serviceService.js +++ b/app/docker/services/serviceService.js @@ -43,14 +43,13 @@ function ServiceServiceFactory($q, Service, ServiceHelper, TaskService, Resource var deferred = $q.defer(); Service.remove({id: service.Id}).$promise - .then(function success() { - if (service.ResourceControl && service.ResourceControl.Type === 2) { - return ResourceControlService.deleteResourceControl(service.ResourceControl.Id); + .then(function success(data) { + if (data.message) { + deferred.reject({ msg: data.message, err: data.message }); + } else { + deferred.resolve(); } }) - .then(function success() { - deferred.resolve(); - }) .catch(function error(err) { deferred.reject({ msg: 'Unable to remove service', err: err }); }); diff --git a/app/docker/services/volumeService.js b/app/docker/services/volumeService.js index 5f3aaf31a..1ad5cbb9c 100644 --- a/app/docker/services/volumeService.js +++ b/app/docker/services/volumeService.js @@ -1,7 +1,7 @@ import { VolumeViewModel } from '../models/volume'; angular.module('portainer.docker') -.factory('VolumeService', ['$q', 'Volume', 'VolumeHelper', 'ResourceControlService', function VolumeServiceFactory($q, Volume, VolumeHelper, ResourceControlService) { +.factory('VolumeService', ['$q', 'Volume', 'VolumeHelper', function VolumeServiceFactory($q, Volume, VolumeHelper) { 'use strict'; var service = {}; @@ -45,13 +45,9 @@ angular.module('portainer.docker') .then(function success(data) { if (data.message) { deferred.reject({ msg: data.message, err: data.message }); + } else { + deferred.resolve(); } - if (volume.ResourceControl && volume.ResourceControl.Type === 3) { - return ResourceControlService.deleteResourceControl(volume.ResourceControl.Id); - } - }) - .then(function success() { - deferred.resolve(); }) .catch(function error(err) { deferred.reject({ msg: 'Unable to remove volume', err: err }); diff --git a/app/docker/views/configs/create/createConfigController.js b/app/docker/views/configs/create/createConfigController.js index 8baebf698..316250c47 100644 --- a/app/docker/views/configs/create/createConfigController.js +++ b/app/docker/views/configs/create/createConfigController.js @@ -104,9 +104,9 @@ class CreateConfigController { } async createAsync() { - let accessControlData = this.formValues.AccessControlData; - let userDetails = this.Authentication.getUserDetails(); - let isAdmin = this.Authentication.isAdmin(); + const accessControlData = this.formValues.AccessControlData; + const userDetails = this.Authentication.getUserDetails(); + const isAdmin = this.Authentication.isAdmin(); if (this.formValues.ConfigContent === "") { this.state.formValidationError = "Config content must not be empty"; @@ -117,19 +117,13 @@ class CreateConfigController { return; } - let config = this.prepareConfiguration(); + const config = this.prepareConfiguration(); try { - let data = await this.ConfigService.create(config); - let configIdentifier = data.ID; - let userId = userDetails.ID; - await this.ResourceControlService.applyResourceControl( - "config", - configIdentifier, - userId, - accessControlData, - [] - ); + const data = await this.ConfigService.create(config); + const resourceControl = data.Portainer.ResourceControl; + const userId = userDetails.ID; + await this.ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); this.Notifications.success("Config successfully created"); this.$state.go("docker.configs", {}, { reload: true }); } catch (err) { diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index e8e48c083..a73b996d2 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -759,16 +759,14 @@ function ($q, $scope, $async, $state, $timeout, $transition$, $filter, Container } function applyResourceControl(newContainer) { - var containerIdentifier = newContainer.Id; - var userId = Authentication.getUserDetails().ID; + const userId = Authentication.getUserDetails().ID; + const resourceControl = newContainer.Portainer.ResourceControl; + const containerId = newContainer.Id; + const accessControlData = $scope.formValues.AccessControlData; - return $q.when(ResourceControlService.applyResourceControl( - 'container', - containerIdentifier, - userId, - $scope.formValues.AccessControlData, [] - )).then(function onApplyResourceControlSuccess() { - return containerIdentifier; + return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl) + .then(function onApplyResourceControlSuccess() { + return containerId; }); } diff --git a/app/docker/views/containers/edit/container.html b/app/docker/views/containers/edit/container.html index ea1e5fde7..5997f2351 100644 --- a/app/docker/views/containers/edit/container.html +++ b/app/docker/views/containers/edit/container.html @@ -14,7 +14,7 @@
- + diff --git a/app/docker/views/containers/edit/containerController.js b/app/docker/views/containers/edit/containerController.js index 12f7ce7ca..860c2ccc0 100644 --- a/app/docker/views/containers/edit/containerController.js +++ b/app/docker/views/containers/edit/containerController.js @@ -1,8 +1,8 @@ import moment from 'moment'; angular.module('portainer.docker') -.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Commit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService', 'HttpRequestHelper', -function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, ContainerService, ImageHelper, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService, HttpRequestHelper) { +.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Commit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'NetworkService', 'Notifications', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService', 'HttpRequestHelper', 'Authentication', +function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, ContainerService, ImageHelper, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService, HttpRequestHelper, Authentication) { $scope.activityTime = 0; $scope.portBindings = []; @@ -205,7 +205,7 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co .then(setMainNetworkAndCreateContainer) .then(connectContainerToOtherNetworks) .then(startContainerIfNeeded) - .then(createResourceControlIfNeeded) + .then(createResourceControl) .then(deleteOldContainer) .then(notifyAndChangeView) .catch(notifyOnError); @@ -276,19 +276,11 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co ); } - function createResourceControlIfNeeded(newContainer) { - if (!container.ResourceControl) { - return $q.when(); - } - var containerIdentifier = newContainer.Id; - var resourceControl = container.ResourceControl; - var users = resourceControl.UserAccesses.map(function(u) { - return u.UserId; - }); - var teams = resourceControl.TeamAccesses.map(function(t) { - return t.TeamId; - }); - return ResourceControlService.createResourceControl(resourceControl.Public, users, teams, containerIdentifier, 'container', []); + function createResourceControl(newContainer) { + const userId = Authentication.getUserDetails().ID; + const oldResourceControl = container.ResourceControl; + const newResourceControl = newContainer.Portainer.ResourceControl; + return ResourceControlService.duplicateResourceControl(userId, oldResourceControl, newResourceControl); } function notifyAndChangeView() { diff --git a/app/docker/views/networks/create/createNetworkController.js b/app/docker/views/networks/create/createNetworkController.js index 615c713ba..a592582a6 100644 --- a/app/docker/views/networks/create/createNetworkController.js +++ b/app/docker/views/networks/create/createNetworkController.js @@ -134,9 +134,10 @@ angular.module('portainer.docker') $scope.state.actionInProgress = true; NetworkService.create(context.networkConfiguration) .then(function success(data) { - var networkIdentifier = data.Id; - var userId = context.userDetails.ID; - return ResourceControlService.applyResourceControl('network', networkIdentifier, userId, context.accessControlData, []); + const userId = context.userDetails.ID; + const accessControlData = context.accessControlData; + const resourceControl = data.Portainer.ResourceControl; + return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); }) .then(function success() { Notifications.success('Network successfully created'); diff --git a/app/docker/views/networks/edit/network.html b/app/docker/views/networks/edit/network.html index 74d2078d0..b5ee95ce0 100644 --- a/app/docker/views/networks/edit/network.html +++ b/app/docker/views/networks/edit/network.html @@ -20,7 +20,7 @@ ID {{ network.Id }} - + @@ -55,7 +55,8 @@ ng-if="network && applicationState.application.authentication" resource-id="network.Id" resource-control="network.ResourceControl" - resource-type="'network'"> + resource-type="'network'" + disable-ownership-change="isSystemNetwork()"> diff --git a/app/docker/views/networks/edit/networkController.js b/app/docker/views/networks/edit/networkController.js index 27ec8787e..294c76c5e 100644 --- a/app/docker/views/networks/edit/networkController.js +++ b/app/docker/views/networks/edit/networkController.js @@ -1,8 +1,6 @@ angular.module('portainer.docker') -.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'Notifications', 'HttpRequestHelper', 'PREDEFINED_NETWORKS', -function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper, PREDEFINED_NETWORKS) { - - $scope.network = {}; +.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'Notifications', 'HttpRequestHelper', 'NetworkHelper', +function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper, NetworkHelper) { $scope.removeNetwork = function removeNetwork() { NetworkService.remove($transition$.params().id, $transition$.params().id) @@ -27,8 +25,12 @@ function ($scope, $state, $transition$, $filter, NetworkService, Container, Noti }); }; - $scope.allowRemove = function allowRemove(item) { - return !PREDEFINED_NETWORKS.includes(item.Name); + $scope.isSystemNetwork = function() { + return $scope.network && NetworkHelper.isSystemNetwork($scope.network); + } + + $scope.allowRemove = function() { + return !$scope.isSystemNetwork(); }; function filterContainersInNetwork(network, containers) { diff --git a/app/docker/views/secrets/create/createSecretController.js b/app/docker/views/secrets/create/createSecretController.js index f0fddc55a..b488ed37d 100644 --- a/app/docker/views/secrets/create/createSecretController.js +++ b/app/docker/views/secrets/create/createSecretController.js @@ -59,9 +59,9 @@ function ($scope, $state, Notifications, SecretService, LabelHelper, Authenticat $scope.create = function () { - var accessControlData = $scope.formValues.AccessControlData; - var userDetails = Authentication.getUserDetails(); - var isAdmin = Authentication.isAdmin(); + const accessControlData = $scope.formValues.AccessControlData; + const userDetails = Authentication.getUserDetails(); + const isAdmin = Authentication.isAdmin(); if (!validateForm(accessControlData, isAdmin)) { return; @@ -71,9 +71,9 @@ function ($scope, $state, Notifications, SecretService, LabelHelper, Authenticat var secretConfiguration = prepareConfiguration(); SecretService.create(secretConfiguration) .then(function success(data) { - var secretIdentifier = data.ID; - var userId = userDetails.ID; - return ResourceControlService.applyResourceControl('secret', secretIdentifier, userId, accessControlData, []); + const userId = userDetails.ID; + const resourceControl = data.Portainer.ResourceControl; + return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); }) .then(function success() { Notifications.success('Secret successfully created'); diff --git a/app/docker/views/services/create/createServiceController.js b/app/docker/views/services/create/createServiceController.js index d23e5bf77..17ed83506 100644 --- a/app/docker/views/services/create/createServiceController.js +++ b/app/docker/views/services/create/createServiceController.js @@ -432,15 +432,14 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : ''; HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails); - var serviceIdentifier; Service.create(config).$promise .then(function success(data) { - serviceIdentifier = data.ID; - return $q.when($scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceIdentifier, EndpointProvider.endpointID())); - }) - .then(function success() { - var userId = Authentication.getUserDetails().ID; - return ResourceControlService.applyResourceControl('service', serviceIdentifier, userId, accessControlData, []); + const serviceId = data.ID; + const resourceControl = data.Portainer.ResourceControl; + const userId = Authentication.getUserDetails().ID; + const rcPromise = ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); + const webhookPromise = $q.when($scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceId, EndpointProvider.endpointID())); + return $q.all([rcPromise, webhookPromise]); }) .then(function success() { Notifications.success('Service successfully created'); diff --git a/app/docker/views/volumes/create/createVolumeController.js b/app/docker/views/volumes/create/createVolumeController.js index 71335e3eb..fbf16f6a6 100644 --- a/app/docker/views/volumes/create/createVolumeController.js +++ b/app/docker/views/volumes/create/createVolumeController.js @@ -81,9 +81,9 @@ function ($q, $scope, $state, VolumeService, PluginService, ResourceControlServi $scope.state.actionInProgress = true; VolumeService.createVolume(volumeConfiguration) .then(function success(data) { - var volumeIdentifier = data.Id; - var userId = userDetails.ID; - return ResourceControlService.applyResourceControl('volume', volumeIdentifier, userId, accessControlData, []); + const userId = userDetails.ID; + const resourceControl = data.ResourceControl; + return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl); }) .then(function success() { Notifications.success('Volume successfully created'); diff --git a/app/portainer/components/accessControlForm/porAccessControlForm.html b/app/portainer/components/accessControlForm/porAccessControlForm.html index 9b62663fc..2dbc533c9 100644 --- a/app/portainer/components/accessControlForm/porAccessControlForm.html +++ b/app/portainer/components/accessControlForm/porAccessControlForm.html @@ -71,7 +71,7 @@
-
+
-
+