mirror of https://github.com/portainer/portainer
commit
b7eca7ce17
2
.babelrc
2
.babelrc
|
@ -5,7 +5,7 @@
|
|||
"@babel/preset-env",
|
||||
{
|
||||
"modules": false,
|
||||
"useBuiltIns": "usage"
|
||||
"useBuiltIns": "entry"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
|
@ -24,7 +24,7 @@ rules:
|
|||
# no-cond-assign: error
|
||||
# no-console: off
|
||||
# no-constant-condition: error
|
||||
# no-control-regex: error
|
||||
no-control-regex: off
|
||||
# no-debugger: error
|
||||
# no-dupe-args: error
|
||||
# no-dupe-keys: error
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -17,32 +17,38 @@ func UnzipArchive(archiveData []byte, dest string) error {
|
|||
}
|
||||
|
||||
for _, zipFile := range zipReader.File {
|
||||
|
||||
f, err := zipFile.Open()
|
||||
err := extractFileFromArchive(zipFile, dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fpath := filepath.Join(dest, zipFile.Name)
|
||||
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zipFile.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(outFile, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outFile.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractFileFromArchive(file *zip.File, dest string) error {
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fpath := filepath.Join(dest, file.Name)
|
||||
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(outFile, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return outFile.Close()
|
||||
}
|
||||
|
|
|
@ -34,6 +34,394 @@ func NewAuthorizationService(parameters *AuthorizationServiceParameters) *Author
|
|||
}
|
||||
}
|
||||
|
||||
// DefaultEndpointAuthorizationsForEndpointAdministratorRole returns the default endpoint authorizations
|
||||
// associated to the endpoint administrator role.
|
||||
func DefaultEndpointAuthorizationsForEndpointAdministratorRole() Authorizations {
|
||||
return map[Authorization]bool{
|
||||
OperationDockerContainerArchiveInfo: true,
|
||||
OperationDockerContainerList: true,
|
||||
OperationDockerContainerExport: true,
|
||||
OperationDockerContainerChanges: true,
|
||||
OperationDockerContainerInspect: true,
|
||||
OperationDockerContainerTop: true,
|
||||
OperationDockerContainerLogs: true,
|
||||
OperationDockerContainerStats: true,
|
||||
OperationDockerContainerAttachWebsocket: true,
|
||||
OperationDockerContainerArchive: true,
|
||||
OperationDockerContainerCreate: true,
|
||||
OperationDockerContainerPrune: true,
|
||||
OperationDockerContainerKill: true,
|
||||
OperationDockerContainerPause: true,
|
||||
OperationDockerContainerUnpause: true,
|
||||
OperationDockerContainerRestart: true,
|
||||
OperationDockerContainerStart: true,
|
||||
OperationDockerContainerStop: true,
|
||||
OperationDockerContainerWait: true,
|
||||
OperationDockerContainerResize: true,
|
||||
OperationDockerContainerAttach: true,
|
||||
OperationDockerContainerExec: true,
|
||||
OperationDockerContainerRename: true,
|
||||
OperationDockerContainerUpdate: true,
|
||||
OperationDockerContainerPutContainerArchive: true,
|
||||
OperationDockerContainerDelete: true,
|
||||
OperationDockerImageList: true,
|
||||
OperationDockerImageSearch: true,
|
||||
OperationDockerImageGetAll: true,
|
||||
OperationDockerImageGet: true,
|
||||
OperationDockerImageHistory: true,
|
||||
OperationDockerImageInspect: true,
|
||||
OperationDockerImageLoad: true,
|
||||
OperationDockerImageCreate: true,
|
||||
OperationDockerImagePrune: true,
|
||||
OperationDockerImagePush: true,
|
||||
OperationDockerImageTag: true,
|
||||
OperationDockerImageDelete: true,
|
||||
OperationDockerImageCommit: true,
|
||||
OperationDockerImageBuild: true,
|
||||
OperationDockerNetworkList: true,
|
||||
OperationDockerNetworkInspect: true,
|
||||
OperationDockerNetworkCreate: true,
|
||||
OperationDockerNetworkConnect: true,
|
||||
OperationDockerNetworkDisconnect: true,
|
||||
OperationDockerNetworkPrune: true,
|
||||
OperationDockerNetworkDelete: true,
|
||||
OperationDockerVolumeList: true,
|
||||
OperationDockerVolumeInspect: true,
|
||||
OperationDockerVolumeCreate: true,
|
||||
OperationDockerVolumePrune: true,
|
||||
OperationDockerVolumeDelete: true,
|
||||
OperationDockerExecInspect: true,
|
||||
OperationDockerExecStart: true,
|
||||
OperationDockerExecResize: true,
|
||||
OperationDockerSwarmInspect: true,
|
||||
OperationDockerSwarmUnlockKey: true,
|
||||
OperationDockerSwarmInit: true,
|
||||
OperationDockerSwarmJoin: true,
|
||||
OperationDockerSwarmLeave: true,
|
||||
OperationDockerSwarmUpdate: true,
|
||||
OperationDockerSwarmUnlock: true,
|
||||
OperationDockerNodeList: true,
|
||||
OperationDockerNodeInspect: true,
|
||||
OperationDockerNodeUpdate: true,
|
||||
OperationDockerNodeDelete: true,
|
||||
OperationDockerServiceList: true,
|
||||
OperationDockerServiceInspect: true,
|
||||
OperationDockerServiceLogs: true,
|
||||
OperationDockerServiceCreate: true,
|
||||
OperationDockerServiceUpdate: true,
|
||||
OperationDockerServiceDelete: true,
|
||||
OperationDockerSecretList: true,
|
||||
OperationDockerSecretInspect: true,
|
||||
OperationDockerSecretCreate: true,
|
||||
OperationDockerSecretUpdate: true,
|
||||
OperationDockerSecretDelete: true,
|
||||
OperationDockerConfigList: true,
|
||||
OperationDockerConfigInspect: true,
|
||||
OperationDockerConfigCreate: true,
|
||||
OperationDockerConfigUpdate: true,
|
||||
OperationDockerConfigDelete: true,
|
||||
OperationDockerTaskList: true,
|
||||
OperationDockerTaskInspect: true,
|
||||
OperationDockerTaskLogs: true,
|
||||
OperationDockerPluginList: true,
|
||||
OperationDockerPluginPrivileges: true,
|
||||
OperationDockerPluginInspect: true,
|
||||
OperationDockerPluginPull: true,
|
||||
OperationDockerPluginCreate: true,
|
||||
OperationDockerPluginEnable: true,
|
||||
OperationDockerPluginDisable: true,
|
||||
OperationDockerPluginPush: true,
|
||||
OperationDockerPluginUpgrade: true,
|
||||
OperationDockerPluginSet: true,
|
||||
OperationDockerPluginDelete: true,
|
||||
OperationDockerSessionStart: true,
|
||||
OperationDockerDistributionInspect: true,
|
||||
OperationDockerBuildPrune: true,
|
||||
OperationDockerBuildCancel: true,
|
||||
OperationDockerPing: true,
|
||||
OperationDockerInfo: true,
|
||||
OperationDockerVersion: true,
|
||||
OperationDockerEvents: true,
|
||||
OperationDockerSystem: true,
|
||||
OperationDockerUndefined: true,
|
||||
OperationDockerAgentPing: true,
|
||||
OperationDockerAgentList: true,
|
||||
OperationDockerAgentHostInfo: true,
|
||||
OperationDockerAgentBrowseDelete: true,
|
||||
OperationDockerAgentBrowseGet: true,
|
||||
OperationDockerAgentBrowseList: true,
|
||||
OperationDockerAgentBrowsePut: true,
|
||||
OperationDockerAgentBrowseRename: true,
|
||||
OperationDockerAgentUndefined: true,
|
||||
OperationPortainerResourceControlCreate: true,
|
||||
OperationPortainerResourceControlUpdate: true,
|
||||
OperationPortainerStackList: true,
|
||||
OperationPortainerStackInspect: true,
|
||||
OperationPortainerStackFile: true,
|
||||
OperationPortainerStackCreate: true,
|
||||
OperationPortainerStackMigrate: true,
|
||||
OperationPortainerStackUpdate: true,
|
||||
OperationPortainerStackDelete: true,
|
||||
OperationPortainerWebsocketExec: true,
|
||||
OperationPortainerWebhookList: true,
|
||||
OperationPortainerWebhookCreate: true,
|
||||
OperationPortainerWebhookDelete: true,
|
||||
OperationIntegrationStoridgeAdmin: true,
|
||||
EndpointResourcesAccess: true,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultEndpointAuthorizationsForHelpDeskRole returns the default endpoint authorizations
|
||||
// associated to the helpdesk role.
|
||||
func DefaultEndpointAuthorizationsForHelpDeskRole(volumeBrowsingAuthorizations bool) Authorizations {
|
||||
authorizations := map[Authorization]bool{
|
||||
OperationDockerContainerArchiveInfo: true,
|
||||
OperationDockerContainerList: true,
|
||||
OperationDockerContainerChanges: true,
|
||||
OperationDockerContainerInspect: true,
|
||||
OperationDockerContainerTop: true,
|
||||
OperationDockerContainerLogs: true,
|
||||
OperationDockerContainerStats: true,
|
||||
OperationDockerImageList: true,
|
||||
OperationDockerImageSearch: true,
|
||||
OperationDockerImageGetAll: true,
|
||||
OperationDockerImageGet: true,
|
||||
OperationDockerImageHistory: true,
|
||||
OperationDockerImageInspect: true,
|
||||
OperationDockerNetworkList: true,
|
||||
OperationDockerNetworkInspect: true,
|
||||
OperationDockerVolumeList: true,
|
||||
OperationDockerVolumeInspect: true,
|
||||
OperationDockerSwarmInspect: true,
|
||||
OperationDockerNodeList: true,
|
||||
OperationDockerNodeInspect: true,
|
||||
OperationDockerServiceList: true,
|
||||
OperationDockerServiceInspect: true,
|
||||
OperationDockerServiceLogs: true,
|
||||
OperationDockerSecretList: true,
|
||||
OperationDockerSecretInspect: true,
|
||||
OperationDockerConfigList: true,
|
||||
OperationDockerConfigInspect: true,
|
||||
OperationDockerTaskList: true,
|
||||
OperationDockerTaskInspect: true,
|
||||
OperationDockerTaskLogs: true,
|
||||
OperationDockerPluginList: true,
|
||||
OperationDockerDistributionInspect: true,
|
||||
OperationDockerPing: true,
|
||||
OperationDockerInfo: true,
|
||||
OperationDockerVersion: true,
|
||||
OperationDockerEvents: true,
|
||||
OperationDockerSystem: true,
|
||||
OperationDockerAgentPing: true,
|
||||
OperationDockerAgentList: true,
|
||||
OperationDockerAgentHostInfo: true,
|
||||
OperationPortainerStackList: true,
|
||||
OperationPortainerStackInspect: true,
|
||||
OperationPortainerStackFile: true,
|
||||
OperationPortainerWebhookList: true,
|
||||
EndpointResourcesAccess: true,
|
||||
}
|
||||
|
||||
if volumeBrowsingAuthorizations {
|
||||
authorizations[OperationDockerAgentBrowseGet] = true
|
||||
authorizations[OperationDockerAgentBrowseList] = true
|
||||
}
|
||||
|
||||
return authorizations
|
||||
}
|
||||
|
||||
// DefaultEndpointAuthorizationsForStandardUserRole returns the default endpoint authorizations
|
||||
// associated to the standard user role.
|
||||
func DefaultEndpointAuthorizationsForStandardUserRole(volumeBrowsingAuthorizations bool) Authorizations {
|
||||
authorizations := map[Authorization]bool{
|
||||
OperationDockerContainerArchiveInfo: true,
|
||||
OperationDockerContainerList: true,
|
||||
OperationDockerContainerExport: true,
|
||||
OperationDockerContainerChanges: true,
|
||||
OperationDockerContainerInspect: true,
|
||||
OperationDockerContainerTop: true,
|
||||
OperationDockerContainerLogs: true,
|
||||
OperationDockerContainerStats: true,
|
||||
OperationDockerContainerAttachWebsocket: true,
|
||||
OperationDockerContainerArchive: true,
|
||||
OperationDockerContainerCreate: true,
|
||||
OperationDockerContainerKill: true,
|
||||
OperationDockerContainerPause: true,
|
||||
OperationDockerContainerUnpause: true,
|
||||
OperationDockerContainerRestart: true,
|
||||
OperationDockerContainerStart: true,
|
||||
OperationDockerContainerStop: true,
|
||||
OperationDockerContainerWait: true,
|
||||
OperationDockerContainerResize: true,
|
||||
OperationDockerContainerAttach: true,
|
||||
OperationDockerContainerExec: true,
|
||||
OperationDockerContainerRename: true,
|
||||
OperationDockerContainerUpdate: true,
|
||||
OperationDockerContainerPutContainerArchive: true,
|
||||
OperationDockerContainerDelete: true,
|
||||
OperationDockerImageList: true,
|
||||
OperationDockerImageSearch: true,
|
||||
OperationDockerImageGetAll: true,
|
||||
OperationDockerImageGet: true,
|
||||
OperationDockerImageHistory: true,
|
||||
OperationDockerImageInspect: true,
|
||||
OperationDockerImageLoad: true,
|
||||
OperationDockerImageCreate: true,
|
||||
OperationDockerImagePush: true,
|
||||
OperationDockerImageTag: true,
|
||||
OperationDockerImageDelete: true,
|
||||
OperationDockerImageCommit: true,
|
||||
OperationDockerImageBuild: true,
|
||||
OperationDockerNetworkList: true,
|
||||
OperationDockerNetworkInspect: true,
|
||||
OperationDockerNetworkCreate: true,
|
||||
OperationDockerNetworkConnect: true,
|
||||
OperationDockerNetworkDisconnect: true,
|
||||
OperationDockerNetworkDelete: true,
|
||||
OperationDockerVolumeList: true,
|
||||
OperationDockerVolumeInspect: true,
|
||||
OperationDockerVolumeCreate: true,
|
||||
OperationDockerVolumeDelete: true,
|
||||
OperationDockerExecInspect: true,
|
||||
OperationDockerExecStart: true,
|
||||
OperationDockerExecResize: true,
|
||||
OperationDockerSwarmInspect: true,
|
||||
OperationDockerSwarmUnlockKey: true,
|
||||
OperationDockerSwarmInit: true,
|
||||
OperationDockerSwarmJoin: true,
|
||||
OperationDockerSwarmLeave: true,
|
||||
OperationDockerSwarmUpdate: true,
|
||||
OperationDockerSwarmUnlock: true,
|
||||
OperationDockerNodeList: true,
|
||||
OperationDockerNodeInspect: true,
|
||||
OperationDockerNodeUpdate: true,
|
||||
OperationDockerNodeDelete: true,
|
||||
OperationDockerServiceList: true,
|
||||
OperationDockerServiceInspect: true,
|
||||
OperationDockerServiceLogs: true,
|
||||
OperationDockerServiceCreate: true,
|
||||
OperationDockerServiceUpdate: true,
|
||||
OperationDockerServiceDelete: true,
|
||||
OperationDockerSecretList: true,
|
||||
OperationDockerSecretInspect: true,
|
||||
OperationDockerSecretCreate: true,
|
||||
OperationDockerSecretUpdate: true,
|
||||
OperationDockerSecretDelete: true,
|
||||
OperationDockerConfigList: true,
|
||||
OperationDockerConfigInspect: true,
|
||||
OperationDockerConfigCreate: true,
|
||||
OperationDockerConfigUpdate: true,
|
||||
OperationDockerConfigDelete: true,
|
||||
OperationDockerTaskList: true,
|
||||
OperationDockerTaskInspect: true,
|
||||
OperationDockerTaskLogs: true,
|
||||
OperationDockerPluginList: true,
|
||||
OperationDockerPluginPrivileges: true,
|
||||
OperationDockerPluginInspect: true,
|
||||
OperationDockerPluginPull: true,
|
||||
OperationDockerPluginCreate: true,
|
||||
OperationDockerPluginEnable: true,
|
||||
OperationDockerPluginDisable: true,
|
||||
OperationDockerPluginPush: true,
|
||||
OperationDockerPluginUpgrade: true,
|
||||
OperationDockerPluginSet: true,
|
||||
OperationDockerPluginDelete: true,
|
||||
OperationDockerSessionStart: true,
|
||||
OperationDockerDistributionInspect: true,
|
||||
OperationDockerBuildPrune: true,
|
||||
OperationDockerBuildCancel: true,
|
||||
OperationDockerPing: true,
|
||||
OperationDockerInfo: true,
|
||||
OperationDockerVersion: true,
|
||||
OperationDockerEvents: true,
|
||||
OperationDockerSystem: true,
|
||||
OperationDockerUndefined: true,
|
||||
OperationDockerAgentPing: true,
|
||||
OperationDockerAgentList: true,
|
||||
OperationDockerAgentHostInfo: true,
|
||||
OperationDockerAgentUndefined: true,
|
||||
OperationPortainerResourceControlUpdate: true,
|
||||
OperationPortainerStackList: true,
|
||||
OperationPortainerStackInspect: true,
|
||||
OperationPortainerStackFile: true,
|
||||
OperationPortainerStackCreate: true,
|
||||
OperationPortainerStackMigrate: true,
|
||||
OperationPortainerStackUpdate: true,
|
||||
OperationPortainerStackDelete: true,
|
||||
OperationPortainerWebsocketExec: true,
|
||||
OperationPortainerWebhookList: true,
|
||||
OperationPortainerWebhookCreate: true,
|
||||
}
|
||||
|
||||
if volumeBrowsingAuthorizations {
|
||||
authorizations[OperationDockerAgentBrowseGet] = true
|
||||
authorizations[OperationDockerAgentBrowseList] = true
|
||||
authorizations[OperationDockerAgentBrowseDelete] = true
|
||||
authorizations[OperationDockerAgentBrowsePut] = true
|
||||
authorizations[OperationDockerAgentBrowseRename] = true
|
||||
}
|
||||
|
||||
return authorizations
|
||||
}
|
||||
|
||||
// DefaultEndpointAuthorizationsForReadOnlyUserRole returns the default endpoint authorizations
|
||||
// associated to the readonly user role.
|
||||
func DefaultEndpointAuthorizationsForReadOnlyUserRole(volumeBrowsingAuthorizations bool) Authorizations {
|
||||
authorizations := map[Authorization]bool{
|
||||
OperationDockerContainerArchiveInfo: true,
|
||||
OperationDockerContainerList: true,
|
||||
OperationDockerContainerChanges: true,
|
||||
OperationDockerContainerInspect: true,
|
||||
OperationDockerContainerTop: true,
|
||||
OperationDockerContainerLogs: true,
|
||||
OperationDockerContainerStats: true,
|
||||
OperationDockerImageList: true,
|
||||
OperationDockerImageSearch: true,
|
||||
OperationDockerImageGetAll: true,
|
||||
OperationDockerImageGet: true,
|
||||
OperationDockerImageHistory: true,
|
||||
OperationDockerImageInspect: true,
|
||||
OperationDockerNetworkList: true,
|
||||
OperationDockerNetworkInspect: true,
|
||||
OperationDockerVolumeList: true,
|
||||
OperationDockerVolumeInspect: true,
|
||||
OperationDockerSwarmInspect: true,
|
||||
OperationDockerNodeList: true,
|
||||
OperationDockerNodeInspect: true,
|
||||
OperationDockerServiceList: true,
|
||||
OperationDockerServiceInspect: true,
|
||||
OperationDockerServiceLogs: true,
|
||||
OperationDockerSecretList: true,
|
||||
OperationDockerSecretInspect: true,
|
||||
OperationDockerConfigList: true,
|
||||
OperationDockerConfigInspect: true,
|
||||
OperationDockerTaskList: true,
|
||||
OperationDockerTaskInspect: true,
|
||||
OperationDockerTaskLogs: true,
|
||||
OperationDockerPluginList: true,
|
||||
OperationDockerDistributionInspect: true,
|
||||
OperationDockerPing: true,
|
||||
OperationDockerInfo: true,
|
||||
OperationDockerVersion: true,
|
||||
OperationDockerEvents: true,
|
||||
OperationDockerSystem: true,
|
||||
OperationDockerAgentPing: true,
|
||||
OperationDockerAgentList: true,
|
||||
OperationDockerAgentHostInfo: true,
|
||||
OperationPortainerStackList: true,
|
||||
OperationPortainerStackInspect: true,
|
||||
OperationPortainerStackFile: true,
|
||||
OperationPortainerWebhookList: true,
|
||||
}
|
||||
|
||||
if volumeBrowsingAuthorizations {
|
||||
authorizations[OperationDockerAgentBrowseGet] = true
|
||||
authorizations[OperationDockerAgentBrowseList] = true
|
||||
}
|
||||
|
||||
return authorizations
|
||||
}
|
||||
|
||||
// DefaultPortainerAuthorizations returns the default Portainer authorizations used by non-admin users.
|
||||
func DefaultPortainerAuthorizations() Authorizations {
|
||||
return map[Authorization]bool{
|
||||
|
@ -164,7 +552,7 @@ func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) err
|
|||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return service.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
// RemoveUserAccessPolicies will remove all existing access policies associated to the specified user
|
||||
|
@ -383,37 +771,25 @@ func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []TeamMembership
|
|||
}
|
||||
|
||||
func getAuthorizationsFromRoles(roleIdentifiers []RoleID, roles []Role) Authorizations {
|
||||
var roleAuthorizations []Authorizations
|
||||
var associatedRoles []Role
|
||||
|
||||
for _, id := range roleIdentifiers {
|
||||
for _, role := range roles {
|
||||
if role.ID == id {
|
||||
roleAuthorizations = append(roleAuthorizations, role.Authorizations)
|
||||
associatedRoles = append(associatedRoles, role)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processedAuthorizations := make(Authorizations)
|
||||
if len(roleAuthorizations) > 0 {
|
||||
processedAuthorizations = roleAuthorizations[0]
|
||||
for idx, authorizations := range roleAuthorizations {
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
processedAuthorizations = mergeAuthorizations(processedAuthorizations, authorizations)
|
||||
var authorizations Authorizations
|
||||
highestPriority := 0
|
||||
for _, role := range associatedRoles {
|
||||
if role.Priority > highestPriority {
|
||||
highestPriority = role.Priority
|
||||
authorizations = role.Authorizations
|
||||
}
|
||||
}
|
||||
|
||||
return processedAuthorizations
|
||||
}
|
||||
|
||||
func mergeAuthorizations(a, b Authorizations) Authorizations {
|
||||
c := make(map[Authorization]bool)
|
||||
|
||||
for k := range b {
|
||||
if _, ok := a[k]; ok {
|
||||
c[k] = true
|
||||
}
|
||||
}
|
||||
return c
|
||||
return authorizations
|
||||
}
|
||||
|
|
371
api/bolt/init.go
371
api/bolt/init.go
|
@ -32,141 +32,10 @@ func (store *Store) Init() error {
|
|||
|
||||
if len(roles) == 0 {
|
||||
environmentAdministratorRole := &portainer.Role{
|
||||
Name: "Endpoint administrator",
|
||||
Description: "Full control of all resources in an endpoint",
|
||||
Authorizations: map[portainer.Authorization]bool{
|
||||
portainer.OperationDockerContainerArchiveInfo: true,
|
||||
portainer.OperationDockerContainerList: true,
|
||||
portainer.OperationDockerContainerExport: true,
|
||||
portainer.OperationDockerContainerChanges: true,
|
||||
portainer.OperationDockerContainerInspect: true,
|
||||
portainer.OperationDockerContainerTop: true,
|
||||
portainer.OperationDockerContainerLogs: true,
|
||||
portainer.OperationDockerContainerStats: true,
|
||||
portainer.OperationDockerContainerAttachWebsocket: true,
|
||||
portainer.OperationDockerContainerArchive: true,
|
||||
portainer.OperationDockerContainerCreate: true,
|
||||
portainer.OperationDockerContainerPrune: true,
|
||||
portainer.OperationDockerContainerKill: true,
|
||||
portainer.OperationDockerContainerPause: true,
|
||||
portainer.OperationDockerContainerUnpause: true,
|
||||
portainer.OperationDockerContainerRestart: true,
|
||||
portainer.OperationDockerContainerStart: true,
|
||||
portainer.OperationDockerContainerStop: true,
|
||||
portainer.OperationDockerContainerWait: true,
|
||||
portainer.OperationDockerContainerResize: true,
|
||||
portainer.OperationDockerContainerAttach: true,
|
||||
portainer.OperationDockerContainerExec: true,
|
||||
portainer.OperationDockerContainerRename: true,
|
||||
portainer.OperationDockerContainerUpdate: true,
|
||||
portainer.OperationDockerContainerPutContainerArchive: true,
|
||||
portainer.OperationDockerContainerDelete: true,
|
||||
portainer.OperationDockerImageList: true,
|
||||
portainer.OperationDockerImageSearch: true,
|
||||
portainer.OperationDockerImageGetAll: true,
|
||||
portainer.OperationDockerImageGet: true,
|
||||
portainer.OperationDockerImageHistory: true,
|
||||
portainer.OperationDockerImageInspect: true,
|
||||
portainer.OperationDockerImageLoad: true,
|
||||
portainer.OperationDockerImageCreate: true,
|
||||
portainer.OperationDockerImagePrune: true,
|
||||
portainer.OperationDockerImagePush: true,
|
||||
portainer.OperationDockerImageTag: true,
|
||||
portainer.OperationDockerImageDelete: true,
|
||||
portainer.OperationDockerImageCommit: true,
|
||||
portainer.OperationDockerImageBuild: true,
|
||||
portainer.OperationDockerNetworkList: true,
|
||||
portainer.OperationDockerNetworkInspect: true,
|
||||
portainer.OperationDockerNetworkCreate: true,
|
||||
portainer.OperationDockerNetworkConnect: true,
|
||||
portainer.OperationDockerNetworkDisconnect: true,
|
||||
portainer.OperationDockerNetworkPrune: true,
|
||||
portainer.OperationDockerNetworkDelete: true,
|
||||
portainer.OperationDockerVolumeList: true,
|
||||
portainer.OperationDockerVolumeInspect: true,
|
||||
portainer.OperationDockerVolumeCreate: true,
|
||||
portainer.OperationDockerVolumePrune: true,
|
||||
portainer.OperationDockerVolumeDelete: true,
|
||||
portainer.OperationDockerExecInspect: true,
|
||||
portainer.OperationDockerExecStart: true,
|
||||
portainer.OperationDockerExecResize: true,
|
||||
portainer.OperationDockerSwarmInspect: true,
|
||||
portainer.OperationDockerSwarmUnlockKey: true,
|
||||
portainer.OperationDockerSwarmInit: true,
|
||||
portainer.OperationDockerSwarmJoin: true,
|
||||
portainer.OperationDockerSwarmLeave: true,
|
||||
portainer.OperationDockerSwarmUpdate: true,
|
||||
portainer.OperationDockerSwarmUnlock: true,
|
||||
portainer.OperationDockerNodeList: true,
|
||||
portainer.OperationDockerNodeInspect: true,
|
||||
portainer.OperationDockerNodeUpdate: true,
|
||||
portainer.OperationDockerNodeDelete: true,
|
||||
portainer.OperationDockerServiceList: true,
|
||||
portainer.OperationDockerServiceInspect: true,
|
||||
portainer.OperationDockerServiceLogs: true,
|
||||
portainer.OperationDockerServiceCreate: true,
|
||||
portainer.OperationDockerServiceUpdate: true,
|
||||
portainer.OperationDockerServiceDelete: true,
|
||||
portainer.OperationDockerSecretList: true,
|
||||
portainer.OperationDockerSecretInspect: true,
|
||||
portainer.OperationDockerSecretCreate: true,
|
||||
portainer.OperationDockerSecretUpdate: true,
|
||||
portainer.OperationDockerSecretDelete: true,
|
||||
portainer.OperationDockerConfigList: true,
|
||||
portainer.OperationDockerConfigInspect: true,
|
||||
portainer.OperationDockerConfigCreate: true,
|
||||
portainer.OperationDockerConfigUpdate: true,
|
||||
portainer.OperationDockerConfigDelete: true,
|
||||
portainer.OperationDockerTaskList: true,
|
||||
portainer.OperationDockerTaskInspect: true,
|
||||
portainer.OperationDockerTaskLogs: true,
|
||||
portainer.OperationDockerPluginList: true,
|
||||
portainer.OperationDockerPluginPrivileges: true,
|
||||
portainer.OperationDockerPluginInspect: true,
|
||||
portainer.OperationDockerPluginPull: true,
|
||||
portainer.OperationDockerPluginCreate: true,
|
||||
portainer.OperationDockerPluginEnable: true,
|
||||
portainer.OperationDockerPluginDisable: true,
|
||||
portainer.OperationDockerPluginPush: true,
|
||||
portainer.OperationDockerPluginUpgrade: true,
|
||||
portainer.OperationDockerPluginSet: true,
|
||||
portainer.OperationDockerPluginDelete: true,
|
||||
portainer.OperationDockerSessionStart: true,
|
||||
portainer.OperationDockerDistributionInspect: true,
|
||||
portainer.OperationDockerBuildPrune: true,
|
||||
portainer.OperationDockerBuildCancel: true,
|
||||
portainer.OperationDockerPing: true,
|
||||
portainer.OperationDockerInfo: true,
|
||||
portainer.OperationDockerVersion: true,
|
||||
portainer.OperationDockerEvents: true,
|
||||
portainer.OperationDockerSystem: true,
|
||||
portainer.OperationDockerUndefined: true,
|
||||
portainer.OperationDockerAgentPing: true,
|
||||
portainer.OperationDockerAgentList: true,
|
||||
portainer.OperationDockerAgentHostInfo: true,
|
||||
portainer.OperationDockerAgentBrowseDelete: true,
|
||||
portainer.OperationDockerAgentBrowseGet: true,
|
||||
portainer.OperationDockerAgentBrowseList: true,
|
||||
portainer.OperationDockerAgentBrowsePut: true,
|
||||
portainer.OperationDockerAgentBrowseRename: true,
|
||||
portainer.OperationDockerAgentUndefined: true,
|
||||
portainer.OperationPortainerResourceControlCreate: true,
|
||||
portainer.OperationPortainerResourceControlUpdate: true,
|
||||
portainer.OperationPortainerResourceControlDelete: true,
|
||||
portainer.OperationPortainerStackList: true,
|
||||
portainer.OperationPortainerStackInspect: true,
|
||||
portainer.OperationPortainerStackFile: true,
|
||||
portainer.OperationPortainerStackCreate: true,
|
||||
portainer.OperationPortainerStackMigrate: true,
|
||||
portainer.OperationPortainerStackUpdate: true,
|
||||
portainer.OperationPortainerStackDelete: true,
|
||||
portainer.OperationPortainerWebsocketExec: true,
|
||||
portainer.OperationPortainerWebhookList: true,
|
||||
portainer.OperationPortainerWebhookCreate: true,
|
||||
portainer.OperationPortainerWebhookDelete: true,
|
||||
portainer.OperationIntegrationStoridgeAdmin: true,
|
||||
portainer.EndpointResourcesAccess: true,
|
||||
},
|
||||
Name: "Endpoint administrator",
|
||||
Description: "Full control of all resources in an endpoint",
|
||||
Priority: 1,
|
||||
Authorizations: portainer.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
|
||||
}
|
||||
|
||||
err = store.RoleService.CreateRole(environmentAdministratorRole)
|
||||
|
@ -175,55 +44,10 @@ func (store *Store) Init() error {
|
|||
}
|
||||
|
||||
environmentReadOnlyUserRole := &portainer.Role{
|
||||
Name: "Helpdesk",
|
||||
Description: "Read-only access of all resources in an endpoint",
|
||||
Authorizations: map[portainer.Authorization]bool{
|
||||
portainer.OperationDockerContainerArchiveInfo: true,
|
||||
portainer.OperationDockerContainerList: true,
|
||||
portainer.OperationDockerContainerChanges: true,
|
||||
portainer.OperationDockerContainerInspect: true,
|
||||
portainer.OperationDockerContainerTop: true,
|
||||
portainer.OperationDockerContainerLogs: true,
|
||||
portainer.OperationDockerContainerStats: true,
|
||||
portainer.OperationDockerImageList: true,
|
||||
portainer.OperationDockerImageSearch: true,
|
||||
portainer.OperationDockerImageGetAll: true,
|
||||
portainer.OperationDockerImageGet: true,
|
||||
portainer.OperationDockerImageHistory: true,
|
||||
portainer.OperationDockerImageInspect: true,
|
||||
portainer.OperationDockerNetworkList: true,
|
||||
portainer.OperationDockerNetworkInspect: true,
|
||||
portainer.OperationDockerVolumeList: true,
|
||||
portainer.OperationDockerVolumeInspect: true,
|
||||
portainer.OperationDockerSwarmInspect: true,
|
||||
portainer.OperationDockerNodeList: true,
|
||||
portainer.OperationDockerNodeInspect: true,
|
||||
portainer.OperationDockerServiceList: true,
|
||||
portainer.OperationDockerServiceInspect: true,
|
||||
portainer.OperationDockerServiceLogs: true,
|
||||
portainer.OperationDockerSecretList: true,
|
||||
portainer.OperationDockerSecretInspect: true,
|
||||
portainer.OperationDockerConfigList: true,
|
||||
portainer.OperationDockerConfigInspect: true,
|
||||
portainer.OperationDockerTaskList: true,
|
||||
portainer.OperationDockerTaskInspect: true,
|
||||
portainer.OperationDockerTaskLogs: true,
|
||||
portainer.OperationDockerPluginList: true,
|
||||
portainer.OperationDockerDistributionInspect: true,
|
||||
portainer.OperationDockerPing: true,
|
||||
portainer.OperationDockerInfo: true,
|
||||
portainer.OperationDockerVersion: true,
|
||||
portainer.OperationDockerEvents: true,
|
||||
portainer.OperationDockerSystem: true,
|
||||
portainer.OperationDockerAgentPing: true,
|
||||
portainer.OperationDockerAgentList: true,
|
||||
portainer.OperationDockerAgentHostInfo: true,
|
||||
portainer.OperationPortainerStackList: true,
|
||||
portainer.OperationPortainerStackInspect: true,
|
||||
portainer.OperationPortainerStackFile: true,
|
||||
portainer.OperationPortainerWebhookList: true,
|
||||
portainer.EndpointResourcesAccess: true,
|
||||
},
|
||||
Name: "Helpdesk",
|
||||
Description: "Read-only access of all resources in an endpoint",
|
||||
Priority: 2,
|
||||
Authorizations: portainer.DefaultEndpointAuthorizationsForHelpDeskRole(false),
|
||||
}
|
||||
|
||||
err = store.RoleService.CreateRole(environmentReadOnlyUserRole)
|
||||
|
@ -232,129 +56,10 @@ func (store *Store) Init() error {
|
|||
}
|
||||
|
||||
standardUserRole := &portainer.Role{
|
||||
Name: "Standard user",
|
||||
Description: "Full control of assigned resources in an endpoint",
|
||||
Authorizations: map[portainer.Authorization]bool{
|
||||
portainer.OperationDockerContainerArchiveInfo: true,
|
||||
portainer.OperationDockerContainerList: true,
|
||||
portainer.OperationDockerContainerExport: true,
|
||||
portainer.OperationDockerContainerChanges: true,
|
||||
portainer.OperationDockerContainerInspect: true,
|
||||
portainer.OperationDockerContainerTop: true,
|
||||
portainer.OperationDockerContainerLogs: true,
|
||||
portainer.OperationDockerContainerStats: true,
|
||||
portainer.OperationDockerContainerAttachWebsocket: true,
|
||||
portainer.OperationDockerContainerArchive: true,
|
||||
portainer.OperationDockerContainerCreate: true,
|
||||
portainer.OperationDockerContainerKill: true,
|
||||
portainer.OperationDockerContainerPause: true,
|
||||
portainer.OperationDockerContainerUnpause: true,
|
||||
portainer.OperationDockerContainerRestart: true,
|
||||
portainer.OperationDockerContainerStart: true,
|
||||
portainer.OperationDockerContainerStop: true,
|
||||
portainer.OperationDockerContainerWait: true,
|
||||
portainer.OperationDockerContainerResize: true,
|
||||
portainer.OperationDockerContainerAttach: true,
|
||||
portainer.OperationDockerContainerExec: true,
|
||||
portainer.OperationDockerContainerRename: true,
|
||||
portainer.OperationDockerContainerUpdate: true,
|
||||
portainer.OperationDockerContainerPutContainerArchive: true,
|
||||
portainer.OperationDockerContainerDelete: true,
|
||||
portainer.OperationDockerImageList: true,
|
||||
portainer.OperationDockerImageSearch: true,
|
||||
portainer.OperationDockerImageGetAll: true,
|
||||
portainer.OperationDockerImageGet: true,
|
||||
portainer.OperationDockerImageHistory: true,
|
||||
portainer.OperationDockerImageInspect: true,
|
||||
portainer.OperationDockerImageLoad: true,
|
||||
portainer.OperationDockerImageCreate: true,
|
||||
portainer.OperationDockerImagePush: true,
|
||||
portainer.OperationDockerImageTag: true,
|
||||
portainer.OperationDockerImageDelete: true,
|
||||
portainer.OperationDockerImageCommit: true,
|
||||
portainer.OperationDockerImageBuild: true,
|
||||
portainer.OperationDockerNetworkList: true,
|
||||
portainer.OperationDockerNetworkInspect: true,
|
||||
portainer.OperationDockerNetworkCreate: true,
|
||||
portainer.OperationDockerNetworkConnect: true,
|
||||
portainer.OperationDockerNetworkDisconnect: true,
|
||||
portainer.OperationDockerNetworkDelete: true,
|
||||
portainer.OperationDockerVolumeList: true,
|
||||
portainer.OperationDockerVolumeInspect: true,
|
||||
portainer.OperationDockerVolumeCreate: true,
|
||||
portainer.OperationDockerVolumeDelete: true,
|
||||
portainer.OperationDockerExecInspect: true,
|
||||
portainer.OperationDockerExecStart: true,
|
||||
portainer.OperationDockerExecResize: true,
|
||||
portainer.OperationDockerSwarmInspect: true,
|
||||
portainer.OperationDockerSwarmUnlockKey: true,
|
||||
portainer.OperationDockerSwarmInit: true,
|
||||
portainer.OperationDockerSwarmJoin: true,
|
||||
portainer.OperationDockerSwarmLeave: true,
|
||||
portainer.OperationDockerSwarmUpdate: true,
|
||||
portainer.OperationDockerSwarmUnlock: true,
|
||||
portainer.OperationDockerNodeList: true,
|
||||
portainer.OperationDockerNodeInspect: true,
|
||||
portainer.OperationDockerNodeUpdate: true,
|
||||
portainer.OperationDockerNodeDelete: true,
|
||||
portainer.OperationDockerServiceList: true,
|
||||
portainer.OperationDockerServiceInspect: true,
|
||||
portainer.OperationDockerServiceLogs: true,
|
||||
portainer.OperationDockerServiceCreate: true,
|
||||
portainer.OperationDockerServiceUpdate: true,
|
||||
portainer.OperationDockerServiceDelete: true,
|
||||
portainer.OperationDockerSecretList: true,
|
||||
portainer.OperationDockerSecretInspect: true,
|
||||
portainer.OperationDockerSecretCreate: true,
|
||||
portainer.OperationDockerSecretUpdate: true,
|
||||
portainer.OperationDockerSecretDelete: true,
|
||||
portainer.OperationDockerConfigList: true,
|
||||
portainer.OperationDockerConfigInspect: true,
|
||||
portainer.OperationDockerConfigCreate: true,
|
||||
portainer.OperationDockerConfigUpdate: true,
|
||||
portainer.OperationDockerConfigDelete: true,
|
||||
portainer.OperationDockerTaskList: true,
|
||||
portainer.OperationDockerTaskInspect: true,
|
||||
portainer.OperationDockerTaskLogs: true,
|
||||
portainer.OperationDockerPluginList: true,
|
||||
portainer.OperationDockerPluginPrivileges: true,
|
||||
portainer.OperationDockerPluginInspect: true,
|
||||
portainer.OperationDockerPluginPull: true,
|
||||
portainer.OperationDockerPluginCreate: true,
|
||||
portainer.OperationDockerPluginEnable: true,
|
||||
portainer.OperationDockerPluginDisable: true,
|
||||
portainer.OperationDockerPluginPush: true,
|
||||
portainer.OperationDockerPluginUpgrade: true,
|
||||
portainer.OperationDockerPluginSet: true,
|
||||
portainer.OperationDockerPluginDelete: true,
|
||||
portainer.OperationDockerSessionStart: true,
|
||||
portainer.OperationDockerDistributionInspect: true,
|
||||
portainer.OperationDockerBuildPrune: true,
|
||||
portainer.OperationDockerBuildCancel: true,
|
||||
portainer.OperationDockerPing: true,
|
||||
portainer.OperationDockerInfo: true,
|
||||
portainer.OperationDockerVersion: true,
|
||||
portainer.OperationDockerEvents: true,
|
||||
portainer.OperationDockerSystem: true,
|
||||
portainer.OperationDockerUndefined: true,
|
||||
portainer.OperationDockerAgentPing: true,
|
||||
portainer.OperationDockerAgentList: true,
|
||||
portainer.OperationDockerAgentHostInfo: true,
|
||||
portainer.OperationDockerAgentUndefined: true,
|
||||
portainer.OperationPortainerResourceControlCreate: true,
|
||||
portainer.OperationPortainerResourceControlUpdate: true,
|
||||
portainer.OperationPortainerResourceControlDelete: true,
|
||||
portainer.OperationPortainerStackList: true,
|
||||
portainer.OperationPortainerStackInspect: true,
|
||||
portainer.OperationPortainerStackFile: true,
|
||||
portainer.OperationPortainerStackCreate: true,
|
||||
portainer.OperationPortainerStackMigrate: true,
|
||||
portainer.OperationPortainerStackUpdate: true,
|
||||
portainer.OperationPortainerStackDelete: true,
|
||||
portainer.OperationPortainerWebsocketExec: true,
|
||||
portainer.OperationPortainerWebhookList: true,
|
||||
portainer.OperationPortainerWebhookCreate: true,
|
||||
},
|
||||
Name: "Standard user",
|
||||
Description: "Full control of assigned resources in an endpoint",
|
||||
Priority: 3,
|
||||
Authorizations: portainer.DefaultEndpointAuthorizationsForStandardUserRole(false),
|
||||
}
|
||||
|
||||
err = store.RoleService.CreateRole(standardUserRole)
|
||||
|
@ -363,54 +68,10 @@ func (store *Store) Init() error {
|
|||
}
|
||||
|
||||
readOnlyUserRole := &portainer.Role{
|
||||
Name: "Read-only user",
|
||||
Description: "Read-only access of assigned resources in an endpoint",
|
||||
Authorizations: map[portainer.Authorization]bool{
|
||||
portainer.OperationDockerContainerArchiveInfo: true,
|
||||
portainer.OperationDockerContainerList: true,
|
||||
portainer.OperationDockerContainerChanges: true,
|
||||
portainer.OperationDockerContainerInspect: true,
|
||||
portainer.OperationDockerContainerTop: true,
|
||||
portainer.OperationDockerContainerLogs: true,
|
||||
portainer.OperationDockerContainerStats: true,
|
||||
portainer.OperationDockerImageList: true,
|
||||
portainer.OperationDockerImageSearch: true,
|
||||
portainer.OperationDockerImageGetAll: true,
|
||||
portainer.OperationDockerImageGet: true,
|
||||
portainer.OperationDockerImageHistory: true,
|
||||
portainer.OperationDockerImageInspect: true,
|
||||
portainer.OperationDockerNetworkList: true,
|
||||
portainer.OperationDockerNetworkInspect: true,
|
||||
portainer.OperationDockerVolumeList: true,
|
||||
portainer.OperationDockerVolumeInspect: true,
|
||||
portainer.OperationDockerSwarmInspect: true,
|
||||
portainer.OperationDockerNodeList: true,
|
||||
portainer.OperationDockerNodeInspect: true,
|
||||
portainer.OperationDockerServiceList: true,
|
||||
portainer.OperationDockerServiceInspect: true,
|
||||
portainer.OperationDockerServiceLogs: true,
|
||||
portainer.OperationDockerSecretList: true,
|
||||
portainer.OperationDockerSecretInspect: true,
|
||||
portainer.OperationDockerConfigList: true,
|
||||
portainer.OperationDockerConfigInspect: true,
|
||||
portainer.OperationDockerTaskList: true,
|
||||
portainer.OperationDockerTaskInspect: true,
|
||||
portainer.OperationDockerTaskLogs: true,
|
||||
portainer.OperationDockerPluginList: true,
|
||||
portainer.OperationDockerDistributionInspect: true,
|
||||
portainer.OperationDockerPing: true,
|
||||
portainer.OperationDockerInfo: true,
|
||||
portainer.OperationDockerVersion: true,
|
||||
portainer.OperationDockerEvents: true,
|
||||
portainer.OperationDockerSystem: true,
|
||||
portainer.OperationDockerAgentPing: true,
|
||||
portainer.OperationDockerAgentList: true,
|
||||
portainer.OperationDockerAgentHostInfo: true,
|
||||
portainer.OperationPortainerStackList: true,
|
||||
portainer.OperationPortainerStackInspect: true,
|
||||
portainer.OperationPortainerStackFile: true,
|
||||
portainer.OperationPortainerWebhookList: true,
|
||||
},
|
||||
Name: "Read-only user",
|
||||
Description: "Read-only access of assigned resources in an endpoint",
|
||||
Priority: 4,
|
||||
Authorizations: portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(false),
|
||||
}
|
||||
|
||||
err = store.RoleService.CreateRole(readOnlyUserRole)
|
||||
|
|
|
@ -82,13 +82,15 @@ func DeleteObject(db *bolt.DB, bucketName string, key []byte) error {
|
|||
func GetNextIdentifier(db *bolt.DB, bucketName string) int {
|
||||
var identifier int
|
||||
|
||||
db.View(func(tx *bolt.Tx) error {
|
||||
db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
id := bucket.Sequence()
|
||||
id, err := bucket.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
identifier = int(id)
|
||||
return nil
|
||||
})
|
||||
|
||||
identifier++
|
||||
return identifier
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) updateResourceControlsToDBVersion22() error {
|
||||
legacyResourceControls, err := m.resourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, resourceControl := range legacyResourceControls {
|
||||
resourceControl.AdministratorsOnly = false
|
||||
|
||||
err := m.resourceControlService.UpdateResourceControl(resourceControl.ID, &resourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
|
||||
legacyUsers, err := m.userService.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, user := range legacyUsers {
|
||||
user.PortainerAuthorizations = portainer.DefaultPortainerAuthorizations()
|
||||
err = m.userService.UpdateUser(user.ID, &user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpointAdministratorRole, err := m.roleService.Role(portainer.RoleID(1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
endpointAdministratorRole.Priority = 1
|
||||
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.Priority = 2
|
||||
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.Priority = 3
|
||||
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.Priority = 4
|
||||
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()
|
||||
}
|
|
@ -287,5 +287,19 @@ func (m *Migrator) Migrate() error {
|
|||
}
|
||||
}
|
||||
|
||||
// Portainer 1.23.0
|
||||
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||
if m.currentDBVersion < 22 {
|
||||
err := m.updateResourceControlsToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateUsersAndRolesToDBVersion22()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
|
|
@ -42,9 +42,10 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
|
|||
return &resourceControl, nil
|
||||
}
|
||||
|
||||
// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
|
||||
// to the main ResourceID or in SubResourceIDs
|
||||
func (service *Service) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
|
||||
// ResourceControlByResourceIDAndType returns a ResourceControl object by checking if the resourceID is equal
|
||||
// to the main ResourceID or in SubResourceIDs. It also performs a check on the resource type. Return nil
|
||||
// if no ResourceControl was found.
|
||||
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||
var resourceControl *portainer.ResourceControl
|
||||
|
||||
err := service.db.View(func(tx *bolt.Tx) error {
|
||||
|
@ -58,7 +59,7 @@ func (service *Service) ResourceControlByResourceID(resourceID string) (*portain
|
|||
return err
|
||||
}
|
||||
|
||||
if rc.ResourceID == resourceID {
|
||||
if rc.ResourceID == resourceID && rc.Type == resourceType {
|
||||
resourceControl = &rc
|
||||
break
|
||||
}
|
||||
|
@ -71,10 +72,6 @@ func (service *Service) ResourceControlByResourceID(resourceID string) (*portain
|
|||
}
|
||||
}
|
||||
|
||||
if resourceControl == nil {
|
||||
return portainer.ErrObjectNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
|
|
|
@ -489,26 +489,11 @@ func initJobService(dockerClientFactory *docker.ClientFactory) portainer.JobServ
|
|||
func initExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) (portainer.ExtensionManager, error) {
|
||||
extensionManager := exec.NewExtensionManager(fileService, extensionService)
|
||||
|
||||
extensions, err := extensionService.Extensions()
|
||||
err := extensionManager.StartExtensions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, extension := range extensions {
|
||||
err := extensionManager.EnableExtension(&extension, extension.License.LicenseKey)
|
||||
if err != nil {
|
||||
log.Printf("Unable to enable extension: %s [extension: %s]", err.Error(), extension.Name)
|
||||
extension.Enabled = false
|
||||
extension.License.Valid = false
|
||||
}
|
||||
|
||||
err = extensionService.Persist(&extension)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return extensionManager, nil
|
||||
}
|
||||
|
||||
|
@ -639,7 +624,7 @@ func main() {
|
|||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
log.Printf("Creating admin user with password hash %s", adminPasswordHash)
|
||||
log.Println("Created admin user with the given password.")
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
const (
|
||||
unsupportedEnvironmentType = portainer.Error("Environment not supported")
|
||||
defaultDockerRequestTimeout = 60
|
||||
dockerClientVersion = "1.40"
|
||||
)
|
||||
|
||||
// ClientFactory is used to create Docker clients
|
||||
|
@ -51,7 +52,7 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam
|
|||
func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
client.WithVersion(dockerClientVersion),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -63,7 +64,7 @@ func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
|||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
client.WithVersion(dockerClientVersion),
|
||||
client.WithHTTPClient(httpCli),
|
||||
)
|
||||
}
|
||||
|
@ -84,7 +85,7 @@ func createEdgeClient(endpoint *portainer.Endpoint, reverseTunnelService portain
|
|||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpointURL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
client.WithVersion(dockerClientVersion),
|
||||
client.WithHTTPClient(httpCli),
|
||||
client.WithHTTPHeaders(headers),
|
||||
)
|
||||
|
@ -112,7 +113,7 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.
|
|||
|
||||
return client.NewClientWithOpts(
|
||||
client.WithHost(endpoint.URL),
|
||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||
client.WithVersion(dockerClientVersion),
|
||||
client.WithHTTPClient(httpCli),
|
||||
client.WithHTTPHeaders(headers),
|
||||
)
|
||||
|
|
|
@ -4,20 +4,26 @@ import (
|
|||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
|
||||
"github.com/orcaman/concurrent-map"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
)
|
||||
|
||||
var extensionDownloadBaseURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com/extensions/"
|
||||
var extensionDownloadBaseURL = portainer.AssetsServerURL + "/extensions/"
|
||||
var extensionVersionRegexp = regexp.MustCompile(`\d+(\.\d+)+`)
|
||||
|
||||
var extensionBinaryMap = map[portainer.ExtensionID]string{
|
||||
portainer.RegistryManagementExtension: "extension-registry-management",
|
||||
|
@ -47,20 +53,11 @@ func processKey(ID portainer.ExtensionID) string {
|
|||
}
|
||||
|
||||
func buildExtensionURL(extension *portainer.Extension) string {
|
||||
extensionURL := extensionDownloadBaseURL
|
||||
extensionURL += extensionBinaryMap[extension.ID]
|
||||
extensionURL += "-" + runtime.GOOS + "-" + runtime.GOARCH
|
||||
extensionURL += "-" + extension.Version
|
||||
extensionURL += ".zip"
|
||||
return extensionURL
|
||||
return fmt.Sprintf("%s%s-%s-%s-%s.zip", extensionDownloadBaseURL, extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
|
||||
}
|
||||
|
||||
func buildExtensionPath(binaryPath string, extension *portainer.Extension) string {
|
||||
|
||||
extensionFilename := extensionBinaryMap[extension.ID]
|
||||
extensionFilename += "-" + runtime.GOOS + "-" + runtime.GOARCH
|
||||
extensionFilename += "-" + extension.Version
|
||||
|
||||
extensionFilename := fmt.Sprintf("%s-%s-%s-%s", extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
|
||||
if runtime.GOOS == "windows" {
|
||||
extensionFilename += ".exe"
|
||||
}
|
||||
|
@ -73,11 +70,20 @@ func buildExtensionPath(binaryPath string, extension *portainer.Extension) strin
|
|||
}
|
||||
|
||||
// FetchExtensionDefinitions will fetch the list of available
|
||||
// extension definitions from the official Portainer assets server
|
||||
// extension definitions from the official Portainer assets server.
|
||||
// If it cannot retrieve the data from the Internet it will fallback to the locally cached
|
||||
// manifest file.
|
||||
func (manager *ExtensionManager) FetchExtensionDefinitions() ([]portainer.Extension, error) {
|
||||
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 30)
|
||||
var extensionData []byte
|
||||
|
||||
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 5)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Printf("[WARN] [exec,extensions] [message: unable to retrieve extensions manifest via Internet. Extensions will be retrieved from local cache and might not be up to date] [err: %s]", err)
|
||||
|
||||
extensionData, err = manager.fileService.GetFileContent(portainer.LocalExtensionManifestFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var extensions []portainer.Extension
|
||||
|
@ -89,6 +95,37 @@ func (manager *ExtensionManager) FetchExtensionDefinitions() ([]portainer.Extens
|
|||
return extensions, nil
|
||||
}
|
||||
|
||||
// InstallExtension will install the extension from an archive. It will extract the extension version number from
|
||||
// the archive file name first and return an error if the file name is not valid (cannot find extension version).
|
||||
// It will then extract the archive and execute the EnableExtension function to enable the extension.
|
||||
// Since we're missing information about this extension (stored on Portainer.io server) we need to assume
|
||||
// default information based on the extension ID.
|
||||
func (manager *ExtensionManager) InstallExtension(extension *portainer.Extension, licenseKey string, archiveFileName string, extensionArchive []byte) error {
|
||||
extensionVersion := extensionVersionRegexp.FindString(archiveFileName)
|
||||
if extensionVersion == "" {
|
||||
return errors.New("invalid extension archive filename: unable to retrieve extension version")
|
||||
}
|
||||
|
||||
err := manager.fileService.ExtractExtensionArchive(extensionArchive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch extension.ID {
|
||||
case portainer.RegistryManagementExtension:
|
||||
extension.Name = "Registry Manager"
|
||||
case portainer.OAuthAuthenticationExtension:
|
||||
extension.Name = "External Authentication"
|
||||
case portainer.RBACExtension:
|
||||
extension.Name = "Role-Based Access Control"
|
||||
}
|
||||
extension.ShortDescription = "Extension enabled offline"
|
||||
extension.Version = extensionVersion
|
||||
extension.Available = true
|
||||
|
||||
return manager.EnableExtension(extension, licenseKey)
|
||||
}
|
||||
|
||||
// EnableExtension will check for the existence of the extension binary on the filesystem
|
||||
// first. If it does not exist, it will download it from the official Portainer assets server.
|
||||
// After installing the binary on the filesystem, it will execute the binary in license check
|
||||
|
@ -145,6 +182,61 @@ func (manager *ExtensionManager) DisableExtension(extension *portainer.Extension
|
|||
return manager.fileService.RemoveDirectory(extensionBinaryPath)
|
||||
}
|
||||
|
||||
// StartExtensions will retrieve the extensions definitions from the Internet and check if a new version of each
|
||||
// extension is available. If so, it will automatically install the new version of the extension. If no update is
|
||||
// available it will simply start the extension.
|
||||
// The purpose of this function is to be ran at startup, as such most of the error handling won't block the program execution
|
||||
// and will log warning messages instead.
|
||||
func (manager *ExtensionManager) StartExtensions() error {
|
||||
extensions, err := manager.extensionService.Extensions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
definitions, err := manager.FetchExtensionDefinitions()
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [exec,extensions] [message: unable to retrieve extension information from Internet. Skipping extensions update check.] [err: %s]", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return manager.updateAndStartExtensions(extensions, definitions)
|
||||
}
|
||||
|
||||
func (manager *ExtensionManager) updateAndStartExtensions(extensions []portainer.Extension, definitions []portainer.Extension) error {
|
||||
for _, definition := range definitions {
|
||||
for _, extension := range extensions {
|
||||
if extension.ID == definition.ID {
|
||||
definitionVersion := semver.New(definition.Version)
|
||||
extensionVersion := semver.New(extension.Version)
|
||||
|
||||
if extensionVersion.LessThan(*definitionVersion) {
|
||||
log.Printf("[INFO] [exec,extensions] [message: new version detected, updating extension] [extension: %s] [current_version: %s] [available_version: %s]", extension.Name, extension.Version, definition.Version)
|
||||
err := manager.UpdateExtension(&extension, definition.Version)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [exec,extensions] [message: unable to update extension automatically] [extension: %s] [current_version: %s] [available_version: %s] [err: %s]", extension.Name, extension.Version, definition.Version, err)
|
||||
}
|
||||
} else {
|
||||
err := manager.EnableExtension(&extension, extension.License.LicenseKey)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [exec,extensions] [message: unable to start extension] [extension: %s] [err: %s]", extension.Name, err)
|
||||
extension.Enabled = false
|
||||
extension.License.Valid = false
|
||||
}
|
||||
}
|
||||
|
||||
err := manager.extensionService.Persist(&extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateExtension will download the new extension binary from the official Portainer assets
|
||||
// server, disable the previous extension via DisableExtension, trigger a license check
|
||||
// and then start the extension process and add it to the processes map
|
||||
|
@ -195,7 +287,7 @@ func validateLicense(binaryPath, licenseKey string) ([]string, error) {
|
|||
err := licenseCheckProcess.Run()
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] [exec,extension] [message: unable to run extension process] [err: %s]", err)
|
||||
return nil, errors.New("Invalid extension license key")
|
||||
return nil, errors.New("invalid extension license key")
|
||||
}
|
||||
|
||||
output := string(cmdOutput.Bytes())
|
||||
|
@ -205,8 +297,12 @@ func validateLicense(binaryPath, licenseKey string) ([]string, error) {
|
|||
|
||||
func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Extension, binaryPath string) error {
|
||||
extensionProcess := exec.Command(binaryPath, "-license", extension.License.LicenseKey)
|
||||
extensionProcess.Stdout = os.Stdout
|
||||
extensionProcess.Stderr = os.Stderr
|
||||
|
||||
err := extensionProcess.Start()
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] [exec,extension] [message: unable to start extension process] [err: %s]", err)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -87,12 +87,7 @@ func (service *Service) GetBinaryFolder() string {
|
|||
// ExtractExtensionArchive extracts the content of an extension archive
|
||||
// specified as raw data into the binary store on the filesystem
|
||||
func (service *Service) ExtractExtensionArchive(data []byte) error {
|
||||
err := archive.UnzipArchive(data, path.Join(service.fileStorePath, BinaryStorePath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return archive.UnzipArchive(data, path.Join(service.fileStorePath, BinaryStorePath))
|
||||
}
|
||||
|
||||
// RemoveDirectory removes a directory on the filesystem.
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
@ -87,6 +88,7 @@ func Get(url string, timeout int) ([]byte, error) {
|
|||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
log.Printf("[ERROR] [http,client] [message: unexpected status code] [status_code: %d]", response.StatusCode)
|
||||
return nil, errInvalidResponseStatus
|
||||
}
|
||||
|
||||
|
|
|
@ -113,6 +113,11 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
|||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
|
|
|
@ -133,6 +133,11 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
|
|
|
@ -34,6 +34,7 @@ type Handler struct {
|
|||
EndpointGroupService portainer.EndpointGroupService
|
||||
RoleService portainer.RoleService
|
||||
ProxyManager *proxy.Manager
|
||||
AuthorizationService *portainer.AuthorizationService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage authentication operations.
|
||||
|
|
|
@ -29,9 +29,9 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R
|
|||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetProxy(endpoint)
|
||||
proxy = handler.ProxyManager.GetEndpointProxy(endpoint)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
|||
|
||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||
handler.ProxyManager.DeleteProxy(endpoint)
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||
if err != nil {
|
||||
|
@ -55,9 +55,9 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
|||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetProxy(endpoint)
|
||||
proxy = handler.ProxyManager.GetEndpointProxy(endpoint)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
|
||||
}
|
||||
|
||||
handler.ProxyManager.DeleteProxy(endpoint)
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
if len(endpoint.UserAccessPolicies) > 0 || len(endpoint.TeamAccessPolicies) > 0 {
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
|
|
|
@ -166,7 +166,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
}
|
||||
|
||||
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
|
||||
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
|
||||
}
|
||||
|
|
|
@ -59,3 +59,59 @@ func (handler *Handler) upgradeRBACData() error {
|
|||
|
||||
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
func updateUserAccessPolicyToNoRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 0
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func updateTeamAccessPolicyToNoRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 0
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func (handler *Handler) downgradeRBACData() error {
|
||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
for key := range endpointGroup.UserAccessPolicies {
|
||||
updateUserAccessPolicyToNoRole(endpointGroup.UserAccessPolicies, key)
|
||||
}
|
||||
|
||||
for key := range endpointGroup.TeamAccessPolicies {
|
||||
updateTeamAccessPolicyToNoRole(endpointGroup.TeamAccessPolicies, key)
|
||||
}
|
||||
|
||||
err := handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
for key := range endpoint.UserAccessPolicies {
|
||||
updateUserAccessPolicyToNoRole(endpoint.UserAccessPolicies, key)
|
||||
}
|
||||
|
||||
for key := range endpoint.TeamAccessPolicies {
|
||||
updateTeamAccessPolicyToNoRole(endpoint.TeamAccessPolicies, key)
|
||||
}
|
||||
|
||||
err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
}
|
|
@ -29,6 +29,13 @@ func (handler *Handler) extensionDelete(w http.ResponseWriter, r *http.Request)
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete extension", err}
|
||||
}
|
||||
|
||||
if extensionID == portainer.RBACExtension {
|
||||
err = handler.downgradeRBACData()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.ExtensionService.DeleteExtension(extensionID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete the extension from the database", err}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
package extensions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
)
|
||||
|
||||
// GET request on /api/extensions/:id
|
||||
|
@ -18,46 +17,39 @@ func (handler *Handler) extensionInspect(w http.ResponseWriter, r *http.Request)
|
|||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension identifier route variable", err}
|
||||
}
|
||||
|
||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||
|
||||
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 30)
|
||||
definitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension definitions", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions informations", err}
|
||||
}
|
||||
|
||||
var extensions []portainer.Extension
|
||||
err = json.Unmarshal(extensionData, &extensions)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse external extension definitions", err}
|
||||
localExtension, err := handler.ExtensionService.Extension(extensionID)
|
||||
if err != nil && err != portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension information from the database", err}
|
||||
}
|
||||
|
||||
var extension portainer.Extension
|
||||
for _, p := range extensions {
|
||||
if p.ID == extensionID {
|
||||
extension = p
|
||||
if extension.DescriptionURL != "" {
|
||||
description, _ := client.Get(extension.DescriptionURL, 10)
|
||||
extension.Description = string(description)
|
||||
}
|
||||
var extensionDefinition portainer.Extension
|
||||
|
||||
for _, definition := range definitions {
|
||||
if definition.ID == extensionID {
|
||||
extensionDefinition = definition
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
storedExtension, err := handler.ExtensionService.Extension(extensionID)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return response.JSON(w, extension)
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
if localExtension == nil {
|
||||
extension = extensionDefinition
|
||||
} else {
|
||||
extension = *localExtension
|
||||
}
|
||||
|
||||
extension.Enabled = storedExtension.Enabled
|
||||
mergeExtensionAndDefinition(&extension, &extensionDefinition)
|
||||
|
||||
extensionVer := semver.New(extension.Version)
|
||||
pVer := semver.New(storedExtension.Version)
|
||||
|
||||
if pVer.LessThan(*extensionVer) {
|
||||
extension.UpdateAvailable = true
|
||||
}
|
||||
description, _ := client.Get(extension.DescriptionURL, 5)
|
||||
extension.Description = string(description)
|
||||
|
||||
return response.JSON(w, extension)
|
||||
}
|
||||
|
|
|
@ -3,54 +3,28 @@ package extensions
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// GET request on /api/extensions?store=<store>
|
||||
func (handler *Handler) extensionList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
storeDetails, _ := request.RetrieveBooleanQueryParameter(r, "store", true)
|
||||
fetchManifestInformation, _ := request.RetrieveBooleanQueryParameter(r, "store", true)
|
||||
|
||||
extensions, err := handler.ExtensionService.Extensions()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions from the database", err}
|
||||
}
|
||||
|
||||
if storeDetails {
|
||||
if fetchManifestInformation {
|
||||
definitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions informations", err}
|
||||
}
|
||||
|
||||
for idx := range definitions {
|
||||
associateExtensionData(&definitions[idx], extensions)
|
||||
}
|
||||
|
||||
extensions = definitions
|
||||
extensions = mergeExtensionsAndDefinitions(extensions, definitions)
|
||||
}
|
||||
|
||||
return response.JSON(w, extensions)
|
||||
}
|
||||
|
||||
func associateExtensionData(definition *portainer.Extension, extensions []portainer.Extension) {
|
||||
for _, extension := range extensions {
|
||||
if extension.ID == definition.ID {
|
||||
|
||||
definition.Enabled = extension.Enabled
|
||||
definition.License.Company = extension.License.Company
|
||||
definition.License.Expiration = extension.License.Expiration
|
||||
definition.License.Valid = extension.License.Valid
|
||||
|
||||
definitionVersion := semver.New(definition.Version)
|
||||
extensionVersion := semver.New(extension.Version)
|
||||
if extensionVersion.LessThan(*definitionVersion) {
|
||||
definition.UpdateAvailable = true
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
package extensions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type extensionUploadPayload struct {
|
||||
License string
|
||||
ExtensionArchive []byte
|
||||
ArchiveFileName string
|
||||
}
|
||||
|
||||
func (payload *extensionUploadPayload) Validate(r *http.Request) error {
|
||||
license, err := request.RetrieveMultiPartFormValue(r, "License", false)
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid license")
|
||||
}
|
||||
payload.License = license
|
||||
|
||||
fileData, fileName, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||
if err != nil {
|
||||
return portainer.Error("Invalid extension archive file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.ExtensionArchive = fileData
|
||||
payload.ArchiveFileName = fileName
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) extensionUpload(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
payload := &extensionUploadPayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
extensionIdentifier, err := strconv.Atoi(string(payload.License[0]))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid license format", err}
|
||||
}
|
||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||
|
||||
extension := &portainer.Extension{
|
||||
ID: extensionID,
|
||||
}
|
||||
|
||||
_ = handler.ExtensionManager.DisableExtension(extension)
|
||||
|
||||
err = handler.ExtensionManager.InstallExtension(extension, payload.License, payload.ArchiveFileName, payload.ExtensionArchive)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to install extension", err}
|
||||
}
|
||||
|
||||
extension.Enabled = true
|
||||
|
||||
if extension.ID == portainer.RBACExtension {
|
||||
err = handler.upgradeRBACData()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.ExtensionService.Persist(extension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -3,6 +3,8 @@ package extensions
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
|
@ -30,6 +32,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
||||
h.Handle("/extensions",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/extensions/upload",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionUpload))).Methods(http.MethodPost)
|
||||
h.Handle("/extensions/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/extensions/{id}",
|
||||
|
@ -39,3 +43,44 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
|
||||
return h
|
||||
}
|
||||
|
||||
func mergeExtensionsAndDefinitions(extensions, definitions []portainer.Extension) []portainer.Extension {
|
||||
for _, definition := range definitions {
|
||||
foundInDB := false
|
||||
|
||||
for idx, extension := range extensions {
|
||||
if extension.ID == definition.ID {
|
||||
foundInDB = true
|
||||
mergeExtensionAndDefinition(&extensions[idx], &definition)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundInDB {
|
||||
extensions = append(extensions, definition)
|
||||
}
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
|
||||
func mergeExtensionAndDefinition(extension, definition *portainer.Extension) {
|
||||
extension.Name = definition.Name
|
||||
extension.ShortDescription = definition.ShortDescription
|
||||
extension.Deal = definition.Deal
|
||||
extension.Available = definition.Available
|
||||
extension.DescriptionURL = definition.DescriptionURL
|
||||
extension.Images = definition.Images
|
||||
extension.Logo = definition.Logo
|
||||
extension.Price = definition.Price
|
||||
extension.PriceDescription = definition.PriceDescription
|
||||
extension.ShopURL = definition.ShopURL
|
||||
|
||||
definitionVersion := semver.New(definition.Version)
|
||||
extensionVersion := semver.New(extension.Version)
|
||||
if extensionVersion.LessThan(*definitionVersion) {
|
||||
extension.UpdateAvailable = true
|
||||
}
|
||||
|
||||
extension.Version = definition.Version
|
||||
}
|
||||
|
|
|
@ -46,6 +46,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
bouncer.AdminAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
||||
h.PathPrefix("/registries/{id}/v2").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
||||
|
||||
h.PathPrefix("/registries/{id}/proxies/gitlab").Handler(
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithRegistry)))
|
||||
h.PathPrefix("/registries/proxies/gitlab").Handler(
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithoutRegistry)))
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt
|
|||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register registry proxy", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package registries
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
)
|
||||
|
||||
// request on /api/registries/proxies/gitlab
|
||||
func (handler *Handler) proxyRequestsToGitlabAPIWithoutRegistry(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
domain := r.Header.Get("X-Gitlab-Domain")
|
||||
if domain == "" {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "No Gitlab domain provided", nil}
|
||||
}
|
||||
|
||||
proxy, err := handler.ProxyManager.CreateGitlabProxy(domain)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create gitlab proxy", err}
|
||||
}
|
||||
|
||||
http.StripPrefix("/registries/proxies/gitlab", proxy).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package registries
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// request on /api/registries/{id}/proxies/gitlab
|
||||
func (handler *Handler) proxyRequestsToGitlabAPIWithRegistry(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
||||
}
|
||||
|
||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.RegistryAccess(r, registry)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied}
|
||||
}
|
||||
|
||||
extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetExtensionProxy(portainer.RegistryManagementExtension)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err}
|
||||
}
|
||||
}
|
||||
|
||||
config := &portainer.RegistryManagementConfiguration{
|
||||
Type: portainer.GitlabRegistry,
|
||||
Password: registry.Password,
|
||||
}
|
||||
|
||||
encodedConfiguration, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to encode management configuration", err}
|
||||
}
|
||||
|
||||
id := strconv.Itoa(int(registryID))
|
||||
r.Header.Set("X-RegistryManagement-Key", id+"-gitlab")
|
||||
r.Header.Set("X-RegistryManagement-URI", registry.Gitlab.InstanceURL)
|
||||
r.Header.Set("X-RegistryManagement-Config", string(encodedConfiguration))
|
||||
r.Header.Set("X-PortainerExtension-License", extension.License.LicenseKey)
|
||||
|
||||
http.StripPrefix("/registries/"+id+"/proxies/gitlab", proxy).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
|
@ -12,11 +12,12 @@ import (
|
|||
|
||||
type registryCreatePayload struct {
|
||||
Name string
|
||||
Type int
|
||||
Type portainer.RegistryType
|
||||
URL string
|
||||
Authentication bool
|
||||
Username string
|
||||
Password string
|
||||
Gitlab portainer.GitlabRegistryData
|
||||
}
|
||||
|
||||
func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
||||
|
@ -29,8 +30,8 @@ func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
|||
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
|
||||
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
|
||||
}
|
||||
if payload.Type != 1 && payload.Type != 2 && payload.Type != 3 {
|
||||
return portainer.Error("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry) or 3 (custom registry)")
|
||||
if payload.Type != portainer.QuayRegistry && payload.Type != portainer.AzureRegistry && payload.Type != portainer.CustomRegistry && payload.Type != portainer.GitlabRegistry {
|
||||
return portainer.Error("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry) or 4 (Gitlab registry)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -42,16 +43,6 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
||||
}
|
||||
for _, r := range registries {
|
||||
if r.URL == payload.URL {
|
||||
return &httperror.HandlerError{http.StatusConflict, "A registry with the same URL already exists", portainer.ErrRegistryAlreadyExists}
|
||||
}
|
||||
}
|
||||
|
||||
registry := &portainer.Registry{
|
||||
Type: portainer.RegistryType(payload.Type),
|
||||
Name: payload.Name,
|
||||
|
@ -61,6 +52,7 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
|
|||
Password: payload.Password,
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
Gitlab: payload.Gitlab,
|
||||
}
|
||||
|
||||
err = handler.RegistryService.CreateRegistry(registry)
|
||||
|
|
|
@ -21,11 +21,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/resource_controls",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlCreate))).Methods(http.MethodPost)
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.resourceControlCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/resource_controls/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/resource_controls/{id}",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlDelete))).Methods(http.MethodDelete)
|
||||
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.resourceControlDelete))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package resourcecontrols
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
|
@ -8,29 +9,33 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type resourceControlCreatePayload struct {
|
||||
ResourceID string
|
||||
Type string
|
||||
Public bool
|
||||
Users []int
|
||||
Teams []int
|
||||
SubResourceIDs []string
|
||||
ResourceID string
|
||||
Type string
|
||||
Public bool
|
||||
AdministratorsOnly bool
|
||||
Users []int
|
||||
Teams []int
|
||||
SubResourceIDs []string
|
||||
}
|
||||
|
||||
func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.ResourceID) {
|
||||
return portainer.Error("Invalid resource identifier")
|
||||
return errors.New("invalid payload: invalid resource identifier")
|
||||
}
|
||||
|
||||
if govalidator.IsNull(payload.Type) {
|
||||
return portainer.Error("Invalid type")
|
||||
return errors.New("invalid payload: invalid type")
|
||||
}
|
||||
|
||||
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public {
|
||||
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or Public")
|
||||
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public && !payload.AdministratorsOnly {
|
||||
return errors.New("invalid payload: must specify Users, Teams, Public or AdministratorsOnly")
|
||||
}
|
||||
|
||||
if payload.Public && payload.AdministratorsOnly {
|
||||
return errors.New("invalid payload: cannot set both public and administrators only flags to true")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -63,8 +68,8 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid type value. Value must be one of: container, service, volume, network, secret, stack or config", portainer.ErrInvalidResourceControlType}
|
||||
}
|
||||
|
||||
rc, err := handler.ResourceControlService.ResourceControlByResourceID(payload.ResourceID)
|
||||
if err != nil && err != portainer.ErrObjectNotFound {
|
||||
rc, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(payload.ResourceID, resourceControlType)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err}
|
||||
}
|
||||
if rc != nil {
|
||||
|
@ -90,21 +95,13 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req
|
|||
}
|
||||
|
||||
resourceControl := portainer.ResourceControl{
|
||||
ResourceID: payload.ResourceID,
|
||||
SubResourceIDs: payload.SubResourceIDs,
|
||||
Type: resourceControlType,
|
||||
Public: payload.Public,
|
||||
UserAccesses: userAccesses,
|
||||
TeamAccesses: teamAccesses,
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedResourceControlCreation(&resourceControl, securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create a resource control for the specified resource", portainer.ErrResourceAccessDenied}
|
||||
ResourceID: payload.ResourceID,
|
||||
SubResourceIDs: payload.SubResourceIDs,
|
||||
Type: resourceControlType,
|
||||
Public: payload.Public,
|
||||
AdministratorsOnly: payload.AdministratorsOnly,
|
||||
UserAccesses: userAccesses,
|
||||
TeamAccesses: teamAccesses,
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.CreateResourceControl(&resourceControl)
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// DELETE request on /api/resource_controls/:id
|
||||
|
@ -17,22 +16,13 @@ func (handler *Handler) resourceControlDelete(w http.ResponseWriter, r *http.Req
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid resource control identifier route variable", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||
_, err = handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a resource control with with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !security.AuthorizedResourceControlDeletion(resourceControl, securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to delete the resource control", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.DeleteResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the resource control from the database", err}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package resourcecontrols
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
@ -11,14 +12,19 @@ import (
|
|||
)
|
||||
|
||||
type resourceControlUpdatePayload struct {
|
||||
Public bool
|
||||
Users []int
|
||||
Teams []int
|
||||
Public bool
|
||||
Users []int
|
||||
Teams []int
|
||||
AdministratorsOnly bool
|
||||
}
|
||||
|
||||
func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error {
|
||||
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public {
|
||||
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or Public")
|
||||
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public && !payload.AdministratorsOnly {
|
||||
return errors.New("invalid payload: must specify Users, Teams, Public or AdministratorsOnly")
|
||||
}
|
||||
|
||||
if payload.Public && payload.AdministratorsOnly {
|
||||
return errors.New("invalid payload: cannot set public and administrators only")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -49,10 +55,11 @@ func (handler *Handler) resourceControlUpdate(w http.ResponseWriter, r *http.Req
|
|||
}
|
||||
|
||||
if !security.AuthorizedResourceControlAccess(resourceControl, securityContext) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the resource control", portainer.ErrResourceAccessDenied}
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access the resource control", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
resourceControl.Public = payload.Public
|
||||
resourceControl.AdministratorsOnly = payload.AdministratorsOnly
|
||||
|
||||
var userAccesses = make([]portainer.UserResourceAccess, 0)
|
||||
for _, v := range payload.Users {
|
||||
|
|
|
@ -4,18 +4,25 @@ import (
|
|||
"errors"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// this is coming from libcompose
|
||||
// https://github.com/portainer/libcompose/blob/master/project/context.go#L117-L120
|
||||
func normalizeStackName(name string) string {
|
||||
r := regexp.MustCompile("[^a-z0-9]+")
|
||||
return r.ReplaceAllString(strings.ToLower(name), "")
|
||||
}
|
||||
|
||||
type composeStackFromFileContentPayload struct {
|
||||
Name string
|
||||
StackFileContent string
|
||||
|
@ -26,13 +33,14 @@ func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) err
|
|||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
payload.Name = normalizeStackName(payload.Name)
|
||||
if govalidator.IsNull(payload.StackFileContent) {
|
||||
return portainer.Error("Invalid stack file content")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||
var payload composeStackFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
|
@ -86,7 +94,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
|||
}
|
||||
|
||||
doCleanUp = false
|
||||
return response.JSON(w, stack)
|
||||
return handler.decorateStackResponse(w, stack, userID)
|
||||
}
|
||||
|
||||
type composeStackFromGitRepositoryPayload struct {
|
||||
|
@ -104,6 +112,7 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
|
|||
if govalidator.IsNull(payload.Name) {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
payload.Name = normalizeStackName(payload.Name)
|
||||
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
||||
return portainer.Error("Invalid repository URL. Must correspond to a valid URL format")
|
||||
}
|
||||
|
@ -116,7 +125,7 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
|
|||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||
var payload composeStackFromGitRepositoryPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
|
@ -180,7 +189,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
|||
}
|
||||
|
||||
doCleanUp = false
|
||||
return response.JSON(w, stack)
|
||||
return handler.decorateStackResponse(w, stack, userID)
|
||||
}
|
||||
|
||||
type composeStackFromFileUploadPayload struct {
|
||||
|
@ -194,7 +203,7 @@ func (payload *composeStackFromFileUploadPayload) Validate(r *http.Request) erro
|
|||
if err != nil {
|
||||
return portainer.Error("Invalid stack name")
|
||||
}
|
||||
payload.Name = name
|
||||
payload.Name = normalizeStackName(name)
|
||||
|
||||
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||
if err != nil {
|
||||
|
@ -211,7 +220,7 @@ func (payload *composeStackFromFileUploadPayload) Validate(r *http.Request) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||
payload := &composeStackFromFileUploadPayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
|
@ -265,7 +274,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
|||
}
|
||||
|
||||
doCleanUp = false
|
||||
return response.JSON(w, stack)
|
||||
return handler.decorateStackResponse(w, stack, userID)
|
||||
}
|
||||
|
||||
type composeStackDeploymentConfig struct {
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
@ -36,7 +35,7 @@ func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||
var payload swarmStackFromFileContentPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
|
@ -91,7 +90,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
|||
}
|
||||
|
||||
doCleanUp = false
|
||||
return response.JSON(w, stack)
|
||||
return handler.decorateStackResponse(w, stack, userID)
|
||||
}
|
||||
|
||||
type swarmStackFromGitRepositoryPayload struct {
|
||||
|
@ -125,7 +124,7 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
|
|||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||
var payload swarmStackFromGitRepositoryPayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
|
@ -190,7 +189,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
|||
}
|
||||
|
||||
doCleanUp = false
|
||||
return response.JSON(w, stack)
|
||||
return handler.decorateStackResponse(w, stack, userID)
|
||||
}
|
||||
|
||||
type swarmStackFromFileUploadPayload struct {
|
||||
|
@ -228,7 +227,7 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||
payload := &swarmStackFromFileUploadPayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
|
@ -283,7 +282,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
|||
}
|
||||
|
||||
doCleanUp = false
|
||||
return response.JSON(w, stack)
|
||||
return handler.decorateStackResponse(w, stack, userID)
|
||||
}
|
||||
|
||||
type swarmStackDeploymentConfig struct {
|
||||
|
|
|
@ -26,6 +26,8 @@ type Handler struct {
|
|||
SwarmStackManager portainer.SwarmStackManager
|
||||
ComposeStackManager portainer.ComposeStackManager
|
||||
SettingsService portainer.SettingsService
|
||||
UserService portainer.UserService
|
||||
ExtensionService portainer.ExtensionService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage stack operations.
|
||||
|
@ -52,3 +54,36 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackMigrate))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID, resourceControl *portainer.ResourceControl) (bool, error) {
|
||||
if securityContext.IsAdmin {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
userTeamIDs := make([]portainer.TeamID, 0)
|
||||
for _, membership := range securityContext.UserMemberships {
|
||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||
}
|
||||
|
||||
if resourceControl != nil && portainer.UserCanAccessResource(securityContext.UserID, userTeamIDs, resourceControl) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
_, err := handler.ExtensionService.Extension(portainer.RBACExtension)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return false, nil
|
||||
} else if err != nil && err != portainer.ErrObjectNotFound {
|
||||
return false, err
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, ok := user.EndpointAuthorizations[endpointID][portainer.EndpointResourcesAccess]
|
||||
if ok {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
|
|
@ -5,12 +5,13 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/cli/cli/compose/types"
|
||||
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/cli/cli/compose/types"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error {
|
||||
|
@ -54,38 +55,43 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
|||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user details from authentication token", err}
|
||||
}
|
||||
|
||||
switch portainer.StackType(stackType) {
|
||||
case portainer.DockerSwarmStack:
|
||||
return handler.createSwarmStack(w, r, method, endpoint)
|
||||
return handler.createSwarmStack(w, r, method, endpoint, tokenData.ID)
|
||||
case portainer.DockerComposeStack:
|
||||
return handler.createComposeStack(w, r, method, endpoint)
|
||||
return handler.createComposeStack(w, r, method, endpoint, tokenData.ID)
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", errors.New(request.ErrInvalidQueryParameter)}
|
||||
}
|
||||
|
||||
func (handler *Handler) createComposeStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) createComposeStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||
|
||||
switch method {
|
||||
case "string":
|
||||
return handler.createComposeStackFromFileContent(w, r, endpoint)
|
||||
return handler.createComposeStackFromFileContent(w, r, endpoint, userID)
|
||||
case "repository":
|
||||
return handler.createComposeStackFromGitRepository(w, r, endpoint)
|
||||
return handler.createComposeStackFromGitRepository(w, r, endpoint, userID)
|
||||
case "file":
|
||||
return handler.createComposeStackFromFileUpload(w, r, endpoint)
|
||||
return handler.createComposeStackFromFileUpload(w, r, endpoint, userID)
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
||||
func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||
switch method {
|
||||
case "string":
|
||||
return handler.createSwarmStackFromFileContent(w, r, endpoint)
|
||||
return handler.createSwarmStackFromFileContent(w, r, endpoint, userID)
|
||||
case "repository":
|
||||
return handler.createSwarmStackFromGitRepository(w, r, endpoint)
|
||||
return handler.createSwarmStackFromGitRepository(w, r, endpoint, userID)
|
||||
case "file":
|
||||
return handler.createSwarmStackFromFileUpload(w, r, endpoint)
|
||||
return handler.createSwarmStackFromFileUpload(w, r, endpoint, userID)
|
||||
}
|
||||
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
|
||||
|
@ -125,3 +131,15 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error)
|
|||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {
|
||||
resourceControl := portainer.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
|
||||
|
||||
err := handler.ResourceControlService.CreateResourceControl(resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err}
|
||||
}
|
||||
|
||||
stack.ResourceControl = resourceControl
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
@ -64,8 +63,8 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
|||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||
if err != nil && err != portainer.ErrObjectNotFound {
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
|
@ -74,10 +73,12 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
err = handler.deleteStack(stack, endpoint)
|
||||
|
@ -90,6 +91,13 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the stack from the database", err}
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
err = handler.ResourceControlService.DeleteResourceControl(resourceControl.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the associated resource control from the database", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove stack files from disk", err}
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
|
@ -42,8 +41,8 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
|||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||
if err != nil && err != portainer.ErrObjectNotFound {
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
|
@ -52,17 +51,12 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
|
||||
if !securityContext.IsAdmin && resourceControl == nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||
extendedStack.ResourceControl = *resourceControl
|
||||
} else {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
|
@ -37,28 +36,27 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
|
|||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||
if err != nil && err != portainer.ErrObjectNotFound {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
|
||||
extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
|
||||
if !securityContext.IsAdmin && resourceControl == nil {
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||
extendedStack.ResourceControl = *resourceControl
|
||||
} else {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
stack.ResourceControl = resourceControl
|
||||
}
|
||||
|
||||
return response.JSON(w, extendedStack)
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
|
@ -40,10 +39,31 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
filteredStacks := proxy.FilterStacks(stacks, resourceControls, securityContext.IsAdmin,
|
||||
securityContext.UserID, securityContext.UserMemberships)
|
||||
stacks = portainer.DecorateStacks(stacks, resourceControls)
|
||||
|
||||
return response.JSON(w, filteredStacks)
|
||||
if !securityContext.IsAdmin {
|
||||
rbacExtensionEnabled := true
|
||||
_, err := handler.ExtensionService.Extension(portainer.RBACExtension)
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
rbacExtensionEnabled = false
|
||||
} else if err != nil && err != portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check if RBAC extension is enabled", err}
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user information from the database", err}
|
||||
}
|
||||
|
||||
userTeamIDs := make([]portainer.TeamID, 0)
|
||||
for _, membership := range securityContext.UserMemberships {
|
||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||
}
|
||||
|
||||
stacks = portainer.FilterAuthorizedStacks(stacks, user, userTeamIDs, rbacExtensionEnabled)
|
||||
}
|
||||
|
||||
return response.JSON(w, stacks)
|
||||
}
|
||||
|
||||
func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters) []portainer.Stack {
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
|
@ -56,8 +55,8 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
|||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||
if err != nil && err != portainer.ErrObjectNotFound {
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
|
@ -66,10 +65,12 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
|
@ -76,8 +75,8 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
|||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||
if err != nil && err != portainer.ErrObjectNotFound {
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
|
@ -86,10 +85,12 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
updateError := handler.updateAndDeployStack(r, stack, endpoint)
|
||||
|
|
|
@ -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 (
|
||||
"net/http"
|
||||
|
@ -16,9 +16,7 @@ type (
|
|||
expirationTime time.Time
|
||||
}
|
||||
|
||||
// AzureTransport represents a transport used when executing HTTP requests
|
||||
// against the Azure API.
|
||||
AzureTransport struct {
|
||||
Transport struct {
|
||||
credentials *portainer.AzureCredentials
|
||||
client *client.HTTPClient
|
||||
token *azureAPIToken
|
||||
|
@ -26,15 +24,27 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
// NewAzureTransport returns a pointer to an AzureTransport instance.
|
||||
func NewAzureTransport(credentials *portainer.AzureCredentials) *AzureTransport {
|
||||
return &AzureTransport{
|
||||
// NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport
|
||||
// interface for proxying requests to the Azure API.
|
||||
func NewTransport(credentials *portainer.AzureCredentials) *Transport {
|
||||
return &Transport{
|
||||
credentials: credentials,
|
||||
client: client.NewHTTPClient(),
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *AzureTransport) authenticate() error {
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
err := transport.retrieveAuthenticationToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Authorization", "Bearer "+transport.token.value)
|
||||
return http.DefaultTransport.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (transport *Transport) authenticate() error {
|
||||
token, err := transport.client.ExecuteAzureAuthenticationRequest(transport.credentials)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -53,7 +63,7 @@ func (transport *AzureTransport) authenticate() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (transport *AzureTransport) retrieveAuthenticationToken() error {
|
||||
func (transport *Transport) retrieveAuthenticationToken() error {
|
||||
transport.mutex.Lock()
|
||||
defer transport.mutex.Unlock()
|
||||
|
||||
|
@ -68,14 +78,3 @@ func (transport *AzureTransport) retrieveAuthenticationToken() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RoundTrip is the implementation of the Transport interface.
|
||||
func (transport *AzureTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
err := transport.retrieveAuthenticationToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set("Authorization", "Bearer "+transport.token.value)
|
||||
return http.DefaultTransport.RoundTrip(request)
|
||||
}
|
|
@ -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 (
|
||||
"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 (
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
type (
|
||||
registryAccessContext struct {
|
||||
isAdmin bool
|
||||
userID portainer.UserID
|
||||
teamMemberships []portainer.TeamMembership
|
||||
registries []portainer.Registry
|
||||
dockerHub *portainer.DockerHub
|
||||
}
|
||||
registryAuthenticationHeader struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Serveraddress string `json:"serveraddress"`
|
||||
}
|
||||
)
|
||||
|
||||
func createRegistryAuthenticationHeader(serverAddress string, accessContext *registryAccessContext) *registryAuthenticationHeader {
|
||||
var authenticationHeader *registryAuthenticationHeader
|
||||
|
|
@ -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 (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
)
|
||||
|
||||
// swarmInspectOperation extracts the response as a JSON object and rewrites the response based
|
||||
|
@ -9,7 +11,7 @@ import (
|
|||
func swarmInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||
// SwarmInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.30/#operation/SwarmInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -19,5 +21,5 @@ func swarmInspectOperation(response *http.Response, executor *operationExecutor)
|
|||
delete(responseObject, "TLSInfo")
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
|
@ -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,727 @@
|
|||
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
|
||||
}
|
||||
|
||||
user, err := transport.userService.User(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if volumeBrowseRestrictionCheck {
|
||||
settings, err := transport.settingsService.Settings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return access denied for all roles except endpoint-administrator
|
||||
_, userCanBrowse := user.EndpointAuthorizations[transport.endpoint.ID][portainer.OperationDockerAgentBrowseList]
|
||||
if rbacExtension != nil && !settings.AllowVolumeBrowserForRegularUsers && !userCanBrowse {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
}
|
||||
}
|
||||
|
||||
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, newNamedPipeTransport(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,114 @@
|
|||
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)
|
||||
}
|
||||
|
||||
// NewGitlabProxy returns a new HTTP proxy to a Gitlab API server
|
||||
func (factory *ProxyFactory) NewGitlabProxy(gitlabAPIUri string) (http.Handler, error) {
|
||||
return newGitlabProxy(gitlabAPIUri)
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package factory
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/gitlab"
|
||||
)
|
||||
|
||||
func newGitlabProxy(uri string) (http.Handler, error) {
|
||||
url, err := url.Parse(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(url)
|
||||
proxy.Transport = gitlab.NewTransport()
|
||||
return proxy, nil
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package gitlab
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Transport struct {
|
||||
httpTransport *http.Transport
|
||||
}
|
||||
|
||||
// NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport
|
||||
// interface for proxying requests to the Gitlab API.
|
||||
func NewTransport() *Transport {
|
||||
return &Transport{
|
||||
httpTransport: &http.Transport{},
|
||||
}
|
||||
}
|
||||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
token := request.Header.Get("Private-Token")
|
||||
if token == "" {
|
||||
return nil, errors.New("no gitlab token provided")
|
||||
}
|
||||
|
||||
r, err := http.NewRequest(request.Method, request.URL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.Header.Set("Private-Token", token)
|
||||
return transport.httpTransport.RoundTrip(r)
|
||||
}
|
|
@ -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 (
|
||||
"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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/orcaman/concurrent-map"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||
)
|
||||
|
||||
// TODO: contain code related to legacy extension management
|
||||
|
||||
var extensionPorts = map[portainer.ExtensionID]string{
|
||||
portainer.RegistryManagementExtension: "7001",
|
||||
portainer.OAuthAuthenticationExtension: "7002",
|
||||
portainer.RBACExtension: "7003",
|
||||
}
|
||||
|
||||
type (
|
||||
// Manager represents a service used to manage Docker proxies.
|
||||
// Manager represents a service used to manage proxies to endpoints and extensions.
|
||||
Manager struct {
|
||||
proxyFactory *proxyFactory
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
proxies cmap.ConcurrentMap
|
||||
proxyFactory *factory.ProxyFactory
|
||||
endpointProxies cmap.ConcurrentMap
|
||||
extensionProxies cmap.ConcurrentMap
|
||||
legacyExtensionProxies cmap.ConcurrentMap
|
||||
}
|
||||
|
@ -32,6 +25,7 @@ type (
|
|||
ManagerParams struct {
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
UserService portainer.UserService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
SettingsService portainer.SettingsService
|
||||
RegistryService portainer.RegistryService
|
||||
|
@ -39,54 +33,71 @@ type (
|
|||
SignatureService portainer.DigitalSignatureService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
ExtensionService portainer.ExtensionService
|
||||
DockerClientFactory *docker.ClientFactory
|
||||
}
|
||||
)
|
||||
|
||||
// NewManager initializes a new proxy Service
|
||||
func NewManager(parameters *ManagerParams) *Manager {
|
||||
proxyFactoryParameters := &factory.ProxyFactoryParameters{
|
||||
ResourceControlService: parameters.ResourceControlService,
|
||||
UserService: parameters.UserService,
|
||||
TeamService: parameters.TeamService,
|
||||
TeamMembershipService: parameters.TeamMembershipService,
|
||||
SettingsService: parameters.SettingsService,
|
||||
RegistryService: parameters.RegistryService,
|
||||
DockerHubService: parameters.DockerHubService,
|
||||
SignatureService: parameters.SignatureService,
|
||||
ReverseTunnelService: parameters.ReverseTunnelService,
|
||||
ExtensionService: parameters.ExtensionService,
|
||||
DockerClientFactory: parameters.DockerClientFactory,
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
proxies: cmap.New(),
|
||||
endpointProxies: cmap.New(),
|
||||
extensionProxies: cmap.New(),
|
||||
legacyExtensionProxies: cmap.New(),
|
||||
proxyFactory: &proxyFactory{
|
||||
ResourceControlService: parameters.ResourceControlService,
|
||||
UserService: parameters.UserService,
|
||||
TeamMembershipService: parameters.TeamMembershipService,
|
||||
SettingsService: parameters.SettingsService,
|
||||
RegistryService: parameters.RegistryService,
|
||||
DockerHubService: parameters.DockerHubService,
|
||||
SignatureService: parameters.SignatureService,
|
||||
ReverseTunnelService: parameters.ReverseTunnelService,
|
||||
ExtensionService: parameters.ExtensionService,
|
||||
},
|
||||
reverseTunnelService: parameters.ReverseTunnelService,
|
||||
proxyFactory: factory.NewProxyFactory(proxyFactoryParameters),
|
||||
}
|
||||
}
|
||||
|
||||
// GetProxy returns the proxy associated to a key
|
||||
func (manager *Manager) GetProxy(endpoint *portainer.Endpoint) http.Handler {
|
||||
proxy, ok := manager.proxies.Get(string(endpoint.ID))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return proxy.(http.Handler)
|
||||
}
|
||||
|
||||
// CreateAndRegisterProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
|
||||
// CreateAndRegisterEndpointProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
|
||||
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
|
||||
func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
proxy, err := manager.createProxy(endpoint)
|
||||
func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
proxy, err := manager.proxyFactory.NewEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manager.proxies.Set(string(endpoint.ID), proxy)
|
||||
manager.endpointProxies.Set(string(endpoint.ID), proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// DeleteProxy deletes the proxy associated to a key
|
||||
func (manager *Manager) DeleteProxy(endpoint *portainer.Endpoint) {
|
||||
manager.proxies.Remove(string(endpoint.ID))
|
||||
// GetEndpointProxy returns the proxy associated to a key
|
||||
func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Handler {
|
||||
proxy, ok := manager.endpointProxies.Get(string(endpoint.ID))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return proxy.(http.Handler)
|
||||
}
|
||||
|
||||
// DeleteEndpointProxy deletes the proxy associated to a key
|
||||
func (manager *Manager) DeleteEndpointProxy(endpoint *portainer.Endpoint) {
|
||||
manager.endpointProxies.Remove(string(endpoint.ID))
|
||||
}
|
||||
|
||||
// CreateExtensionProxy creates a new HTTP reverse proxy for an extension and
|
||||
// registers it in the extension map associated to the specified extension identifier
|
||||
func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) {
|
||||
proxy, err := manager.proxyFactory.NewExtensionProxy(extensionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manager.extensionProxies.Set(strconv.Itoa(int(extensionID)), proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// GetExtensionProxy returns an extension proxy associated to an extension identifier
|
||||
|
@ -95,28 +106,13 @@ func (manager *Manager) GetExtensionProxy(extensionID portainer.ExtensionID) htt
|
|||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return proxy.(http.Handler)
|
||||
}
|
||||
|
||||
// CreateExtensionProxy creates a new HTTP reverse proxy for an extension and
|
||||
// registers it in the extension map associated to the specified extension identifier
|
||||
func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) {
|
||||
address := "http://" + portainer.ExtensionServer + ":" + extensionPorts[extensionID]
|
||||
|
||||
extensionURL, err := url.Parse(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := manager.proxyFactory.newHTTPProxy(extensionURL)
|
||||
manager.extensionProxies.Set(strconv.Itoa(int(extensionID)), proxy)
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// GetExtensionURL retrieves the URL of an extension running locally based on the extension port table
|
||||
func (manager *Manager) GetExtensionURL(extensionID portainer.ExtensionID) string {
|
||||
return "http://localhost:" + extensionPorts[extensionID]
|
||||
return factory.BuildExtensionURL(extensionID)
|
||||
}
|
||||
|
||||
// DeleteExtensionProxy deletes the extension proxy associated to an extension identifier
|
||||
|
@ -124,6 +120,17 @@ func (manager *Manager) DeleteExtensionProxy(extensionID portainer.ExtensionID)
|
|||
manager.extensionProxies.Remove(strconv.Itoa(int(extensionID)))
|
||||
}
|
||||
|
||||
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies
|
||||
func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
|
||||
proxy, err := manager.proxyFactory.NewLegacyExtensionProxy(extensionAPIURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manager.legacyExtensionProxies.Set(key, proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// GetLegacyExtensionProxy returns a legacy extension proxy associated to a key
|
||||
func (manager *Manager) GetLegacyExtensionProxy(key string) http.Handler {
|
||||
proxy, ok := manager.legacyExtensionProxies.Get(key)
|
||||
|
@ -133,52 +140,7 @@ func (manager *Manager) GetLegacyExtensionProxy(key string) http.Handler {
|
|||
return proxy.(http.Handler)
|
||||
}
|
||||
|
||||
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies.
|
||||
func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
|
||||
extensionURL, err := url.Parse(extensionAPIURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := manager.proxyFactory.newHTTPProxy(extensionURL)
|
||||
manager.legacyExtensionProxies.Set(key, proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func (manager *Manager) createDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
baseURL := endpoint.URL
|
||||
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
baseURL = fmt.Sprintf("http://localhost:%d", tunnel.Port)
|
||||
}
|
||||
|
||||
endpointURL, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch endpoint.Type {
|
||||
case portainer.AgentOnDockerEnvironment:
|
||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, endpoint)
|
||||
case portainer.EdgeAgentEnvironment:
|
||||
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, endpoint), nil
|
||||
}
|
||||
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
|
||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, endpoint)
|
||||
}
|
||||
|
||||
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, endpoint), nil
|
||||
}
|
||||
|
||||
return manager.proxyFactory.newLocalProxy(endpointURL.Path, endpoint), nil
|
||||
}
|
||||
|
||||
func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
return newAzureProxy(&endpoint.AzureCredentials)
|
||||
}
|
||||
|
||||
return manager.createDockerProxy(endpoint)
|
||||
// CreateGitlabProxy creates a new HTTP reverse proxy that can be used to send requests to the Gitlab API
|
||||
func (manager *Manager) CreateGitlabProxy(url string) (http.Handler, error) {
|
||||
return manager.proxyFactory.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"
|
||||
)
|
||||
|
||||
// AuthorizedResourceControlDeletion ensure that the user can delete a resource control object.
|
||||
// A non-administrator user cannot delete a resource control where:
|
||||
// * the Public flag is false
|
||||
// * he is not one of the users in the user accesses
|
||||
// * he is not a member of any team within the team accesses
|
||||
func AuthorizedResourceControlDeletion(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||
if context.IsAdmin || resourceControl.Public {
|
||||
return true
|
||||
}
|
||||
|
||||
userAccessesCount := len(resourceControl.UserAccesses)
|
||||
teamAccessesCount := len(resourceControl.TeamAccesses)
|
||||
|
||||
if teamAccessesCount > 0 {
|
||||
for _, access := range resourceControl.TeamAccesses {
|
||||
for _, membership := range context.UserMemberships {
|
||||
if membership.TeamID == access.TeamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if userAccessesCount > 0 {
|
||||
for _, access := range resourceControl.UserAccesses {
|
||||
if access.UserID == context.UserID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// AuthorizedResourceControlAccess checks whether the user can alter an existing resource control.
|
||||
func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||
if context.IsAdmin || resourceControl.Public {
|
||||
|
@ -62,30 +28,22 @@ func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl,
|
|||
}
|
||||
|
||||
// AuthorizedResourceControlUpdate ensure that the user can update a resource control object.
|
||||
// It reuses the creation restrictions and adds extra checks.
|
||||
// A non-administrator user cannot update a resource control where:
|
||||
// * he wants to put one or more user in the user accesses
|
||||
func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||
userAccessesCount := len(resourceControl.UserAccesses)
|
||||
if !context.IsAdmin && userAccessesCount > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return AuthorizedResourceControlCreation(resourceControl, context)
|
||||
}
|
||||
|
||||
// AuthorizedResourceControlCreation ensure that the user can create a resource control object.
|
||||
// A non-administrator user cannot create a resource control where:
|
||||
// * the Public flag is set false
|
||||
// * the AdministatorsOnly flag is set to true
|
||||
// * he wants to create a resource control without any user/team accesses
|
||||
// * he wants to add more than one user in the user accesses
|
||||
// * he wants to add a user in the user accesses that is not corresponding to its id
|
||||
// * he wants to add a team he is not a member of
|
||||
func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||
func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||
if context.IsAdmin || resourceControl.Public {
|
||||
return true
|
||||
}
|
||||
|
||||
if resourceControl.AdministratorsOnly {
|
||||
return false
|
||||
}
|
||||
|
||||
userAccessesCount := len(resourceControl.UserAccesses)
|
||||
teamAccessesCount := len(resourceControl.TeamAccesses)
|
||||
|
||||
|
@ -133,15 +91,6 @@ func AuthorizedTeamManagement(teamID portainer.TeamID, context *RestrictedReques
|
|||
return false
|
||||
}
|
||||
|
||||
// AuthorizedUserManagement ensure that access to the management of the specified user is granted.
|
||||
// It will check if the user is either administrator or the owner of the user account.
|
||||
func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedRequestContext) bool {
|
||||
if context.IsAdmin || context.UserID == userID {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// authorizedEndpointAccess ensure that the user can access the specified endpoint.
|
||||
// It will check if the user is part of the authorized users or part of a team that is
|
||||
// listed in the authorized teams of the endpoint and the associated group.
|
||||
|
|
|
@ -87,6 +87,7 @@ func (server *Server) Start() error {
|
|||
proxyManagerParameters := &proxy.ManagerParams{
|
||||
ResourceControlService: server.ResourceControlService,
|
||||
UserService: server.UserService,
|
||||
TeamService: server.TeamService,
|
||||
TeamMembershipService: server.TeamMembershipService,
|
||||
SettingsService: server.SettingsService,
|
||||
RegistryService: server.RegistryService,
|
||||
|
@ -94,6 +95,7 @@ func (server *Server) Start() error {
|
|||
SignatureService: server.SignatureService,
|
||||
ReverseTunnelService: server.ReverseTunnelService,
|
||||
ExtensionService: server.ExtensionService,
|
||||
DockerClientFactory: server.DockerClientFactory,
|
||||
}
|
||||
proxyManager := proxy.NewManager(proxyManagerParameters)
|
||||
|
||||
|
@ -134,6 +136,7 @@ func (server *Server) Start() error {
|
|||
authHandler.EndpointGroupService = server.EndpointGroupService
|
||||
authHandler.RoleService = server.RoleService
|
||||
authHandler.ProxyManager = proxyManager
|
||||
authHandler.AuthorizationService = authorizationService
|
||||
|
||||
var roleHandler = roles.NewHandler(requestBouncer)
|
||||
roleHandler.RoleService = server.RoleService
|
||||
|
@ -214,6 +217,8 @@ func (server *Server) Start() error {
|
|||
stackHandler.RegistryService = server.RegistryService
|
||||
stackHandler.DockerHubService = server.DockerHubService
|
||||
stackHandler.SettingsService = server.SettingsService
|
||||
stackHandler.UserService = server.UserService
|
||||
stackHandler.ExtensionService = server.ExtensionService
|
||||
|
||||
var tagHandler = tags.NewHandler(requestBouncer)
|
||||
tagHandler.TagService = server.TagService
|
||||
|
|
|
@ -16,6 +16,10 @@ import (
|
|||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const (
|
||||
dockerClientVersion = "1.24"
|
||||
)
|
||||
|
||||
// ComposeStackManager represents a service for managing compose stacks.
|
||||
type ComposeStackManager struct {
|
||||
dataPath string
|
||||
|
@ -40,7 +44,7 @@ func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (
|
|||
|
||||
clientOpts := client.Options{
|
||||
Host: endpointURL,
|
||||
APIVersion: portainer.SupportedDockerAPIVersion,
|
||||
APIVersion: dockerClientVersion,
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
|
|
|
@ -175,14 +175,15 @@ type (
|
|||
|
||||
// Stack represents a Docker stack created via docker stack deploy
|
||||
Stack struct {
|
||||
ID StackID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Type StackType `json:"Type"`
|
||||
EndpointID EndpointID `json:"EndpointId"`
|
||||
SwarmID string `json:"SwarmId"`
|
||||
EntryPoint string `json:"EntryPoint"`
|
||||
Env []Pair `json:"Env"`
|
||||
ProjectPath string
|
||||
ID StackID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
Type StackType `json:"Type"`
|
||||
EndpointID EndpointID `json:"EndpointId"`
|
||||
SwarmID string `json:"SwarmId"`
|
||||
EntryPoint string `json:"EntryPoint"`
|
||||
Env []Pair `json:"Env"`
|
||||
ResourceControl *ResourceControl `json:"ResourceControl"`
|
||||
ProjectPath string
|
||||
}
|
||||
|
||||
// RegistryID represents a registry identifier
|
||||
|
@ -191,6 +192,13 @@ type (
|
|||
// RegistryType represents a type of registry
|
||||
RegistryType int
|
||||
|
||||
// GitlabRegistryData represents data required for gitlab registry to work
|
||||
GitlabRegistryData struct {
|
||||
ProjectID int `json:"ProjectId"`
|
||||
InstanceURL string `json:"InstanceURL"`
|
||||
ProjectPath string `json:"ProjectPath"`
|
||||
}
|
||||
|
||||
// Registry represents a Docker registry with all the info required
|
||||
// to connect to it
|
||||
Registry struct {
|
||||
|
@ -202,6 +210,7 @@ type (
|
|||
Username string `json:"Username"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
|
||||
Gitlab GitlabRegistryData `json:"Gitlab"`
|
||||
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||
|
||||
|
@ -295,6 +304,7 @@ type (
|
|||
Name string `json:"Name"`
|
||||
Description string `json:"Description"`
|
||||
Authorizations Authorizations `json:"Authorizations"`
|
||||
Priority int `json:"Priority"`
|
||||
}
|
||||
|
||||
// AccessPolicy represent a policy that can be associated to a user or team
|
||||
|
@ -439,21 +449,20 @@ type (
|
|||
|
||||
// ResourceControl represent a reference to a Docker resource with specific access controls
|
||||
ResourceControl struct {
|
||||
ID ResourceControlID `json:"Id"`
|
||||
ResourceID string `json:"ResourceId"`
|
||||
SubResourceIDs []string `json:"SubResourceIds"`
|
||||
Type ResourceControlType `json:"Type"`
|
||||
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
||||
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
||||
Public bool `json:"Public"`
|
||||
ID ResourceControlID `json:"Id"`
|
||||
ResourceID string `json:"ResourceId"`
|
||||
SubResourceIDs []string `json:"SubResourceIds"`
|
||||
Type ResourceControlType `json:"Type"`
|
||||
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
||||
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
||||
Public bool `json:"Public"`
|
||||
AdministratorsOnly bool `json:"AdministratorsOnly"`
|
||||
System bool `json:"System"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 2
|
||||
OwnerID UserID `json:"OwnerId,omitempty"`
|
||||
AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"`
|
||||
|
||||
// Deprecated in DBVersion == 14
|
||||
AdministratorsOnly bool `json:"AdministratorsOnly,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceControlType represents the type of resource associated to the resource control (volume, container, service...)
|
||||
|
@ -742,7 +751,7 @@ type (
|
|||
// ResourceControlService represents a service for managing resource control data
|
||||
ResourceControlService interface {
|
||||
ResourceControl(ID ResourceControlID) (*ResourceControl, error)
|
||||
ResourceControlByResourceID(resourceID string) (*ResourceControl, error)
|
||||
ResourceControlByResourceIDAndType(resourceID string, resourceType ResourceControlType) (*ResourceControl, error)
|
||||
ResourceControls() ([]ResourceControl, error)
|
||||
CreateResourceControl(rc *ResourceControl) error
|
||||
UpdateResourceControl(ID ResourceControlID, resourceControl *ResourceControl) error
|
||||
|
@ -883,9 +892,11 @@ type (
|
|||
// ExtensionManager represents a service used to manage extensions
|
||||
ExtensionManager interface {
|
||||
FetchExtensionDefinitions() ([]Extension, error)
|
||||
InstallExtension(extension *Extension, licenseKey string, archiveFileName string, extensionArchive []byte) error
|
||||
EnableExtension(extension *Extension, licenseKey string) error
|
||||
DisableExtension(extension *Extension) error
|
||||
UpdateExtension(extension *Extension, version string) error
|
||||
StartExtensions() error
|
||||
}
|
||||
|
||||
// ReverseTunnelService represensts a service used to manage reverse tunnel connections.
|
||||
|
@ -903,9 +914,9 @@ type (
|
|||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "1.22.1"
|
||||
APIVersion = "1.23.0"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 20
|
||||
DBVersion = 22
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||
|
@ -913,7 +924,7 @@ const (
|
|||
// VersionCheckURL represents the URL used to retrieve the latest version of Portainer
|
||||
VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest"
|
||||
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
||||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.22.1.json"
|
||||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-" + APIVersion + ".json"
|
||||
// SupportProductsURL represents the URL where Portainer support products can be retrieved
|
||||
SupportProductsURL = AssetsServerURL + "/support.json"
|
||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||
|
@ -929,12 +940,12 @@ const (
|
|||
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
||||
// to be used when communicating with an agent
|
||||
PortainerAgentSignatureMessage = "Portainer-App"
|
||||
// SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer
|
||||
SupportedDockerAPIVersion = "1.24"
|
||||
// ExtensionServer represents the server used by Portainer to communicate with extensions
|
||||
ExtensionServer = "localhost"
|
||||
// DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance
|
||||
DefaultEdgeAgentCheckinIntervalInSeconds = 5
|
||||
// LocalExtensionManifestFile represents the name of the local manifest file for extensions
|
||||
LocalExtensionManifestFile = "/extensions.json"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -1076,6 +1087,8 @@ const (
|
|||
AzureRegistry
|
||||
// CustomRegistry represents a custom registry
|
||||
CustomRegistry
|
||||
// GitlabRegistry represents a gitlab registry
|
||||
GitlabRegistry
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -54,7 +54,7 @@ info:
|
|||
|
||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||
|
||||
version: "1.22.1"
|
||||
version: "1.23.0"
|
||||
title: "Portainer API"
|
||||
contact:
|
||||
email: "info@portainer.io"
|
||||
|
@ -1235,7 +1235,7 @@ paths:
|
|||
summary: "Create a new resource control"
|
||||
description: |
|
||||
Create a new resource control to restrict access to a Docker resource.
|
||||
**Access policy**: restricted
|
||||
**Access policy**: administrator
|
||||
operationId: "ResourceControlCreate"
|
||||
consumes:
|
||||
- "application/json"
|
||||
|
@ -1343,7 +1343,7 @@ paths:
|
|||
summary: "Remove a resource control"
|
||||
description: |
|
||||
Remove a resource control.
|
||||
**Access policy**: restricted
|
||||
**Access policy**: administrator
|
||||
operationId: "ResourceControlDelete"
|
||||
security:
|
||||
- jwt: []
|
||||
|
@ -3174,7 +3174,7 @@ definitions:
|
|||
description: "Is analytics enabled"
|
||||
Version:
|
||||
type: "string"
|
||||
example: "1.22.1"
|
||||
example: "1.23.0"
|
||||
description: "Portainer API version"
|
||||
PublicSettingsInspectResponse:
|
||||
type: "object"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"packageName": "portainer",
|
||||
"packageVersion": "1.22.1",
|
||||
"packageVersion": "1.23.0",
|
||||
"projectName": "portainer"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import '../assets/css/app.css';
|
||||
import './libraries/isteven-angular-multiselect/isteven-multi-select.css';
|
||||
import angular from 'angular';
|
||||
|
||||
import './agent/_module';
|
||||
|
|
57
app/app.js
57
app/app.js
|
@ -1,25 +1,13 @@
|
|||
import _ from 'lodash-es';
|
||||
import $ from 'jquery';
|
||||
import '@babel/polyfill'
|
||||
|
||||
angular.module('portainer')
|
||||
.run(['$rootScope', '$state', '$interval', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
|
||||
function ($rootScope, $state, $interval, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) {
|
||||
.run(['$rootScope', '$state', '$interval', 'LocalStorage', 'EndpointProvider', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
|
||||
function ($rootScope, $state, $interval, LocalStorage, EndpointProvider, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) {
|
||||
'use strict';
|
||||
|
||||
EndpointProvider.initialize();
|
||||
|
||||
StateManager.initialize()
|
||||
.then(function success(state) {
|
||||
if (state.application.authentication) {
|
||||
initAuthentication(authManager, Authentication, $rootScope, $state);
|
||||
}
|
||||
if (state.application.analytics) {
|
||||
initAnalytics(Analytics, $rootScope);
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
||||
});
|
||||
|
||||
$rootScope.$state = $state;
|
||||
|
||||
// Workaround to prevent the loading bar from going backward
|
||||
|
@ -35,11 +23,23 @@ function ($rootScope, $state, $interval, Authentication, authManager, StateManag
|
|||
HttpRequestHelper.resetAgentHeaders();
|
||||
});
|
||||
|
||||
$state.defaultErrorHandler(function() {
|
||||
// Do not log transitionTo errors
|
||||
});
|
||||
|
||||
// Keep-alive Edge endpoints by sending a ping request every minute
|
||||
$interval(function() {
|
||||
ping(EndpointProvider, SystemService);
|
||||
}, 60 * 1000)
|
||||
|
||||
$(document).ajaxSend(function (event, jqXhr, jqOpts) {
|
||||
const type = jqOpts.type === 'POST' || jqOpts.type === 'PUT' || jqOpts.type === 'PATCH';
|
||||
const hasNoContentType = jqOpts.contentType !== 'application/json' && jqOpts.headers && !jqOpts.headers['Content-Type'];
|
||||
if (type && hasNoContentType) {
|
||||
jqXhr.setRequestHeader('Content-Type', 'application/json');
|
||||
}
|
||||
jqXhr.setRequestHeader('Authorization', 'Bearer ' + LocalStorage.getJWT());
|
||||
});
|
||||
}]);
|
||||
|
||||
function ping(EndpointProvider, SystemService) {
|
||||
|
@ -48,28 +48,3 @@ function ping(EndpointProvider, SystemService) {
|
|||
SystemService.ping(endpoint.Id);
|
||||
}
|
||||
}
|
||||
|
||||
function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
||||
authManager.checkAuthOnRefresh();
|
||||
Authentication.init();
|
||||
|
||||
// The unauthenticated event is broadcasted by the jwtInterceptor when
|
||||
// hitting a 401. We're using this instead of the usual combination of
|
||||
// authManager.redirectWhenUnauthenticated() + unauthenticatedRedirector
|
||||
// to have more controls on which URL should trigger the unauthenticated state.
|
||||
$rootScope.$on('unauthenticated', function (event, data) {
|
||||
if (!_.includes(data.config.url, '/v2/')) {
|
||||
$state.go('portainer.auth', { error: 'Your session has expired' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initAnalytics(Analytics, $rootScope) {
|
||||
Analytics.offline(false);
|
||||
Analytics.registerScriptTags();
|
||||
Analytics.registerTrackers();
|
||||
$rootScope.$on('$stateChangeSuccess', function (event, toState) {
|
||||
Analytics.trackPage(toState.url);
|
||||
Analytics.pageView();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
<td ng-if="$ctrl.showOwnershipColumn">
|
||||
<span>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -272,7 +272,7 @@
|
|||
<td ng-if="$ctrl.showOwnershipColumn" ng-show="$ctrl.columnVisibility.columns.ownership.display">
|
||||
<span>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<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>
|
||||
<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>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||
<td>{{ item.Scope }}</td>
|
||||
|
@ -22,6 +23,6 @@
|
|||
<td ng-if="parentCtrl.showOwnershipColumn">
|
||||
<span>
|
||||
<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>
|
||||
</td>
|
|
@ -1,3 +1,5 @@
|
|||
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.directive('networkRowContent', [function networkRowContent() {
|
||||
var directive = {
|
||||
|
@ -9,6 +11,9 @@ angular.module('portainer.docker')
|
|||
parentCtrl: '<',
|
||||
allowCheckbox: '<',
|
||||
allowExpand: '<'
|
||||
},
|
||||
controller: ($scope) => {
|
||||
$scope.RCO = RCO;
|
||||
}
|
||||
};
|
||||
return directive;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS', 'DatatableService',
|
||||
function ($scope, $controller, PREDEFINED_NETWORKS, DatatableService) {
|
||||
.controller('NetworksDatatableController', ['$scope', '$controller', 'NetworkHelper', 'DatatableService',
|
||||
function ($scope, $controller, NetworkHelper, DatatableService) {
|
||||
|
||||
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||
|
||||
this.disableRemove = function(item) {
|
||||
return PREDEFINED_NETWORKS.includes(item.Name);
|
||||
return NetworkHelper.isSystemNetwork(item);
|
||||
};
|
||||
|
||||
this.state = Object.assign(this.state, {
|
||||
|
@ -15,7 +15,7 @@ angular.module('portainer.docker')
|
|||
})
|
||||
|
||||
/**
|
||||
* Do not allow PREDEFINED_NETWORKS to be selected
|
||||
* Do not allow system networks to be selected
|
||||
*/
|
||||
this.allowSelection = function(item) {
|
||||
return !this.disableRemove(item);
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
<td ng-if="$ctrl.showOwnershipColumn">
|
||||
<span>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -156,7 +156,7 @@
|
|||
<td ng-if="$ctrl.showOwnershipColumn">
|
||||
<span>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue