mirror of https://github.com/portainer/portainer
feat(api): rewrite access control management in Docker (#3337)
* feat(api): decorate Docker resource creation response with resource control * fix(api): fix a potential resource control conflict between stacks/volumes * feat(api): generate a default private resource control instead of admin only * fix(api): fix default RC value * fix(api): update RC authorizations check to support admin only flag * refactor(api): relocate access control related methods * fix(api): fix a potential conflict when fetching RC from database * refactor(api): refactor access control logic * refactor(api): remove the concept of DecoratedStack * feat(api): automatically remove RC when removing a Docker resource * refactor(api): update filter resource methods documentation * refactor(api): update proxy package structure * refactor(api): renamed proxy/misc package * feat(api): re-introduce ResourceControlDelete operation as admin restricted * refactor(api): relocate default endpoint authorizations * feat(api): migrate RBAC data * feat(app): ResourceControl management refactor * fix(api): fix access control issue on stack deletion and automatically delete RC * fix(api): fix stack filtering * fix(api): fix UpdateResourceControl operation checks * refactor(api): introduce a NewTransport builder method * refactor(api): inject endpoint in Docker transport * refactor(api): introduce Docker client into Docker transport * refactor(api): refactor http/proxy package * feat(api): inspect a Docker resource labels during access control validation * fix(api): only apply automatic resource control creation on success response * fix(api): fix stack access control check * fix(api): use StatusCreated instead of StatusOK for automatic resource control creation * fix(app): resource control fixes * fix(api): fix an issue preventing administrator to inspect a resource with a RC * refactor(api): remove useless error return * refactor(api): document DecorateStacks function * fix(api): fix invalid resource control type for container deletion * feat(api): support Docker system networks * feat(api): update Swagger docs * refactor(api): rename transport variable * refactor(api): rename transport variable * feat(networks): add system tag for system networks * feat(api): add support for resource control labels * feat(api): upgrade to DBVersion 22 * refactor(api): refactor access control management in Docker proxy * refactor(api): re-implement docker proxy taskListOperation * refactor(api): review parameters declaration * refactor(api): remove extra blank line * refactor(api): review method comments * fix(api): fix invalid ServerAddress property and review method visibility * feat(api): update error message * feat(api): update restrictedVolumeBrowserOperation method * refactor(api): refactor method parameters * refactor(api): minor refactor * refactor(api): change Azure transport visibility * refactor(api): update struct documentation * refactor(api): update struct documentation * feat(api): review restrictedResourceOperation method * refactor(api): remove unused authorization methods * feat(api): apply RBAC when enabled on stack operations * fix(api): fix invalid data migration procedure for DBVersion = 22 * fix(app): RC duplicate on private resource * feat(api): change Docker API version logic for libcompose/client factory * fix(api): update access denied error message to be Docker API compliant * fix(api): update volume browsing authorizations data migration * fix(api): fix an issue with access control in multi-node agent Swarm clusterpull/3349/head
parent
198e92c734
commit
19d4db13be
|
@ -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
|
||||||
|
}
|
|
@ -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.
|
// DefaultPortainerAuthorizations returns the default Portainer authorizations used by non-admin users.
|
||||||
func DefaultPortainerAuthorizations() Authorizations {
|
func DefaultPortainerAuthorizations() Authorizations {
|
||||||
return map[Authorization]bool{
|
return map[Authorization]bool{
|
||||||
|
|
367
api/bolt/init.go
367
api/bolt/init.go
|
@ -32,141 +32,9 @@ func (store *Store) Init() error {
|
||||||
|
|
||||||
if len(roles) == 0 {
|
if len(roles) == 0 {
|
||||||
environmentAdministratorRole := &portainer.Role{
|
environmentAdministratorRole := &portainer.Role{
|
||||||
Name: "Endpoint administrator",
|
Name: "Endpoint administrator",
|
||||||
Description: "Full control of all resources in an endpoint",
|
Description: "Full control of all resources in an endpoint",
|
||||||
Authorizations: map[portainer.Authorization]bool{
|
Authorizations: portainer.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = store.RoleService.CreateRole(environmentAdministratorRole)
|
err = store.RoleService.CreateRole(environmentAdministratorRole)
|
||||||
|
@ -175,55 +43,9 @@ func (store *Store) Init() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
environmentReadOnlyUserRole := &portainer.Role{
|
environmentReadOnlyUserRole := &portainer.Role{
|
||||||
Name: "Helpdesk",
|
Name: "Helpdesk",
|
||||||
Description: "Read-only access of all resources in an endpoint",
|
Description: "Read-only access of all resources in an endpoint",
|
||||||
Authorizations: map[portainer.Authorization]bool{
|
Authorizations: portainer.DefaultEndpointAuthorizationsForHelpDeskRole(false),
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = store.RoleService.CreateRole(environmentReadOnlyUserRole)
|
err = store.RoleService.CreateRole(environmentReadOnlyUserRole)
|
||||||
|
@ -232,129 +54,9 @@ func (store *Store) Init() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
standardUserRole := &portainer.Role{
|
standardUserRole := &portainer.Role{
|
||||||
Name: "Standard user",
|
Name: "Standard user",
|
||||||
Description: "Full control of assigned resources in an endpoint",
|
Description: "Full control of assigned resources in an endpoint",
|
||||||
Authorizations: map[portainer.Authorization]bool{
|
Authorizations: portainer.DefaultEndpointAuthorizationsForStandardUserRole(false),
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = store.RoleService.CreateRole(standardUserRole)
|
err = store.RoleService.CreateRole(standardUserRole)
|
||||||
|
@ -363,54 +65,9 @@ func (store *Store) Init() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
readOnlyUserRole := &portainer.Role{
|
readOnlyUserRole := &portainer.Role{
|
||||||
Name: "Read-only user",
|
Name: "Read-only user",
|
||||||
Description: "Read-only access of assigned resources in an endpoint",
|
Description: "Read-only access of assigned resources in an endpoint",
|
||||||
Authorizations: map[portainer.Authorization]bool{
|
Authorizations: portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(false),
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = store.RoleService.CreateRole(readOnlyUserRole)
|
err = store.RoleService.CreateRole(readOnlyUserRole)
|
||||||
|
|
|
@ -1,15 +1,36 @@
|
||||||
package migrator
|
package migrator
|
||||||
|
|
||||||
import (
|
import portainer "github.com/portainer/portainer/api"
|
||||||
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()
|
legacyUsers, err := m.userService.Users()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settings, err := m.settingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, user := range legacyUsers {
|
for _, user := range legacyUsers {
|
||||||
user.PortainerAuthorizations = portainer.DefaultPortainerAuthorizations()
|
user.PortainerAuthorizations = portainer.DefaultPortainerAuthorizations()
|
||||||
err = m.userService.UpdateUser(user.ID, &user)
|
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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -288,8 +288,14 @@ func (m *Migrator) Migrate() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Portainer next
|
// Portainer next
|
||||||
if m.currentDBVersion < 21 {
|
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||||
err := m.updateUsersToDBVersion21()
|
if m.currentDBVersion < 22 {
|
||||||
|
err := m.updateResourceControlsToDBVersion22()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.updateUsersAndRolesToDBVersion22()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,9 +42,10 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
|
||||||
return &resourceControl, nil
|
return &resourceControl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
|
// ResourceControlByResourceIDAndType returns a ResourceControl object by checking if the resourceID is equal
|
||||||
// to the main ResourceID or in SubResourceIDs
|
// to the main ResourceID or in SubResourceIDs. It also performs a check on the resource type. Return nil
|
||||||
func (service *Service) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
|
// if no ResourceControl was found.
|
||||||
|
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||||
var resourceControl *portainer.ResourceControl
|
var resourceControl *portainer.ResourceControl
|
||||||
|
|
||||||
err := service.db.View(func(tx *bolt.Tx) error {
|
err := service.db.View(func(tx *bolt.Tx) error {
|
||||||
|
@ -58,7 +59,7 @@ func (service *Service) ResourceControlByResourceID(resourceID string) (*portain
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if rc.ResourceID == resourceID {
|
if rc.ResourceID == resourceID && rc.Type == resourceType {
|
||||||
resourceControl = &rc
|
resourceControl = &rc
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -71,10 +72,6 @@ func (service *Service) ResourceControlByResourceID(resourceID string) (*portain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if resourceControl == nil {
|
|
||||||
return portainer.ErrObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
const (
|
const (
|
||||||
unsupportedEnvironmentType = portainer.Error("Environment not supported")
|
unsupportedEnvironmentType = portainer.Error("Environment not supported")
|
||||||
defaultDockerRequestTimeout = 60
|
defaultDockerRequestTimeout = 60
|
||||||
|
dockerClientVersion = "1.40"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClientFactory is used to create Docker clients
|
// 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) {
|
func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||||
return client.NewClientWithOpts(
|
return client.NewClientWithOpts(
|
||||||
client.WithHost(endpoint.URL),
|
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(
|
return client.NewClientWithOpts(
|
||||||
client.WithHost(endpoint.URL),
|
client.WithHost(endpoint.URL),
|
||||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
client.WithVersion(dockerClientVersion),
|
||||||
client.WithHTTPClient(httpCli),
|
client.WithHTTPClient(httpCli),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -84,7 +85,7 @@ func createEdgeClient(endpoint *portainer.Endpoint, reverseTunnelService portain
|
||||||
|
|
||||||
return client.NewClientWithOpts(
|
return client.NewClientWithOpts(
|
||||||
client.WithHost(endpointURL),
|
client.WithHost(endpointURL),
|
||||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
client.WithVersion(dockerClientVersion),
|
||||||
client.WithHTTPClient(httpCli),
|
client.WithHTTPClient(httpCli),
|
||||||
client.WithHTTPHeaders(headers),
|
client.WithHTTPHeaders(headers),
|
||||||
)
|
)
|
||||||
|
@ -112,7 +113,7 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.
|
||||||
|
|
||||||
return client.NewClientWithOpts(
|
return client.NewClientWithOpts(
|
||||||
client.WithHost(endpoint.URL),
|
client.WithHost(endpoint.URL),
|
||||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
client.WithVersion(dockerClientVersion),
|
||||||
client.WithHTTPClient(httpCli),
|
client.WithHTTPClient(httpCli),
|
||||||
client.WithHTTPHeaders(headers),
|
client.WithHTTPHeaders(headers),
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,9 +29,9 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
proxy = handler.ProxyManager.GetProxy(endpoint)
|
proxy = handler.ProxyManager.GetEndpointProxy(endpoint)
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||||
|
|
||||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||||
handler.ProxyManager.DeleteProxy(endpoint)
|
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||||
|
|
||||||
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -55,9 +55,9 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
proxy = handler.ProxyManager.GetProxy(endpoint)
|
proxy = handler.ProxyManager.GetEndpointProxy(endpoint)
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
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 {
|
if len(endpoint.UserAccessPolicies) > 0 || len(endpoint.TeamAccessPolicies) > 0 {
|
||||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||||
|
|
|
@ -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 {
|
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
|
||||||
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/resource_controls",
|
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}",
|
h.Handle("/resource_controls/{id}",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlUpdate))).Methods(http.MethodPut)
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/resource_controls/{id}",
|
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
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package resourcecontrols
|
package resourcecontrols
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
@ -8,29 +9,33 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type resourceControlCreatePayload struct {
|
type resourceControlCreatePayload struct {
|
||||||
ResourceID string
|
ResourceID string
|
||||||
Type string
|
Type string
|
||||||
Public bool
|
Public bool
|
||||||
Users []int
|
AdministratorsOnly bool
|
||||||
Teams []int
|
Users []int
|
||||||
SubResourceIDs []string
|
Teams []int
|
||||||
|
SubResourceIDs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
|
func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
|
||||||
if govalidator.IsNull(payload.ResourceID) {
|
if govalidator.IsNull(payload.ResourceID) {
|
||||||
return portainer.Error("Invalid resource identifier")
|
return errors.New("invalid payload: invalid resource identifier")
|
||||||
}
|
}
|
||||||
|
|
||||||
if govalidator.IsNull(payload.Type) {
|
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 {
|
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public && !payload.AdministratorsOnly {
|
||||||
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or Public")
|
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
|
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}
|
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)
|
rc, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(payload.ResourceID, resourceControlType)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err}
|
||||||
}
|
}
|
||||||
if rc != nil {
|
if rc != nil {
|
||||||
|
@ -90,21 +95,13 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl := portainer.ResourceControl{
|
resourceControl := portainer.ResourceControl{
|
||||||
ResourceID: payload.ResourceID,
|
ResourceID: payload.ResourceID,
|
||||||
SubResourceIDs: payload.SubResourceIDs,
|
SubResourceIDs: payload.SubResourceIDs,
|
||||||
Type: resourceControlType,
|
Type: resourceControlType,
|
||||||
Public: payload.Public,
|
Public: payload.Public,
|
||||||
UserAccesses: userAccesses,
|
AdministratorsOnly: payload.AdministratorsOnly,
|
||||||
TeamAccesses: teamAccesses,
|
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}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.ResourceControlService.CreateResourceControl(&resourceControl)
|
err = handler.ResourceControlService.CreateResourceControl(&resourceControl)
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DELETE request on /api/resource_controls/:id
|
// 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}
|
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 {
|
if err == portainer.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a resource control with with the specified identifier inside the database", err}
|
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))
|
err = handler.ResourceControlService.DeleteResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the resource control from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the resource control from the database", err}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package resourcecontrols
|
package resourcecontrols
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
@ -11,14 +12,19 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type resourceControlUpdatePayload struct {
|
type resourceControlUpdatePayload struct {
|
||||||
Public bool
|
Public bool
|
||||||
Users []int
|
Users []int
|
||||||
Teams []int
|
Teams []int
|
||||||
|
AdministratorsOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error {
|
func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error {
|
||||||
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public {
|
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public && !payload.AdministratorsOnly {
|
||||||
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or Public")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -49,10 +55,11 @@ func (handler *Handler) resourceControlUpdate(w http.ResponseWriter, r *http.Req
|
||||||
}
|
}
|
||||||
|
|
||||||
if !security.AuthorizedResourceControlAccess(resourceControl, securityContext) {
|
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.Public = payload.Public
|
||||||
|
resourceControl.AdministratorsOnly = payload.AdministratorsOnly
|
||||||
|
|
||||||
var userAccesses = make([]portainer.UserResourceAccess, 0)
|
var userAccesses = make([]portainer.UserResourceAccess, 0)
|
||||||
for _, v := range payload.Users {
|
for _, v := range payload.Users {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
@ -32,7 +31,7 @@ func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) err
|
||||||
return nil
|
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
|
var payload composeStackFromFileContentPayload
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -86,7 +85,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
doCleanUp = false
|
||||||
return response.JSON(w, stack)
|
return handler.decorateStackResponse(w, stack, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type composeStackFromGitRepositoryPayload struct {
|
type composeStackFromGitRepositoryPayload struct {
|
||||||
|
@ -116,7 +115,7 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
|
||||||
return nil
|
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
|
var payload composeStackFromGitRepositoryPayload
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -180,7 +179,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
doCleanUp = false
|
||||||
return response.JSON(w, stack)
|
return handler.decorateStackResponse(w, stack, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type composeStackFromFileUploadPayload struct {
|
type composeStackFromFileUploadPayload struct {
|
||||||
|
@ -211,7 +210,7 @@ func (payload *composeStackFromFileUploadPayload) Validate(r *http.Request) erro
|
||||||
return nil
|
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{}
|
payload := &composeStackFromFileUploadPayload{}
|
||||||
err := payload.Validate(r)
|
err := payload.Validate(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -265,7 +264,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
doCleanUp = false
|
||||||
return response.JSON(w, stack)
|
return handler.decorateStackResponse(w, stack, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type composeStackDeploymentConfig struct {
|
type composeStackDeploymentConfig struct {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
@ -36,7 +35,7 @@ func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error
|
||||||
return nil
|
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
|
var payload swarmStackFromFileContentPayload
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -91,7 +90,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
doCleanUp = false
|
||||||
return response.JSON(w, stack)
|
return handler.decorateStackResponse(w, stack, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type swarmStackFromGitRepositoryPayload struct {
|
type swarmStackFromGitRepositoryPayload struct {
|
||||||
|
@ -125,7 +124,7 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
|
||||||
return nil
|
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
|
var payload swarmStackFromGitRepositoryPayload
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -190,7 +189,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
doCleanUp = false
|
||||||
return response.JSON(w, stack)
|
return handler.decorateStackResponse(w, stack, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type swarmStackFromFileUploadPayload struct {
|
type swarmStackFromFileUploadPayload struct {
|
||||||
|
@ -228,7 +227,7 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error
|
||||||
return nil
|
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{}
|
payload := &swarmStackFromFileUploadPayload{}
|
||||||
err := payload.Validate(r)
|
err := payload.Validate(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -283,7 +282,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
doCleanUp = false
|
||||||
return response.JSON(w, stack)
|
return handler.decorateStackResponse(w, stack, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type swarmStackDeploymentConfig struct {
|
type swarmStackDeploymentConfig struct {
|
||||||
|
|
|
@ -26,6 +26,8 @@ type Handler struct {
|
||||||
SwarmStackManager portainer.SwarmStackManager
|
SwarmStackManager portainer.SwarmStackManager
|
||||||
ComposeStackManager portainer.ComposeStackManager
|
ComposeStackManager portainer.ComposeStackManager
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
|
UserService portainer.UserService
|
||||||
|
ExtensionService portainer.ExtensionService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage stack operations.
|
// 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)
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackMigrate))).Methods(http.MethodPost)
|
||||||
return h
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -5,12 +5,13 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/compose/types"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
|
"github.com/docker/cli/cli/compose/types"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error {
|
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}
|
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) {
|
switch portainer.StackType(stackType) {
|
||||||
case portainer.DockerSwarmStack:
|
case portainer.DockerSwarmStack:
|
||||||
return handler.createSwarmStack(w, r, method, endpoint)
|
return handler.createSwarmStack(w, r, method, endpoint, tokenData.ID)
|
||||||
case portainer.DockerComposeStack:
|
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)}
|
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 {
|
switch method {
|
||||||
case "string":
|
case "string":
|
||||||
return handler.createComposeStackFromFileContent(w, r, endpoint)
|
return handler.createComposeStackFromFileContent(w, r, endpoint, userID)
|
||||||
case "repository":
|
case "repository":
|
||||||
return handler.createComposeStackFromGitRepository(w, r, endpoint)
|
return handler.createComposeStackFromGitRepository(w, r, endpoint, userID)
|
||||||
case "file":
|
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)}
|
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 {
|
switch method {
|
||||||
case "string":
|
case "string":
|
||||||
return handler.createSwarmStackFromFileContent(w, r, endpoint)
|
return handler.createSwarmStackFromFileContent(w, r, endpoint, userID)
|
||||||
case "repository":
|
case "repository":
|
||||||
return handler.createSwarmStackFromGitRepository(w, r, endpoint)
|
return handler.createSwarmStackFromGitRepository(w, r, endpoint, userID)
|
||||||
case "file":
|
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)}
|
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
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
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}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
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)
|
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}
|
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)
|
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove stack files from disk", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove stack files from disk", err}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"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}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if !securityContext.IsAdmin && resourceControl == nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||||
}
|
}
|
||||||
|
if !access {
|
||||||
if resourceControl != nil {
|
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||||
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}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
|
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"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}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
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}
|
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 {
|
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}
|
||||||
}
|
}
|
||||||
|
if !access {
|
||||||
extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
|
|
||||||
if !securityContext.IsAdmin && resourceControl == nil {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||||
}
|
}
|
||||||
|
|
||||||
if resourceControl != nil {
|
if resourceControl != nil {
|
||||||
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
stack.ResourceControl = resourceControl
|
||||||
extendedStack.ResourceControl = *resourceControl
|
|
||||||
} else {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, extendedStack)
|
return response.JSON(w, stack)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"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}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredStacks := proxy.FilterStacks(stacks, resourceControls, securityContext.IsAdmin,
|
stacks = portainer.DecorateStacks(stacks, resourceControls)
|
||||||
securityContext.UserID, securityContext.UserMemberships)
|
|
||||||
|
|
||||||
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 {
|
func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters) []portainer.Stack {
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"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}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
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
|
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"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}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
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)
|
updateError := handler.updateAndDeployStack(r, stack, endpoint)
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package proxy
|
package azure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -16,9 +16,7 @@ type (
|
||||||
expirationTime time.Time
|
expirationTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// AzureTransport represents a transport used when executing HTTP requests
|
Transport struct {
|
||||||
// against the Azure API.
|
|
||||||
AzureTransport struct {
|
|
||||||
credentials *portainer.AzureCredentials
|
credentials *portainer.AzureCredentials
|
||||||
client *client.HTTPClient
|
client *client.HTTPClient
|
||||||
token *azureAPIToken
|
token *azureAPIToken
|
||||||
|
@ -26,15 +24,27 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewAzureTransport returns a pointer to an AzureTransport instance.
|
// NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport
|
||||||
func NewAzureTransport(credentials *portainer.AzureCredentials) *AzureTransport {
|
// interface for proxying requests to the Azure API.
|
||||||
return &AzureTransport{
|
func NewTransport(credentials *portainer.AzureCredentials) *Transport {
|
||||||
|
return &Transport{
|
||||||
credentials: credentials,
|
credentials: credentials,
|
||||||
client: client.NewHTTPClient(),
|
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)
|
token, err := transport.client.ExecuteAzureAuthenticationRequest(transport.credentials)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -53,7 +63,7 @@ func (transport *AzureTransport) authenticate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (transport *AzureTransport) retrieveAuthenticationToken() error {
|
func (transport *Transport) retrieveAuthenticationToken() error {
|
||||||
transport.mutex.Lock()
|
transport.mutex.Lock()
|
||||||
defer transport.mutex.Unlock()
|
defer transport.mutex.Unlock()
|
||||||
|
|
||||||
|
@ -68,14 +78,3 @@ func (transport *AzureTransport) retrieveAuthenticationToken() error {
|
||||||
|
|
||||||
return nil
|
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)
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package proxy
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -1,10 +1,25 @@
|
||||||
package proxy
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"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 {
|
func createRegistryAuthenticationHeader(serverAddress string, accessContext *registryAccessContext) *registryAuthenticationHeader {
|
||||||
var authenticationHeader *registryAuthenticationHeader
|
var authenticationHeader *registryAuthenticationHeader
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
package proxy
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// swarmInspectOperation extracts the response as a JSON object and rewrites the response based
|
// 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 {
|
func swarmInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
// SwarmInspect response is a JSON object
|
// SwarmInspect response is a JSON object
|
||||||
// https://docs.docker.com/engine/api/v1.30/#operation/SwarmInspect
|
// https://docs.docker.com/engine/api/v1.30/#operation/SwarmInspect
|
||||||
responseObject, err := getResponseAsJSONOBject(response)
|
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -19,5 +21,5 @@ func swarmInspectOperation(response *http.Response, executor *operationExecutor)
|
||||||
delete(responseObject, "TLSInfo")
|
delete(responseObject, "TLSInfo")
|
||||||
}
|
}
|
||||||
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package proxy
|
package factory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +1,22 @@
|
||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/orcaman/concurrent-map"
|
"github.com/orcaman/concurrent-map"
|
||||||
"github.com/portainer/portainer/api"
|
"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
|
// TODO: contain code related to legacy extension management
|
||||||
|
|
||||||
var extensionPorts = map[portainer.ExtensionID]string{
|
|
||||||
portainer.RegistryManagementExtension: "7001",
|
|
||||||
portainer.OAuthAuthenticationExtension: "7002",
|
|
||||||
portainer.RBACExtension: "7003",
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Manager represents a service used to manage Docker proxies.
|
// Manager represents a service used to manage proxies to endpoints and extensions.
|
||||||
Manager struct {
|
Manager struct {
|
||||||
proxyFactory *proxyFactory
|
proxyFactory *factory.ProxyFactory
|
||||||
reverseTunnelService portainer.ReverseTunnelService
|
endpointProxies cmap.ConcurrentMap
|
||||||
proxies cmap.ConcurrentMap
|
|
||||||
extensionProxies cmap.ConcurrentMap
|
extensionProxies cmap.ConcurrentMap
|
||||||
legacyExtensionProxies cmap.ConcurrentMap
|
legacyExtensionProxies cmap.ConcurrentMap
|
||||||
}
|
}
|
||||||
|
@ -32,6 +25,7 @@ type (
|
||||||
ManagerParams struct {
|
ManagerParams struct {
|
||||||
ResourceControlService portainer.ResourceControlService
|
ResourceControlService portainer.ResourceControlService
|
||||||
UserService portainer.UserService
|
UserService portainer.UserService
|
||||||
|
TeamService portainer.TeamService
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
RegistryService portainer.RegistryService
|
RegistryService portainer.RegistryService
|
||||||
|
@ -39,54 +33,71 @@ type (
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
ReverseTunnelService portainer.ReverseTunnelService
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
ExtensionService portainer.ExtensionService
|
ExtensionService portainer.ExtensionService
|
||||||
|
DockerClientFactory *docker.ClientFactory
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewManager initializes a new proxy Service
|
// NewManager initializes a new proxy Service
|
||||||
func NewManager(parameters *ManagerParams) *Manager {
|
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{
|
return &Manager{
|
||||||
proxies: cmap.New(),
|
endpointProxies: cmap.New(),
|
||||||
extensionProxies: cmap.New(),
|
extensionProxies: cmap.New(),
|
||||||
legacyExtensionProxies: cmap.New(),
|
legacyExtensionProxies: cmap.New(),
|
||||||
proxyFactory: &proxyFactory{
|
proxyFactory: factory.NewProxyFactory(proxyFactoryParameters),
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProxy returns the proxy associated to a key
|
// CreateAndRegisterEndpointProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
|
||||||
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.
|
|
||||||
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
|
// 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) {
|
func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
proxy, err := manager.createProxy(endpoint)
|
proxy, err := manager.proxyFactory.NewEndpointProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.proxies.Set(string(endpoint.ID), proxy)
|
manager.endpointProxies.Set(string(endpoint.ID), proxy)
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteProxy deletes the proxy associated to a key
|
// GetEndpointProxy returns the proxy associated to a key
|
||||||
func (manager *Manager) DeleteProxy(endpoint *portainer.Endpoint) {
|
func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Handler {
|
||||||
manager.proxies.Remove(string(endpoint.ID))
|
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
|
// GetExtensionProxy returns an extension proxy associated to an extension identifier
|
||||||
|
@ -95,28 +106,13 @@ func (manager *Manager) GetExtensionProxy(extensionID portainer.ExtensionID) htt
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxy.(http.Handler)
|
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
|
// GetExtensionURL retrieves the URL of an extension running locally based on the extension port table
|
||||||
func (manager *Manager) GetExtensionURL(extensionID portainer.ExtensionID) string {
|
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
|
// 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)))
|
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
|
// GetLegacyExtensionProxy returns a legacy extension proxy associated to a key
|
||||||
func (manager *Manager) GetLegacyExtensionProxy(key string) http.Handler {
|
func (manager *Manager) GetLegacyExtensionProxy(key string) http.Handler {
|
||||||
proxy, ok := manager.legacyExtensionProxies.Get(key)
|
proxy, ok := manager.legacyExtensionProxies.Get(key)
|
||||||
|
@ -133,56 +140,6 @@ func (manager *Manager) GetLegacyExtensionProxy(key string) http.Handler {
|
||||||
return proxy.(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..
|
// 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) {
|
func (manager *Manager) CreateGitlabProxy(url string) (http.Handler, error) {
|
||||||
return newGitlabProxy(url)
|
return newGitlabProxy(url)
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -4,40 +4,6 @@ import (
|
||||||
"github.com/portainer/portainer/api"
|
"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.
|
// AuthorizedResourceControlAccess checks whether the user can alter an existing resource control.
|
||||||
func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||||
if context.IsAdmin || resourceControl.Public {
|
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.
|
// 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:
|
// A non-administrator user cannot create a resource control where:
|
||||||
// * the Public flag is set false
|
// * 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 create a resource control without any user/team accesses
|
||||||
// * he wants to add more than one user in the user accesses
|
// * he wants to add more than one user in the user accesses
|
||||||
// * he wants to add a user in the user accesses that is not corresponding to its id
|
// * he wants to add a user in the user accesses that is not corresponding to its id
|
||||||
// * he wants to add a team he is not a member of
|
// * he wants to add a team he is not a member of
|
||||||
func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||||
if context.IsAdmin || resourceControl.Public {
|
if context.IsAdmin || resourceControl.Public {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resourceControl.AdministratorsOnly {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
userAccessesCount := len(resourceControl.UserAccesses)
|
userAccessesCount := len(resourceControl.UserAccesses)
|
||||||
teamAccessesCount := len(resourceControl.TeamAccesses)
|
teamAccessesCount := len(resourceControl.TeamAccesses)
|
||||||
|
|
||||||
|
@ -133,15 +91,6 @@ func AuthorizedTeamManagement(teamID portainer.TeamID, context *RestrictedReques
|
||||||
return false
|
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.
|
// 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
|
// It will check if the user is part of the authorized users or part of a team that is
|
||||||
// listed in the authorized teams of the endpoint and the associated group.
|
// listed in the authorized teams of the endpoint and the associated group.
|
||||||
|
|
|
@ -87,6 +87,7 @@ func (server *Server) Start() error {
|
||||||
proxyManagerParameters := &proxy.ManagerParams{
|
proxyManagerParameters := &proxy.ManagerParams{
|
||||||
ResourceControlService: server.ResourceControlService,
|
ResourceControlService: server.ResourceControlService,
|
||||||
UserService: server.UserService,
|
UserService: server.UserService,
|
||||||
|
TeamService: server.TeamService,
|
||||||
TeamMembershipService: server.TeamMembershipService,
|
TeamMembershipService: server.TeamMembershipService,
|
||||||
SettingsService: server.SettingsService,
|
SettingsService: server.SettingsService,
|
||||||
RegistryService: server.RegistryService,
|
RegistryService: server.RegistryService,
|
||||||
|
@ -94,6 +95,7 @@ func (server *Server) Start() error {
|
||||||
SignatureService: server.SignatureService,
|
SignatureService: server.SignatureService,
|
||||||
ReverseTunnelService: server.ReverseTunnelService,
|
ReverseTunnelService: server.ReverseTunnelService,
|
||||||
ExtensionService: server.ExtensionService,
|
ExtensionService: server.ExtensionService,
|
||||||
|
DockerClientFactory: server.DockerClientFactory,
|
||||||
}
|
}
|
||||||
proxyManager := proxy.NewManager(proxyManagerParameters)
|
proxyManager := proxy.NewManager(proxyManagerParameters)
|
||||||
|
|
||||||
|
@ -214,6 +216,8 @@ func (server *Server) Start() error {
|
||||||
stackHandler.RegistryService = server.RegistryService
|
stackHandler.RegistryService = server.RegistryService
|
||||||
stackHandler.DockerHubService = server.DockerHubService
|
stackHandler.DockerHubService = server.DockerHubService
|
||||||
stackHandler.SettingsService = server.SettingsService
|
stackHandler.SettingsService = server.SettingsService
|
||||||
|
stackHandler.UserService = server.UserService
|
||||||
|
stackHandler.ExtensionService = server.ExtensionService
|
||||||
|
|
||||||
var tagHandler = tags.NewHandler(requestBouncer)
|
var tagHandler = tags.NewHandler(requestBouncer)
|
||||||
tagHandler.TagService = server.TagService
|
tagHandler.TagService = server.TagService
|
||||||
|
|
|
@ -16,6 +16,10 @@ import (
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dockerClientVersion = "1.24"
|
||||||
|
)
|
||||||
|
|
||||||
// ComposeStackManager represents a service for managing compose stacks.
|
// ComposeStackManager represents a service for managing compose stacks.
|
||||||
type ComposeStackManager struct {
|
type ComposeStackManager struct {
|
||||||
dataPath string
|
dataPath string
|
||||||
|
@ -40,7 +44,7 @@ func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (
|
||||||
|
|
||||||
clientOpts := client.Options{
|
clientOpts := client.Options{
|
||||||
Host: endpointURL,
|
Host: endpointURL,
|
||||||
APIVersion: portainer.SupportedDockerAPIVersion,
|
APIVersion: dockerClientVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
if endpoint.TLSConfig.TLS {
|
if endpoint.TLSConfig.TLS {
|
||||||
|
|
|
@ -175,14 +175,15 @@ type (
|
||||||
|
|
||||||
// Stack represents a Docker stack created via docker stack deploy
|
// Stack represents a Docker stack created via docker stack deploy
|
||||||
Stack struct {
|
Stack struct {
|
||||||
ID StackID `json:"Id"`
|
ID StackID `json:"Id"`
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
Type StackType `json:"Type"`
|
Type StackType `json:"Type"`
|
||||||
EndpointID EndpointID `json:"EndpointId"`
|
EndpointID EndpointID `json:"EndpointId"`
|
||||||
SwarmID string `json:"SwarmId"`
|
SwarmID string `json:"SwarmId"`
|
||||||
EntryPoint string `json:"EntryPoint"`
|
EntryPoint string `json:"EntryPoint"`
|
||||||
Env []Pair `json:"Env"`
|
Env []Pair `json:"Env"`
|
||||||
ProjectPath string
|
ResourceControl *ResourceControl `json:"ResourceControl"`
|
||||||
|
ProjectPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistryID represents a registry identifier
|
// RegistryID represents a registry identifier
|
||||||
|
@ -446,21 +447,20 @@ type (
|
||||||
|
|
||||||
// ResourceControl represent a reference to a Docker resource with specific access controls
|
// ResourceControl represent a reference to a Docker resource with specific access controls
|
||||||
ResourceControl struct {
|
ResourceControl struct {
|
||||||
ID ResourceControlID `json:"Id"`
|
ID ResourceControlID `json:"Id"`
|
||||||
ResourceID string `json:"ResourceId"`
|
ResourceID string `json:"ResourceId"`
|
||||||
SubResourceIDs []string `json:"SubResourceIds"`
|
SubResourceIDs []string `json:"SubResourceIds"`
|
||||||
Type ResourceControlType `json:"Type"`
|
Type ResourceControlType `json:"Type"`
|
||||||
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
||||||
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
||||||
Public bool `json:"Public"`
|
Public bool `json:"Public"`
|
||||||
|
AdministratorsOnly bool `json:"AdministratorsOnly"`
|
||||||
|
System bool `json:"System"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 2
|
// Deprecated in DBVersion == 2
|
||||||
OwnerID UserID `json:"OwnerId,omitempty"`
|
OwnerID UserID `json:"OwnerId,omitempty"`
|
||||||
AccessLevel ResourceAccessLevel `json:"AccessLevel,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...)
|
// 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 represents a service for managing resource control data
|
||||||
ResourceControlService interface {
|
ResourceControlService interface {
|
||||||
ResourceControl(ID ResourceControlID) (*ResourceControl, error)
|
ResourceControl(ID ResourceControlID) (*ResourceControl, error)
|
||||||
ResourceControlByResourceID(resourceID string) (*ResourceControl, error)
|
ResourceControlByResourceIDAndType(resourceID string, resourceType ResourceControlType) (*ResourceControl, error)
|
||||||
ResourceControls() ([]ResourceControl, error)
|
ResourceControls() ([]ResourceControl, error)
|
||||||
CreateResourceControl(rc *ResourceControl) error
|
CreateResourceControl(rc *ResourceControl) error
|
||||||
UpdateResourceControl(ID ResourceControlID, resourceControl *ResourceControl) error
|
UpdateResourceControl(ID ResourceControlID, resourceControl *ResourceControl) error
|
||||||
|
@ -912,7 +912,7 @@ const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "1.23.0-dev"
|
APIVersion = "1.23.0-dev"
|
||||||
// DBVersion is the version number of the Portainer database
|
// DBVersion is the version number of the Portainer database
|
||||||
DBVersion = 21
|
DBVersion = 22
|
||||||
// AssetsServerURL represents the URL of the Portainer asset server
|
// AssetsServerURL represents the URL of the Portainer asset server
|
||||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||||
|
@ -936,8 +936,6 @@ const (
|
||||||
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
||||||
// to be used when communicating with an agent
|
// to be used when communicating with an agent
|
||||||
PortainerAgentSignatureMessage = "Portainer-App"
|
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 represents the server used by Portainer to communicate with extensions
|
||||||
ExtensionServer = "localhost"
|
ExtensionServer = "localhost"
|
||||||
// DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance
|
// DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance
|
||||||
|
|
|
@ -1235,7 +1235,7 @@ paths:
|
||||||
summary: "Create a new resource control"
|
summary: "Create a new resource control"
|
||||||
description: |
|
description: |
|
||||||
Create a new resource control to restrict access to a Docker resource.
|
Create a new resource control to restrict access to a Docker resource.
|
||||||
**Access policy**: restricted
|
**Access policy**: administrator
|
||||||
operationId: "ResourceControlCreate"
|
operationId: "ResourceControlCreate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1343,7 +1343,7 @@ paths:
|
||||||
summary: "Remove a resource control"
|
summary: "Remove a resource control"
|
||||||
description: |
|
description: |
|
||||||
Remove a resource control.
|
Remove a resource control.
|
||||||
**Access policy**: restricted
|
**Access policy**: administrator
|
||||||
operationId: "ResourceControlDelete"
|
operationId: "ResourceControlDelete"
|
||||||
security:
|
security:
|
||||||
- jwt: []
|
- jwt: []
|
||||||
|
|
|
@ -101,7 +101,7 @@
|
||||||
<td ng-if="$ctrl.showOwnershipColumn">
|
<td ng-if="$ctrl.showOwnershipColumn">
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
|
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -272,7 +272,7 @@
|
||||||
<td ng-if="$ctrl.showOwnershipColumn" ng-show="$ctrl.columnVisibility.columns.ownership.display">
|
<td ng-if="$ctrl.showOwnershipColumn" ng-show="$ctrl.columnVisibility.columns.ownership.display">
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
|
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
<td>
|
<td>
|
||||||
<a ng-if="!parentCtrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
<a ng-if="!parentCtrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
||||||
<span ng-if="parentCtrl.offlineMode">{{ item.Name | truncate:40 }}</span>
|
<span ng-if="parentCtrl.offlineMode">{{ item.Name | truncate:40 }}</span>
|
||||||
|
<span style="margin-left: 10px;" class="label label-info image-tag space-left" ng-if="item.ResourceControl.System">System</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||||
<td>{{ item.Scope }}</td>
|
<td>{{ item.Scope }}</td>
|
||||||
|
@ -22,6 +23,6 @@
|
||||||
<td ng-if="parentCtrl.showOwnershipColumn">
|
<td ng-if="parentCtrl.showOwnershipColumn">
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
|
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = RCO.ADMINISTRATORS }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.directive('networkRowContent', [function networkRowContent() {
|
.directive('networkRowContent', [function networkRowContent() {
|
||||||
var directive = {
|
var directive = {
|
||||||
|
@ -9,6 +11,9 @@ angular.module('portainer.docker')
|
||||||
parentCtrl: '<',
|
parentCtrl: '<',
|
||||||
allowCheckbox: '<',
|
allowCheckbox: '<',
|
||||||
allowExpand: '<'
|
allowExpand: '<'
|
||||||
|
},
|
||||||
|
controller: ($scope) => {
|
||||||
|
$scope.RCO = RCO;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directive;
|
return directive;
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS', 'DatatableService',
|
.controller('NetworksDatatableController', ['$scope', '$controller', 'NetworkHelper', 'DatatableService',
|
||||||
function ($scope, $controller, PREDEFINED_NETWORKS, DatatableService) {
|
function ($scope, $controller, NetworkHelper, DatatableService) {
|
||||||
|
|
||||||
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
|
|
||||||
this.disableRemove = function(item) {
|
this.disableRemove = function(item) {
|
||||||
return PREDEFINED_NETWORKS.includes(item.Name);
|
return NetworkHelper.isSystemNetwork(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state = Object.assign(this.state, {
|
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) {
|
this.allowSelection = function(item) {
|
||||||
return !this.disableRemove(item);
|
return !this.disableRemove(item);
|
||||||
|
|
|
@ -101,7 +101,7 @@
|
||||||
<td ng-if="$ctrl.showOwnershipColumn">
|
<td ng-if="$ctrl.showOwnershipColumn">
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
|
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -156,7 +156,7 @@
|
||||||
<td ng-if="$ctrl.showOwnershipColumn">
|
<td ng-if="$ctrl.showOwnershipColumn">
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
|
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -164,7 +164,7 @@
|
||||||
<td ng-if="$ctrl.showOwnershipColumn">
|
<td ng-if="$ctrl.showOwnershipColumn">
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
|
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ResourceControlViewModel } from '../../portainer/models/resourceControl';
|
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
|
||||||
|
|
||||||
export function ConfigViewModel(data) {
|
export function ConfigViewModel(data) {
|
||||||
this.Id = data.ID;
|
this.Id = data.ID;
|
||||||
|
@ -9,9 +9,7 @@ export function ConfigViewModel(data) {
|
||||||
this.Labels = data.Spec.Labels;
|
this.Labels = data.Spec.Labels;
|
||||||
this.Data = atob(data.Spec.Data);
|
this.Data = atob(data.Spec.Data);
|
||||||
|
|
||||||
if (data.Portainer) {
|
if (data.Portainer && data.Portainer.ResourceControl) {
|
||||||
if (data.Portainer.ResourceControl) {
|
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import { ResourceControlViewModel } from '../../portainer/models/resourceControl';
|
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
|
||||||
|
|
||||||
export function createStatus(statusText) {
|
export function createStatus(statusText) {
|
||||||
var status = _.toLower(statusText);
|
var status = _.toLower(statusText);
|
||||||
|
@ -103,9 +103,7 @@ export function ContainerDetailsViewModel(data) {
|
||||||
this.Config = data.Config;
|
this.Config = data.Config;
|
||||||
this.HostConfig = data.HostConfig;
|
this.HostConfig = data.HostConfig;
|
||||||
this.Mounts = data.Mounts;
|
this.Mounts = data.Mounts;
|
||||||
if (data.Portainer) {
|
if (data.Portainer && data.Portainer.ResourceControl) {
|
||||||
if (data.Portainer.ResourceControl) {
|
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
||||||
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ResourceControlViewModel } from "../../portainer/models/resourceControl";
|
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
|
||||||
|
|
||||||
export function NetworkViewModel(data) {
|
export function NetworkViewModel(data) {
|
||||||
this.Id = data.Id;
|
this.Id = data.Id;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ResourceControlViewModel } from '../../portainer/models/resourceControl'
|
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
|
||||||
|
|
||||||
export function SecretViewModel(data) {
|
export function SecretViewModel(data) {
|
||||||
this.Id = data.ID;
|
this.Id = data.ID;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ResourceControlViewModel } from '../../portainer/models/resourceControl';
|
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
|
||||||
|
|
||||||
export function ServiceViewModel(data, runningTasks, allTasks) {
|
export function ServiceViewModel(data, runningTasks, allTasks) {
|
||||||
this.Model = data;
|
this.Model = data;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ResourceControlViewModel } from "../../portainer/models/resourceControl";
|
import { ResourceControlViewModel } from 'Portainer/models/resourceControl/resourceControl';
|
||||||
|
|
||||||
export function VolumeViewModel(data) {
|
export function VolumeViewModel(data) {
|
||||||
this.Id = data.Name;
|
this.Id = data.Name;
|
||||||
|
|
|
@ -107,14 +107,14 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe
|
||||||
|
|
||||||
service.createAndStartContainer = function(configuration) {
|
service.createAndStartContainer = function(configuration) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
var containerID;
|
var container;
|
||||||
service.createContainer(configuration)
|
service.createContainer(configuration)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
containerID = data.Id;
|
container = data;
|
||||||
return service.startContainer(containerID);
|
return service.startContainer(container.Id);
|
||||||
})
|
})
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
deferred.resolve({ Id: containerID });
|
deferred.resolve(container);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
deferred.reject(err);
|
deferred.reject(err);
|
||||||
|
@ -129,13 +129,9 @@ function ContainerServiceFactory($q, Container, ResourceControlService, LogHelpe
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
if (data.message) {
|
if (data.message) {
|
||||||
deferred.reject({ msg: data.message, err: 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) {
|
.catch(function error(err) {
|
||||||
deferred.reject({ msg: 'Unable to remove container', err: err });
|
deferred.reject({ msg: 'Unable to remove container', err: err });
|
||||||
|
|
|
@ -43,14 +43,13 @@ function ServiceServiceFactory($q, Service, ServiceHelper, TaskService, Resource
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
Service.remove({id: service.Id}).$promise
|
Service.remove({id: service.Id}).$promise
|
||||||
.then(function success() {
|
.then(function success(data) {
|
||||||
if (service.ResourceControl && service.ResourceControl.Type === 2) {
|
if (data.message) {
|
||||||
return ResourceControlService.deleteResourceControl(service.ResourceControl.Id);
|
deferred.reject({ msg: data.message, err: data.message });
|
||||||
|
} else {
|
||||||
|
deferred.resolve();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(function success() {
|
|
||||||
deferred.resolve();
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
deferred.reject({ msg: 'Unable to remove service', err: err });
|
deferred.reject({ msg: 'Unable to remove service', err: err });
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { VolumeViewModel } from '../models/volume';
|
import { VolumeViewModel } from '../models/volume';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
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';
|
'use strict';
|
||||||
var service = {};
|
var service = {};
|
||||||
|
|
||||||
|
@ -45,13 +45,9 @@ angular.module('portainer.docker')
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
if (data.message) {
|
if (data.message) {
|
||||||
deferred.reject({ msg: data.message, err: 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) {
|
.catch(function error(err) {
|
||||||
deferred.reject({ msg: 'Unable to remove volume', err: err });
|
deferred.reject({ msg: 'Unable to remove volume', err: err });
|
||||||
|
|
|
@ -104,9 +104,9 @@ class CreateConfigController {
|
||||||
}
|
}
|
||||||
|
|
||||||
async createAsync() {
|
async createAsync() {
|
||||||
let accessControlData = this.formValues.AccessControlData;
|
const accessControlData = this.formValues.AccessControlData;
|
||||||
let userDetails = this.Authentication.getUserDetails();
|
const userDetails = this.Authentication.getUserDetails();
|
||||||
let isAdmin = this.Authentication.isAdmin();
|
const isAdmin = this.Authentication.isAdmin();
|
||||||
|
|
||||||
if (this.formValues.ConfigContent === "") {
|
if (this.formValues.ConfigContent === "") {
|
||||||
this.state.formValidationError = "Config content must not be empty";
|
this.state.formValidationError = "Config content must not be empty";
|
||||||
|
@ -117,19 +117,13 @@ class CreateConfigController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = this.prepareConfiguration();
|
const config = this.prepareConfiguration();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let data = await this.ConfigService.create(config);
|
const data = await this.ConfigService.create(config);
|
||||||
let configIdentifier = data.ID;
|
const resourceControl = data.Portainer.ResourceControl;
|
||||||
let userId = userDetails.ID;
|
const userId = userDetails.ID;
|
||||||
await this.ResourceControlService.applyResourceControl(
|
await this.ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||||
"config",
|
|
||||||
configIdentifier,
|
|
||||||
userId,
|
|
||||||
accessControlData,
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
this.Notifications.success("Config successfully created");
|
this.Notifications.success("Config successfully created");
|
||||||
this.$state.go("docker.configs", {}, { reload: true });
|
this.$state.go("docker.configs", {}, { reload: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -759,16 +759,14 @@ function ($q, $scope, $async, $state, $timeout, $transition$, $filter, Container
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyResourceControl(newContainer) {
|
function applyResourceControl(newContainer) {
|
||||||
var containerIdentifier = newContainer.Id;
|
const userId = Authentication.getUserDetails().ID;
|
||||||
var userId = Authentication.getUserDetails().ID;
|
const resourceControl = newContainer.Portainer.ResourceControl;
|
||||||
|
const containerId = newContainer.Id;
|
||||||
|
const accessControlData = $scope.formValues.AccessControlData;
|
||||||
|
|
||||||
return $q.when(ResourceControlService.applyResourceControl(
|
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl)
|
||||||
'container',
|
.then(function onApplyResourceControlSuccess() {
|
||||||
containerIdentifier,
|
return containerId;
|
||||||
userId,
|
|
||||||
$scope.formValues.AccessControlData, []
|
|
||||||
)).then(function onApplyResourceControlSuccess() {
|
|
||||||
return containerIdentifier;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<div class="btn-group" role="group" aria-label="...">
|
<div class="btn-group" role="group" aria-label="...">
|
||||||
<button authorization="DockerContainerStart" class="btn btn-success btn-sm" ng-click="start()" ng-disabled="container.State.Running"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
|
<button authorization="DockerContainerStart" class="btn btn-success btn-sm" ng-click="start()" ng-disabled="container.State.Running"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
|
||||||
<button authorization="DockerContainerStop" class="btn btn-danger btn-sm" ng-click="stop()" ng-disabled="!container.State.Running"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
|
<button authorization="DockerContainerStop" class="btn btn-danger btn-sm" ng-click="stop()" ng-disabled="!container.State.Running"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
|
||||||
<button authorization=DockerContainerKill" class="btn btn-danger btn-sm" ng-click="kill()" ng-disabled="!container.State.Running"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
|
<button authorization="DockerContainerKill" class="btn btn-danger btn-sm" ng-click="kill()" ng-disabled="!container.State.Running"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
|
||||||
<button authorization="DockerContainerRestart" class="btn btn-primary btn-sm" ng-click="restart()" ng-disabled="!container.State.Running"><i class="fa fa-sync space-right" aria-hidden="true"></i>Restart</button>
|
<button authorization="DockerContainerRestart" class="btn btn-primary btn-sm" ng-click="restart()" ng-disabled="!container.State.Running"><i class="fa fa-sync space-right" aria-hidden="true"></i>Restart</button>
|
||||||
<button authorization="DockerContainerPause" class="btn btn-primary btn-sm" ng-click="pause()" ng-disabled="!container.State.Running || container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
<button authorization="DockerContainerPause" class="btn btn-primary btn-sm" ng-click="pause()" ng-disabled="!container.State.Running || container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
||||||
<button authorization="DockerContainerUnpause" class="btn btn-primary btn-sm" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
<button authorization="DockerContainerUnpause" class="btn btn-primary btn-sm" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ContainerController', ['$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) {
|
function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, ContainerService, ImageHelper, NetworkService, Notifications, ModalService, ResourceControlService, RegistryService, ImageService, HttpRequestHelper, Authentication) {
|
||||||
$scope.activityTime = 0;
|
$scope.activityTime = 0;
|
||||||
$scope.portBindings = [];
|
$scope.portBindings = [];
|
||||||
|
|
||||||
|
@ -205,7 +205,7 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co
|
||||||
.then(setMainNetworkAndCreateContainer)
|
.then(setMainNetworkAndCreateContainer)
|
||||||
.then(connectContainerToOtherNetworks)
|
.then(connectContainerToOtherNetworks)
|
||||||
.then(startContainerIfNeeded)
|
.then(startContainerIfNeeded)
|
||||||
.then(createResourceControlIfNeeded)
|
.then(createResourceControl)
|
||||||
.then(deleteOldContainer)
|
.then(deleteOldContainer)
|
||||||
.then(notifyAndChangeView)
|
.then(notifyAndChangeView)
|
||||||
.catch(notifyOnError);
|
.catch(notifyOnError);
|
||||||
|
@ -276,19 +276,11 @@ function ($q, $scope, $state, $transition$, $filter, Commit, ContainerHelper, Co
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createResourceControlIfNeeded(newContainer) {
|
function createResourceControl(newContainer) {
|
||||||
if (!container.ResourceControl) {
|
const userId = Authentication.getUserDetails().ID;
|
||||||
return $q.when();
|
const oldResourceControl = container.ResourceControl;
|
||||||
}
|
const newResourceControl = newContainer.Portainer.ResourceControl;
|
||||||
var containerIdentifier = newContainer.Id;
|
return ResourceControlService.duplicateResourceControl(userId, oldResourceControl, newResourceControl);
|
||||||
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 notifyAndChangeView() {
|
function notifyAndChangeView() {
|
||||||
|
|
|
@ -134,9 +134,10 @@ angular.module('portainer.docker')
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
NetworkService.create(context.networkConfiguration)
|
NetworkService.create(context.networkConfiguration)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var networkIdentifier = data.Id;
|
const userId = context.userDetails.ID;
|
||||||
var userId = context.userDetails.ID;
|
const accessControlData = context.accessControlData;
|
||||||
return ResourceControlService.applyResourceControl('network', networkIdentifier, userId, context.accessControlData, []);
|
const resourceControl = data.Portainer.ResourceControl;
|
||||||
|
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||||
})
|
})
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
Notifications.success('Network successfully created');
|
Notifications.success('Network successfully created');
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<td>ID</td>
|
<td>ID</td>
|
||||||
<td>
|
<td>
|
||||||
{{ network.Id }}
|
{{ network.Id }}
|
||||||
<button authorization="DockerNetworkDelete" ng-if="allowRemove(network)" class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this network</button>
|
<button authorization="DockerNetworkDelete" ng-if="allowRemove()" class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this network</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -55,7 +55,8 @@
|
||||||
ng-if="network && applicationState.application.authentication"
|
ng-if="network && applicationState.application.authentication"
|
||||||
resource-id="network.Id"
|
resource-id="network.Id"
|
||||||
resource-control="network.ResourceControl"
|
resource-control="network.ResourceControl"
|
||||||
resource-type="'network'">
|
resource-type="'network'"
|
||||||
|
disable-ownership-change="isSystemNetwork()">
|
||||||
</por-access-control-panel>
|
</por-access-control-panel>
|
||||||
<!-- !access-control-panel -->
|
<!-- !access-control-panel -->
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'Notifications', 'HttpRequestHelper', 'PREDEFINED_NETWORKS',
|
.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'Notifications', 'HttpRequestHelper', 'NetworkHelper',
|
||||||
function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper, PREDEFINED_NETWORKS) {
|
function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper, NetworkHelper) {
|
||||||
|
|
||||||
$scope.network = {};
|
|
||||||
|
|
||||||
$scope.removeNetwork = function removeNetwork() {
|
$scope.removeNetwork = function removeNetwork() {
|
||||||
NetworkService.remove($transition$.params().id, $transition$.params().id)
|
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) {
|
$scope.isSystemNetwork = function() {
|
||||||
return !PREDEFINED_NETWORKS.includes(item.Name);
|
return $scope.network && NetworkHelper.isSystemNetwork($scope.network);
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.allowRemove = function() {
|
||||||
|
return !$scope.isSystemNetwork();
|
||||||
};
|
};
|
||||||
|
|
||||||
function filterContainersInNetwork(network, containers) {
|
function filterContainersInNetwork(network, containers) {
|
||||||
|
|
|
@ -59,9 +59,9 @@ function ($scope, $state, Notifications, SecretService, LabelHelper, Authenticat
|
||||||
|
|
||||||
$scope.create = function () {
|
$scope.create = function () {
|
||||||
|
|
||||||
var accessControlData = $scope.formValues.AccessControlData;
|
const accessControlData = $scope.formValues.AccessControlData;
|
||||||
var userDetails = Authentication.getUserDetails();
|
const userDetails = Authentication.getUserDetails();
|
||||||
var isAdmin = Authentication.isAdmin();
|
const isAdmin = Authentication.isAdmin();
|
||||||
|
|
||||||
if (!validateForm(accessControlData, isAdmin)) {
|
if (!validateForm(accessControlData, isAdmin)) {
|
||||||
return;
|
return;
|
||||||
|
@ -71,9 +71,9 @@ function ($scope, $state, Notifications, SecretService, LabelHelper, Authenticat
|
||||||
var secretConfiguration = prepareConfiguration();
|
var secretConfiguration = prepareConfiguration();
|
||||||
SecretService.create(secretConfiguration)
|
SecretService.create(secretConfiguration)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var secretIdentifier = data.ID;
|
const userId = userDetails.ID;
|
||||||
var userId = userDetails.ID;
|
const resourceControl = data.Portainer.ResourceControl;
|
||||||
return ResourceControlService.applyResourceControl('secret', secretIdentifier, userId, accessControlData, []);
|
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||||
})
|
})
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
Notifications.success('Secret successfully created');
|
Notifications.success('Secret successfully created');
|
||||||
|
|
|
@ -432,15 +432,14 @@ function ($q, $scope, $state, $timeout, Service, ServiceHelper, ConfigService, C
|
||||||
var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : '';
|
var authenticationDetails = registry.Authentication ? RegistryService.encodedCredentials(registry) : '';
|
||||||
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
|
HttpRequestHelper.setRegistryAuthenticationHeader(authenticationDetails);
|
||||||
|
|
||||||
var serviceIdentifier;
|
|
||||||
Service.create(config).$promise
|
Service.create(config).$promise
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
serviceIdentifier = data.ID;
|
const serviceId = data.ID;
|
||||||
return $q.when($scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceIdentifier, EndpointProvider.endpointID()));
|
const resourceControl = data.Portainer.ResourceControl;
|
||||||
})
|
const userId = Authentication.getUserDetails().ID;
|
||||||
.then(function success() {
|
const rcPromise = ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||||
var userId = Authentication.getUserDetails().ID;
|
const webhookPromise = $q.when($scope.formValues.Webhook && WebhookService.createServiceWebhook(serviceId, EndpointProvider.endpointID()));
|
||||||
return ResourceControlService.applyResourceControl('service', serviceIdentifier, userId, accessControlData, []);
|
return $q.all([rcPromise, webhookPromise]);
|
||||||
})
|
})
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
Notifications.success('Service successfully created');
|
Notifications.success('Service successfully created');
|
||||||
|
|
|
@ -81,9 +81,9 @@ function ($q, $scope, $state, VolumeService, PluginService, ResourceControlServi
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
VolumeService.createVolume(volumeConfiguration)
|
VolumeService.createVolume(volumeConfiguration)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var volumeIdentifier = data.Id;
|
const userId = userDetails.ID;
|
||||||
var userId = userDetails.ID;
|
const resourceControl = data.ResourceControl;
|
||||||
return ResourceControlService.applyResourceControl('volume', volumeIdentifier, userId, accessControlData, []);
|
return ResourceControlService.applyResourceControl(userId, accessControlData, resourceControl);
|
||||||
})
|
})
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
Notifications.success('Volume successfully created');
|
Notifications.success('Volume successfully created');
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- restricted-access -->
|
<!-- restricted-access -->
|
||||||
<!-- authorized-teams -->
|
<!-- authorized-teams -->
|
||||||
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === 'restricted' && ($ctrl.isAdmin || (!$ctrl.isAdmin && $ctrl.availableTeams.length > 1))" >
|
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === $ctrl.RCO.RESTRICTED && ($ctrl.isAdmin || (!$ctrl.isAdmin && $ctrl.availableTeams.length > 1))" >
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label for="group-access" class="control-label text-left">
|
<label for="group-access" class="control-label text-left">
|
||||||
Authorized teams
|
Authorized teams
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- !authorized-teams -->
|
<!-- !authorized-teams -->
|
||||||
<!-- authorized-users -->
|
<!-- authorized-users -->
|
||||||
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === 'restricted' && $ctrl.isAdmin">
|
<div class="form-group" ng-if="$ctrl.formData.AccessControlEnabled && $ctrl.formData.Ownership === $ctrl.RCO.RESTRICTED && $ctrl.isAdmin">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label for="group-access" class="control-label text-left">
|
<label for="group-access" class="control-label text-left">
|
||||||
Authorized users
|
Authorized users
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
|
||||||
|
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('porAccessControlFormController', ['$q', 'UserService', 'TeamService', 'Notifications', 'Authentication', 'ResourceControlService',
|
.controller('porAccessControlFormController', ['$q', 'UserService', 'TeamService', 'Notifications', 'Authentication', 'ResourceControlService',
|
||||||
function ($q, UserService, TeamService, Notifications, Authentication, ResourceControlService) {
|
function ($q, UserService, TeamService, Notifications, Authentication, ResourceControlService) {
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
|
ctrl.RCO = RCO;
|
||||||
|
|
||||||
ctrl.availableTeams = [];
|
ctrl.availableTeams = [];
|
||||||
ctrl.availableUsers = [];
|
ctrl.availableUsers = [];
|
||||||
|
|
||||||
function setOwnership(resourceControl, isAdmin) {
|
function setOwnership(resourceControl, isAdmin) {
|
||||||
if (isAdmin && resourceControl.Ownership === 'private') {
|
if (isAdmin && resourceControl.Ownership === RCO.PRIVATE) {
|
||||||
ctrl.formData.Ownership = 'restricted';
|
ctrl.formData.Ownership = RCO.RESTRICTED;
|
||||||
} else {
|
} else {
|
||||||
ctrl.formData.Ownership = resourceControl.Ownership;
|
ctrl.formData.Ownership = resourceControl.Ownership;
|
||||||
}
|
}
|
||||||
|
@ -37,7 +40,7 @@ function ($q, UserService, TeamService, Notifications, Authentication, ResourceC
|
||||||
ctrl.isAdmin = isAdmin;
|
ctrl.isAdmin = isAdmin;
|
||||||
|
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
ctrl.formData.Ownership = 'administrators';
|
ctrl.formData.Ownership = ctrl.RCO.ADMINISTRATORS;
|
||||||
}
|
}
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
|
||||||
|
|
||||||
export function AccessControlFormData() {
|
export function AccessControlFormData() {
|
||||||
this.AccessControlEnabled = true;
|
this.AccessControlEnabled = true;
|
||||||
this.Ownership = 'private';
|
this.Ownership = RCO.PRIVATE;
|
||||||
this.AuthorizedUsers = [];
|
this.AuthorizedUsers = [];
|
||||||
this.AuthorizedTeams = [];
|
this.AuthorizedTeams = [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ angular.module('portainer.app').component('porAccessControlPanel', {
|
||||||
// This component is usually displayed inside a resource-details view.
|
// This component is usually displayed inside a resource-details view.
|
||||||
// This variable specifies the type of the associated resource.
|
// This variable specifies the type of the associated resource.
|
||||||
// Accepted values: 'container', 'service' or 'volume'.
|
// Accepted values: 'container', 'service' or 'volume'.
|
||||||
resourceType: '<'
|
resourceType: '<',
|
||||||
|
// Allow to disable the Ownership edition based on non resource control data
|
||||||
|
disableOwnershipChange: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,28 +16,28 @@
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="$ctrl.resourceControl">
|
<span ng-if="$ctrl.resourceControl">
|
||||||
{{ $ctrl.resourceControl.Ownership }}
|
{{ $ctrl.resourceControl.Ownership }}
|
||||||
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === 'public'" message="This resource can be managed by any user with access to this endpoint." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
|
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === $ctrl.RCO.PUBLIC" message="This resource can be managed by any user with access to this endpoint." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
|
||||||
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === 'private'" message="Management of this resource is restricted to a single user." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
|
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === $ctrl.RCO.PRIVATE" message="Management of this resource is restricted to a single user." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
|
||||||
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === 'restricted'" message="This resource can be managed by a restricted set of users and/or teams." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
|
<portainer-tooltip ng-if="$ctrl.resourceControl.Ownership === $ctrl.RCO.RESTRICTED" message="This resource can be managed by a restricted set of users and/or teams." position="bottom" style="margin-left: -3px;"></portainer-tooltip>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- !ownership -->
|
<!-- !ownership -->
|
||||||
<tr ng-if="$ctrl.resourceControl.Type === 2 && $ctrl.resourceType === 'container'">
|
<tr ng-if="$ctrl.resourceControl.Type === $ctrl.RCTI.SERVICE && $ctrl.resourceType === $ctrl.RCTS.CONTAINER">
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<i class="fa fa-info-circle" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-info-circle" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
Access control on this resource is inherited from the following service: <a ui-sref="docker.services.service({ id: $ctrl.resourceControl.ResourceId })">{{ $ctrl.resourceControl.ResourceId | truncate }}</a>
|
Access control on this resource is inherited from the following service: <a ui-sref="docker.services.service({ id: $ctrl.resourceControl.ResourceId })">{{ $ctrl.resourceControl.ResourceId | truncate }}</a>
|
||||||
<portainer-tooltip message="Access control applied on a service is also applied on each container of that service." position="bottom" style="margin-left: 2px;"></portainer-tooltip>
|
<portainer-tooltip message="Access control applied on a service is also applied on each container of that service." position="bottom" style="margin-left: 2px;"></portainer-tooltip>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="$ctrl.resourceControl.Type === 1 && $ctrl.resourceType === 'volume'">
|
<tr ng-if="$ctrl.resourceControl.Type === $ctrl.RCTI.CONTAINER && $ctrl.resourceType === $ctrl.RCTS.VOLUME">
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<i class="fa fa-info-circle" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-info-circle" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
Access control on this resource is inherited from the following container: <a ui-sref="docker.containers.container({ id: $ctrl.resourceControl.ResourceId })">{{ $ctrl.resourceControl.ResourceId | truncate }}</a>
|
Access control on this resource is inherited from the following container: <a ui-sref="docker.containers.container({ id: $ctrl.resourceControl.ResourceId })">{{ $ctrl.resourceControl.ResourceId | truncate }}</a>
|
||||||
<portainer-tooltip message="Access control applied on a container created using a template is also applied on each volume associated to the container." position="bottom" style="margin-left: 2px;"></portainer-tooltip>
|
<portainer-tooltip message="Access control applied on a container created using a template is also applied on each volume associated to the container." position="bottom" style="margin-left: 2px;"></portainer-tooltip>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="$ctrl.resourceControl.Type === 6 && $ctrl.resourceType !== 'stack'">
|
<tr ng-if="$ctrl.resourceControl.Type === $ctrl.RCTI.STACK && $ctrl.resourceType !== $ctrl.RCTS.STACK">
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<i class="fa fa-info-circle" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i class="fa fa-info-circle" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
Access control on this resource is inherited from the following stack: {{ $ctrl.resourceControl.ResourceId }}
|
Access control on this resource is inherited from the following stack: {{ $ctrl.resourceControl.ResourceId }}
|
||||||
|
@ -61,11 +61,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<!-- !authorized-teams -->
|
<!-- !authorized-teams -->
|
||||||
<!-- edit-ownership -->
|
<!-- edit-ownership -->
|
||||||
<tr ng-if="!($ctrl.resourceControl.Type === 1 && $ctrl.resourceType === 'volume')
|
<tr ng-if="$ctrl.canEditOwnership();">
|
||||||
&& !($ctrl.resourceControl.Type === 2 && $ctrl.resourceType === 'container')
|
|
||||||
&& !($ctrl.resourceControl.Type === 6 && $ctrl.resourceType !== 'stack')
|
|
||||||
&& !$ctrl.state.editOwnership
|
|
||||||
&& ($ctrl.isAdmin || $ctrl.state.canEditOwnership)">
|
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<a ng-click="$ctrl.state.editOwnership = true"><i class="fa fa-edit space-right" aria-hidden="true"></i>Change ownership</a>
|
<a ng-click="$ctrl.state.editOwnership = true"><i class="fa fa-edit space-right" aria-hidden="true"></i>Change ownership</a>
|
||||||
</td>
|
</td>
|
||||||
|
@ -76,7 +72,7 @@
|
||||||
<td colspan="2" style="white-space: inherit;">
|
<td colspan="2" style="white-space: inherit;">
|
||||||
<div class="boxselector_wrapper">
|
<div class="boxselector_wrapper">
|
||||||
<div ng-if="$ctrl.isAdmin">
|
<div ng-if="$ctrl.isAdmin">
|
||||||
<input type="radio" id="access_administrators" ng-model="$ctrl.formValues.Ownership" value="administrators">
|
<input type="radio" id="access_administrators" ng-model="$ctrl.formValues.Ownership" ng-value="$ctrl.RCO.ADMINISTRATORS">
|
||||||
<label for="access_administrators">
|
<label for="access_administrators">
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i ng-class="'administrators' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i ng-class="'administrators' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
@ -86,7 +82,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="$ctrl.isAdmin">
|
<div ng-if="$ctrl.isAdmin">
|
||||||
<input type="radio" id="access_restricted" ng-model="$ctrl.formValues.Ownership" value="restricted">
|
<input type="radio" id="access_restricted" ng-model="$ctrl.formValues.Ownership" ng-value="$ctrl.RCO.RESTRICTED">
|
||||||
<label for="access_restricted">
|
<label for="access_restricted">
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
@ -98,7 +94,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="!$ctrl.isAdmin && $ctrl.state.canChangeOwnershipToTeam && $ctrl.availableTeams.length > 0">
|
<div ng-if="!$ctrl.isAdmin && $ctrl.state.canChangeOwnershipToTeam && $ctrl.availableTeams.length > 0">
|
||||||
<input type="radio" id="access_restricted" ng-model="$ctrl.formValues.Ownership" value="restricted">
|
<input type="radio" id="access_restricted" ng-model="$ctrl.formValues.Ownership" ng-value="$ctrl.RCO.RESTRICTED">
|
||||||
<label for="access_restricted">
|
<label for="access_restricted">
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i ng-class="'restricted' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
@ -113,7 +109,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id="access_public" ng-model="$ctrl.formValues.Ownership" value="public">
|
<input type="radio" id="access_public" ng-model="$ctrl.formValues.Ownership" ng-value="$ctrl.RCO.PUBLIC">
|
||||||
<label for="access_public">
|
<label for="access_public">
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
<i ng-class="'public' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
<i ng-class="'public' | ownershipicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
@ -127,7 +123,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<!-- edit-ownership-choices -->
|
<!-- edit-ownership-choices -->
|
||||||
<!-- select-teams -->
|
<!-- select-teams -->
|
||||||
<tr ng-if="$ctrl.state.editOwnership && $ctrl.formValues.Ownership === 'restricted' && ($ctrl.isAdmin || !$ctrl.isAdmin && $ctrl.availableTeams.length > 1)">
|
<tr ng-if="$ctrl.state.editOwnership && $ctrl.formValues.Ownership === $ctrl.RCO.RESTRICTED && ($ctrl.isAdmin || !$ctrl.isAdmin && $ctrl.availableTeams.length > 1)">
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<span>Teams</span>
|
<span>Teams</span>
|
||||||
<span ng-if="$ctrl.isAdmin && $ctrl.availableTeams.length === 0" class="small text-muted" style="margin-left: 10px;">
|
<span ng-if="$ctrl.isAdmin && $ctrl.availableTeams.length === 0" class="small text-muted" style="margin-left: 10px;">
|
||||||
|
@ -149,7 +145,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<!-- !select-teams -->
|
<!-- !select-teams -->
|
||||||
<!-- select-users -->
|
<!-- select-users -->
|
||||||
<tr ng-if="$ctrl.isAdmin && $ctrl.state.editOwnership && $ctrl.formValues.Ownership === 'restricted'">
|
<tr ng-if="$ctrl.isAdmin && $ctrl.state.editOwnership && $ctrl.formValues.Ownership === $ctrl.RCO.RESTRICTED">
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<span>Users</span>
|
<span>Users</span>
|
||||||
<span ng-if="$ctrl.availableUsers.length === 0" class="small text-muted" style="margin-left: 10px;">
|
<span ng-if="$ctrl.availableUsers.length === 0" class="small text-muted" style="margin-left: 10px;">
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
import { AccessControlPanelData } from './porAccessControlPanelModel';
|
||||||
|
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
|
||||||
|
import { ResourceControlTypeString as RCTS, ResourceControlTypeInt as RCTI} from 'Portainer/models/resourceControl/resourceControlTypes';
|
||||||
|
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('porAccessControlPanelController', ['$q', '$state', 'UserService', 'TeamService', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'FormValidator',
|
.controller('porAccessControlPanelController', ['$q', '$state', 'UserService', 'TeamService', 'ResourceControlHelper', 'ResourceControlService', 'Notifications', 'Authentication', 'ModalService', 'FormValidator',
|
||||||
function ($q, $state, UserService, TeamService, ResourceControlService, Notifications, Authentication, ModalService, FormValidator) {
|
function ($q, $state, UserService, TeamService, ResourceControlHelper, ResourceControlService, Notifications, Authentication, ModalService, FormValidator) {
|
||||||
|
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
|
ctrl.RCO = RCO;
|
||||||
|
ctrl.RCTS = RCTS;
|
||||||
|
ctrl.RCTI = RCTI;
|
||||||
ctrl.state = {
|
ctrl.state = {
|
||||||
displayAccessControlPanel: false,
|
displayAccessControlPanel: false,
|
||||||
canEditOwnership: false,
|
canEditOwnership: false,
|
||||||
|
@ -13,17 +19,24 @@ function ($q, $state, UserService, TeamService, ResourceControlService, Notifica
|
||||||
formValidationError: ''
|
formValidationError: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
ctrl.formValues = {
|
ctrl.formValues = new AccessControlPanelData();
|
||||||
Ownership: 'administrators',
|
|
||||||
Ownership_Users: [],
|
|
||||||
Ownership_Teams: []
|
|
||||||
};
|
|
||||||
|
|
||||||
ctrl.authorizedUsers = [];
|
ctrl.authorizedUsers = [];
|
||||||
ctrl.availableUsers = [];
|
ctrl.availableUsers = [];
|
||||||
ctrl.authorizedTeams = [];
|
ctrl.authorizedTeams = [];
|
||||||
ctrl.availableTeams = [];
|
ctrl.availableTeams = [];
|
||||||
|
|
||||||
|
ctrl.canEditOwnership = function() {
|
||||||
|
const hasRC = ctrl.resourceControl;
|
||||||
|
const inheritedVolume = hasRC && ctrl.resourceControl.Type === RCTI.CONTAINER && ctrl.resourceType === RCTS.VOLUME;
|
||||||
|
const inheritedContainer = hasRC && ctrl.resourceControl.Type === RCTI.SERVICE && ctrl.resourceType === RCTS.CONTAINER;
|
||||||
|
const inheritedFromStack = hasRC && ctrl.resourceControl.Type === RCTI.STACK && ctrl.resourceType !== RCTS.STACK;
|
||||||
|
const hasSpecialDisable = ctrl.disableOwnershipChange;
|
||||||
|
|
||||||
|
return !inheritedVolume && !inheritedContainer && !inheritedFromStack && !hasSpecialDisable
|
||||||
|
&& !ctrl.state.editOwnership && (ctrl.isAdmin || ctrl.state.canEditOwnership);
|
||||||
|
}
|
||||||
|
|
||||||
ctrl.confirmUpdateOwnership = function () {
|
ctrl.confirmUpdateOwnership = function () {
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return;
|
return;
|
||||||
|
@ -39,7 +52,7 @@ function ($q, $state, UserService, TeamService, ResourceControlService, Notifica
|
||||||
var error = '';
|
var error = '';
|
||||||
|
|
||||||
var accessControlData = {
|
var accessControlData = {
|
||||||
AccessControlEnabled: ctrl.formValues.Ownership === 'public' ? false : true,
|
AccessControlEnabled: ctrl.formValues.Ownership === RCO.PUBLIC ? false : true,
|
||||||
Ownership: ctrl.formValues.Ownership,
|
Ownership: ctrl.formValues.Ownership,
|
||||||
AuthorizedUsers: ctrl.formValues.Ownership_Users,
|
AuthorizedUsers: ctrl.formValues.Ownership_Users,
|
||||||
AuthorizedTeams: ctrl.formValues.Ownership_Teams
|
AuthorizedTeams: ctrl.formValues.Ownership_Teams
|
||||||
|
@ -53,32 +66,8 @@ function ($q, $state, UserService, TeamService, ResourceControlService, Notifica
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function processOwnershipFormValues() {
|
|
||||||
var userIds = [];
|
|
||||||
angular.forEach(ctrl.formValues.Ownership_Users, function(user) {
|
|
||||||
userIds.push(user.Id);
|
|
||||||
});
|
|
||||||
var teamIds = [];
|
|
||||||
angular.forEach(ctrl.formValues.Ownership_Teams, function(team) {
|
|
||||||
teamIds.push(team.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
var publicOnly = ctrl.formValues.Ownership === 'public' ? true : false;
|
|
||||||
|
|
||||||
return {
|
|
||||||
ownership: ctrl.formValues.Ownership,
|
|
||||||
authorizedUserIds: publicOnly ? [] : userIds,
|
|
||||||
authorizedTeamIds: publicOnly ? [] : teamIds,
|
|
||||||
publicOnly: publicOnly
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateOwnership() {
|
function updateOwnership() {
|
||||||
var resourceId = ctrl.resourceId;
|
ResourceControlService.applyResourceControlChange(ctrl.resourceType, ctrl.resourceId, ctrl.resourceControl, ctrl.formValues)
|
||||||
var ownershipParameters = processOwnershipFormValues();
|
|
||||||
|
|
||||||
ResourceControlService.applyResourceControlChange(ctrl.resourceType, resourceId,
|
|
||||||
ctrl.resourceControl, ownershipParameters)
|
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
Notifications.success('Access control successfully updated');
|
Notifications.success('Access control successfully updated');
|
||||||
$state.reload();
|
$state.reload();
|
||||||
|
@ -95,17 +84,12 @@ function ($q, $state, UserService, TeamService, ResourceControlService, Notifica
|
||||||
ctrl.isAdmin = isAdmin;
|
ctrl.isAdmin = isAdmin;
|
||||||
var resourceControl = ctrl.resourceControl;
|
var resourceControl = ctrl.resourceControl;
|
||||||
|
|
||||||
if (isAdmin) {
|
if (isAdmin && resourceControl) {
|
||||||
if (resourceControl) {
|
ctrl.formValues.Ownership = resourceControl.Ownership === RCO.PRIVATE ? RCO.RESTRICTED : resourceControl.Ownership;
|
||||||
ctrl.formValues.Ownership = resourceControl.Ownership === 'private' ? 'restricted' : resourceControl.Ownership;
|
|
||||||
} else {
|
|
||||||
ctrl.formValues.Ownership = 'administrators';
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ctrl.formValues.Ownership = 'administrators';
|
ctrl.formValues.Ownership = RCO.ADMINISTRATORS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ResourceControlService.retrieveOwnershipDetails(resourceControl)
|
ResourceControlService.retrieveOwnershipDetails(resourceControl)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
ctrl.authorizedUsers = data.authorizedUsers;
|
ctrl.authorizedUsers = data.authorizedUsers;
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { ResourceControlOwnership } from 'Portainer/models/resourceControl/resourceControlOwnership';
|
||||||
|
|
||||||
|
export function AccessControlPanelData() {
|
||||||
|
this.Ownership = ResourceControlOwnership.ADMINISTRATORS;
|
||||||
|
this.Ownership_Users = [];
|
||||||
|
this.Ownership_Teams = [];
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue