chore(docker): clean up the code EE-7325 (#11997)

pull/12017/head
andres-portainer 2024-07-05 16:19:46 -03:00 committed by GitHub
parent faca64442f
commit 340830d121
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 280 additions and 254 deletions

View File

@ -7,6 +7,7 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/utils" "github.com/portainer/portainer/api/http/proxy/factory/utils"
"github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/slicesx"
"github.com/portainer/portainer/api/stacks/stackutils" "github.com/portainer/portainer/api/stacks/stackutils"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -23,7 +24,8 @@ const (
type ( type (
resourceLabelsObjectSelector func(map[string]any) map[string]any resourceLabelsObjectSelector func(map[string]any) map[string]any
resourceOperationParameters struct {
resourceOperationParameters struct {
resourceIdentifierAttribute string resourceIdentifierAttribute string
resourceType portainer.ResourceControlType resourceType portainer.ResourceControlType
labelsObjectSelector resourceLabelsObjectSelector labelsObjectSelector resourceLabelsObjectSelector
@ -31,28 +33,18 @@ type (
) )
func getUniqueElements(items string) []string { func getUniqueElements(items string) []string {
result := []string{} xs := strings.Split(items, ",")
seen := make(map[string]struct{}) xs = slicesx.Map(xs, strings.TrimSpace)
for _, item := range strings.Split(items, ",") { xs = slicesx.Filter(xs, func(x string) bool { return len(x) > 0 })
v := strings.TrimSpace(item)
if v == "" {
continue
}
if _, ok := seen[v]; !ok {
result = append(result, v)
seen[v] = struct{}{}
}
}
return result return slicesx.Unique(xs)
} }
func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject map[string]any, resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) { func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject map[string]any, resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
if labelsObject[resourceLabelForPortainerPublicResourceControl] != nil { if labelsObject[resourceLabelForPortainerPublicResourceControl] != nil {
resourceControl := authorization.NewPublicResourceControl(resourceID, resourceType) resourceControl := authorization.NewPublicResourceControl(resourceID, resourceType)
err := transport.dataStore.ResourceControl().Create(resourceControl) if err := transport.dataStore.ResourceControl().Create(resourceControl); err != nil {
if err != nil {
return nil, err return nil, err
} }
@ -61,6 +53,7 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m
teamNames := make([]string, 0) teamNames := make([]string, 0)
userNames := make([]string, 0) userNames := make([]string, 0)
if labelsObject[resourceLabelForPortainerTeamResourceControl] != nil { if labelsObject[resourceLabelForPortainerTeamResourceControl] != nil {
concatenatedTeamNames := labelsObject[resourceLabelForPortainerTeamResourceControl].(string) concatenatedTeamNames := labelsObject[resourceLabelForPortainerTeamResourceControl].(string)
teamNames = getUniqueElements(concatenatedTeamNames) teamNames = getUniqueElements(concatenatedTeamNames)
@ -71,48 +64,48 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m
userNames = getUniqueElements(concatenatedUserNames) userNames = getUniqueElements(concatenatedUserNames)
} }
if len(teamNames) > 0 || len(userNames) > 0 { if len(teamNames) == 0 && len(userNames) == 0 {
teamIDs := make([]portainer.TeamID, 0) return nil, nil
userIDs := make([]portainer.UserID, 0)
for _, name := range teamNames {
team, err := transport.dataStore.Team().TeamByName(name)
if err != nil {
log.Warn().
Str("name", name).
Str("resource_id", resourceID).
Msg("unknown team name in access control label, ignoring access control rule for this team")
continue
}
teamIDs = append(teamIDs, team.ID)
}
for _, name := range userNames {
user, err := transport.dataStore.User().UserByUsername(name)
if err != nil {
log.Warn().
Str("name", name).
Str("resource_id", resourceID).
Msg("unknown user name in access control label, ignoring access control rule for this user")
continue
}
userIDs = append(userIDs, user.ID)
}
resourceControl := authorization.NewRestrictedResourceControl(resourceID, resourceType, userIDs, teamIDs)
if err := transport.dataStore.ResourceControl().Create(resourceControl); err != nil {
return nil, err
}
return resourceControl, nil
} }
return nil, nil teamIDs := make([]portainer.TeamID, 0)
userIDs := make([]portainer.UserID, 0)
for _, name := range teamNames {
team, err := transport.dataStore.Team().TeamByName(name)
if err != nil {
log.Warn().
Str("name", name).
Str("resource_id", resourceID).
Msg("unknown team name in access control label, ignoring access control rule for this team")
continue
}
teamIDs = append(teamIDs, team.ID)
}
for _, name := range userNames {
user, err := transport.dataStore.User().UserByUsername(name)
if err != nil {
log.Warn().
Str("name", name).
Str("resource_id", resourceID).
Msg("unknown user name in access control label, ignoring access control rule for this user")
continue
}
userIDs = append(userIDs, user.ID)
}
resourceControl := authorization.NewRestrictedResourceControl(resourceID, resourceType, userIDs, teamIDs)
if err := transport.dataStore.ResourceControl().Create(resourceControl); err != nil {
return nil, err
}
return resourceControl, nil
} }
func (transport *Transport) createPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) (*portainer.ResourceControl, error) { func (transport *Transport) createPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) (*portainer.ResourceControl, error) {
@ -298,40 +291,40 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
return resourceControl, nil return resourceControl, nil
} }
if resourceLabelsObject != nil { if resourceLabelsObject == nil {
if resourceLabelsObject[resourceLabelForDockerServiceID] != nil { return nil, nil
inheritedServiceIdentifier := resourceLabelsObject[resourceLabelForDockerServiceID].(string)
resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedServiceIdentifier, portainer.ServiceResourceControl, resourceControls)
if resourceControl != nil {
return resourceControl, nil
}
}
if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != nil {
stackName := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string)
stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName)
resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls)
if resourceControl != nil {
return resourceControl, nil
}
}
if resourceLabelsObject[resourceLabelForDockerComposeStackName] != nil {
stackName := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string)
stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName)
resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls)
if resourceControl != nil {
return resourceControl, nil
}
}
return transport.newResourceControlFromPortainerLabels(resourceLabelsObject, resourceIdentifier, resourceType)
} }
return nil, nil if resourceLabelsObject[resourceLabelForDockerServiceID] != nil {
inheritedServiceIdentifier := resourceLabelsObject[resourceLabelForDockerServiceID].(string)
resourceControl = authorization.GetResourceControlByResourceIDAndType(inheritedServiceIdentifier, portainer.ServiceResourceControl, resourceControls)
if resourceControl != nil {
return resourceControl, nil
}
}
if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != nil {
stackName := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string)
stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName)
resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls)
if resourceControl != nil {
return resourceControl, nil
}
}
if resourceLabelsObject[resourceLabelForDockerComposeStackName] != nil {
stackName := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string)
stackResourceID := stackutils.ResourceControlID(transport.endpoint.ID, stackName)
resourceControl = authorization.GetResourceControlByResourceIDAndType(stackResourceID, portainer.StackResourceControl, resourceControls)
if resourceControl != nil {
return resourceControl, nil
}
}
return transport.newResourceControlFromPortainerLabels(resourceLabelsObject, resourceIdentifier, resourceType)
} }
func getStackResourceIDFromLabels(resourceLabelsObject map[string]string, endpointID portainer.EndpointID) string { func getStackResourceIDFromLabels(resourceLabelsObject map[string]string, endpointID portainer.EndpointID) string {

View File

@ -19,6 +19,8 @@ type postDockerfileRequest struct {
Content string Content string
} }
var ErrUploadedFilesNotFound = errors.New("uploaded files not found to build image")
// buildOperation inspects the "Content-Type" header to determine if it needs to alter the request. // buildOperation inspects the "Content-Type" header to determine if it needs to alter the request.
// //
// If the value of the header is empty, it means that a Dockerfile is posted via upload, the function // If the value of the header is empty, it means that a Dockerfile is posted via upload, the function
@ -69,13 +71,12 @@ func buildOperation(request *http.Request) error {
} }
case "multipart/form-data": case "multipart/form-data":
err := request.ParseMultipartForm(32 * OneMegabyte) // limit parser memory to 32MB if err := request.ParseMultipartForm(32 * OneMegabyte); err != nil {
if err != nil {
return err return err
} }
if request.MultipartForm == nil || request.MultipartForm.File == nil { if request.MultipartForm == nil || request.MultipartForm.File == nil {
return errors.New("uploaded files not found to build image") return ErrUploadedFilesNotFound
} }
tfb := archive.NewTarFileInBuffer() tfb := archive.NewTarFileInBuffer()

View File

@ -19,6 +19,15 @@ import (
const containerObjectIdentifier = "Id" const containerObjectIdentifier = "Id"
var (
ErrPrivilegedModeForbidden = errors.New("forbidden to use privileged mode")
ErrPIDHostNamespaceForbidden = errors.New("forbidden to use pid host namespace")
ErrDeviceMappingForbidden = errors.New("forbidden to use device mapping")
ErrSysCtlSettingsForbidden = errors.New("forbidden to use sysctl settings")
ErrContainerCapabilitiesForbidden = errors.New("forbidden to use container capabilities")
ErrBindMountsForbidden = errors.New("forbidden to use bind mounts")
)
func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, endpointID portainer.EndpointID, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) { func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, endpointID portainer.EndpointID, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
container, err := dockerClient.ContainerInspect(context.Background(), containerID) container, err := dockerClient.ContainerInspect(context.Background(), containerID)
if err != nil { if err != nil {
@ -108,6 +117,7 @@ func selectorContainerLabelsFromContainerInspectOperation(responseObject map[str
containerLabelsObject := utils.GetJSONObject(containerConfigObject, "Labels") containerLabelsObject := utils.GetJSONObject(containerConfigObject, "Labels")
return containerLabelsObject return containerLabelsObject
} }
return nil return nil
} }
@ -117,6 +127,7 @@ func selectorContainerLabelsFromContainerInspectOperation(responseObject map[str
// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList // API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
func selectorContainerLabelsFromContainerListOperation(responseObject map[string]any) map[string]any { func selectorContainerLabelsFromContainerListOperation(responseObject map[string]any) map[string]any {
containerLabelsObject := utils.GetJSONObject(responseObject, "Labels") containerLabelsObject := utils.GetJSONObject(responseObject, "Labels")
return containerLabelsObject return containerLabelsObject
} }
@ -129,13 +140,12 @@ func filterContainersWithBlackListedLabels(containerData []any, labelBlackList [
containerObject := container.(map[string]any) containerObject := container.(map[string]any)
containerLabels := selectorContainerLabelsFromContainerListOperation(containerObject) containerLabels := selectorContainerLabelsFromContainerListOperation(containerObject)
if containerLabels != nil {
if !containerHasBlackListedLabel(containerLabels, labelBlackList) { if containerHasBlackListedLabel(containerLabels, labelBlackList) {
filteredContainerData = append(filteredContainerData, containerObject) continue
}
} else {
filteredContainerData = append(filteredContainerData, containerObject)
} }
filteredContainerData = append(filteredContainerData, containerObject)
} }
return filteredContainerData, nil return filteredContainerData, nil
@ -195,35 +205,34 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
} }
partialContainer := &PartialContainer{} partialContainer := &PartialContainer{}
err = json.Unmarshal(body, partialContainer) if err := json.Unmarshal(body, partialContainer); err != nil {
if err != nil {
return nil, err return nil, err
} }
if !securitySettings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged { if !securitySettings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
return forbiddenResponse, errors.New("forbidden to use privileged mode") return forbiddenResponse, ErrPrivilegedModeForbidden
} }
if !securitySettings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" { if !securitySettings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
return forbiddenResponse, errors.New("forbidden to use pid host namespace") return forbiddenResponse, ErrPIDHostNamespaceForbidden
} }
if !securitySettings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 { if !securitySettings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 {
return forbiddenResponse, errors.New("forbidden to use device mapping") return forbiddenResponse, ErrDeviceMappingForbidden
} }
if !securitySettings.AllowSysctlSettingForRegularUsers && len(partialContainer.HostConfig.Sysctls) > 0 { if !securitySettings.AllowSysctlSettingForRegularUsers && len(partialContainer.HostConfig.Sysctls) > 0 {
return forbiddenResponse, errors.New("forbidden to use sysctl settings") return forbiddenResponse, ErrSysCtlSettingsForbidden
} }
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) { if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
return nil, errors.New("forbidden to use container capabilities") return nil, ErrContainerCapabilitiesForbidden
} }
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) { if !securitySettings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
for _, bind := range partialContainer.HostConfig.Binds { for _, bind := range partialContainer.HostConfig.Binds {
if strings.HasPrefix(bind, "/") { if strings.HasPrefix(bind, "/") {
return forbiddenResponse, errors.New("forbidden to use bind mounts") return forbiddenResponse, ErrBindMountsForbidden
} }
} }
} }

View File

@ -17,12 +17,15 @@ func init() {
func (transport *Transport) applyPortainerContainers(resources []any) ([]any, error) { func (transport *Transport) applyPortainerContainers(resources []any) ([]any, error) {
decoratedResourceData := make([]any, 0) decoratedResourceData := make([]any, 0)
for _, resource := range resources { for _, resource := range resources {
responseObject, ok := resource.(map[string]any) responseObject, ok := resource.(map[string]any)
if !ok { if !ok {
decoratedResourceData = append(decoratedResourceData, resource) decoratedResourceData = append(decoratedResourceData, resource)
continue continue
} }
responseObject, _ = transport.applyPortainerContainer(responseObject) responseObject, _ = transport.applyPortainerContainer(responseObject)
decoratedResourceData = append(decoratedResourceData, responseObject) decoratedResourceData = append(decoratedResourceData, responseObject)
} }

View File

@ -29,31 +29,36 @@ type (
func createRegistryAuthenticationHeader( func createRegistryAuthenticationHeader(
dataStore dataservices.DataStore, dataStore dataservices.DataStore,
registryId portainer.RegistryID, registryID portainer.RegistryID,
accessContext *registryAccessContext, accessContext *registryAccessContext,
) (authenticationHeader registryAuthenticationHeader, err error) { ) (authenticationHeader registryAuthenticationHeader, err error) {
if registryId == 0 { // dockerhub (anonymous) if registryID == 0 { // dockerhub (anonymous)
authenticationHeader.Serveraddress = "docker.io" authenticationHeader.Serveraddress = "docker.io"
} else { // any "custom" registry
var matchingRegistry *portainer.Registry return
for _, registry := range accessContext.registries { }
registry := registry
if registry.ID == registryId && // Any "custom" registry
(accessContext.isAdmin || var matchingRegistry *portainer.Registry
security.AuthorizedRegistryAccess(&registry, accessContext.user, accessContext.teamMemberships, accessContext.endpointID)) {
matchingRegistry = &registry for _, registry := range accessContext.registries {
break registry := registry
} if registry.ID == registryID &&
(accessContext.isAdmin ||
security.AuthorizedRegistryAccess(&registry, accessContext.user, accessContext.teamMemberships, accessContext.endpointID)) {
matchingRegistry = &registry
break
}
}
if matchingRegistry != nil {
if err = registryutils.EnsureRegTokenValid(dataStore, matchingRegistry); err != nil {
return
} }
if matchingRegistry != nil { authenticationHeader.Serveraddress = matchingRegistry.URL
err = registryutils.EnsureRegTokenValid(dataStore, matchingRegistry) authenticationHeader.Username, authenticationHeader.Password, err = registryutils.GetRegEffectiveCredential(matchingRegistry)
if err != nil {
return
}
authenticationHeader.Serveraddress = matchingRegistry.URL
authenticationHeader.Username, authenticationHeader.Password, err = registryutils.GetRegEffectiveCredential(matchingRegistry)
}
} }
return return

View File

@ -59,8 +59,8 @@ func (transport *Transport) serviceListOperation(response *http.Response, execut
// serviceInspectOperation extracts the response as a JSON object, verify that the user // 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. // 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 { func (transport *Transport) serviceInspectOperation(response *http.Response, executor *operationExecutor) error {
//ServiceInspect response is a JSON object // ServiceInspect response is a JSON object
//https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect // https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
responseObject, err := utils.GetResponseAsJSONObject(response) responseObject, err := utils.GetResponseAsJSONObject(response)
if err != nil { if err != nil {
return err return err
@ -85,6 +85,7 @@ func selectorServiceLabels(responseObject map[string]any) map[string]any {
if serviceSpecObject != nil { if serviceSpecObject != nil {
return utils.GetJSONObject(serviceSpecObject, "Labels") return utils.GetJSONObject(serviceSpecObject, "Labels")
} }
return nil return nil
} }
@ -108,33 +109,34 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque
return nil, err return nil, err
} }
if !isAdminOrEndpointAdmin { if isAdminOrEndpointAdmin {
securitySettings, err := transport.fetchEndpointSecuritySettings() return transport.replaceRegistryAuthenticationHeader(request)
if err != nil { }
return nil, err
}
body, err := io.ReadAll(request.Body) securitySettings, err := transport.fetchEndpointSecuritySettings()
if err != nil { if err != nil {
return nil, err return nil, err
} }
partialService := &PartialService{} body, err := io.ReadAll(request.Body)
err = json.Unmarshal(body, partialService) if err != nil {
if err != nil { return nil, err
return nil, err }
}
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) { partialService := &PartialService{}
for _, mount := range partialService.TaskTemplate.ContainerSpec.Mounts { if err := json.Unmarshal(body, partialService); err != nil {
if mount.Type == "bind" { return nil, err
return forbiddenResponse, errors.New("forbidden to use bind mounts") }
}
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) {
for _, mount := range partialService.TaskTemplate.ContainerSpec.Mounts {
if mount.Type == "bind" {
return forbiddenResponse, errors.New("forbidden to use bind mounts")
} }
} }
request.Body = io.NopCloser(bytes.NewBuffer(body))
} }
request.Body = io.NopCloser(bytes.NewBuffer(body))
return transport.replaceRegistryAuthenticationHeader(request) return transport.replaceRegistryAuthenticationHeader(request)
} }

View File

@ -59,6 +59,7 @@ type (
operationContext *restrictedDockerOperationContext operationContext *restrictedDockerOperationContext
labelBlackList []portainer.Pair labelBlackList []portainer.Pair
} }
restrictedOperationRequest func(*http.Response, *operationExecutor) error restrictedOperationRequest func(*http.Response, *operationExecutor) error
operationRequest func(*http.Request) error operationRequest func(*http.Request) error
) )
@ -85,18 +86,18 @@ func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, er
} }
var prefixProxyFuncMap = map[string]func(*Transport, *http.Request, string) (*http.Response, error){ var prefixProxyFuncMap = map[string]func(*Transport, *http.Request, string) (*http.Response, error){
"build": (*Transport).proxyBuildRequest,
"configs": (*Transport).proxyConfigRequest, "configs": (*Transport).proxyConfigRequest,
"containers": (*Transport).proxyContainerRequest, "containers": (*Transport).proxyContainerRequest,
"services": (*Transport).proxyServiceRequest,
"volumes": (*Transport).proxyVolumeRequest,
"networks": (*Transport).proxyNetworkRequest,
"secrets": (*Transport).proxySecretRequest,
"swarm": (*Transport).proxySwarmRequest,
"nodes": (*Transport).proxyNodeRequest,
"tasks": (*Transport).proxyTaskRequest,
"build": (*Transport).proxyBuildRequest,
"images": (*Transport).proxyImageRequest, "images": (*Transport).proxyImageRequest,
"networks": (*Transport).proxyNetworkRequest,
"nodes": (*Transport).proxyNodeRequest,
"secrets": (*Transport).proxySecretRequest,
"services": (*Transport).proxyServiceRequest,
"swarm": (*Transport).proxySwarmRequest,
"tasks": (*Transport).proxyTaskRequest,
"v2": (*Transport).proxyAgentRequest, "v2": (*Transport).proxyAgentRequest,
"volumes": (*Transport).proxyVolumeRequest,
} }
// ProxyDockerRequest intercepts a Docker API request and apply logic based // ProxyDockerRequest intercepts a Docker API request and apply logic based
@ -119,6 +120,7 @@ func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Res
if proxyFunc := prefixProxyFuncMap[prefix]; proxyFunc != nil { if proxyFunc := prefixProxyFuncMap[prefix]; proxyFunc != nil {
return proxyFunc(transport, request, unversionedPath) return proxyFunc(transport, request, unversionedPath)
} }
return transport.executeDockerRequest(request) return transport.executeDockerRequest(request)
} }
@ -141,7 +143,7 @@ func (transport *Transport) proxyAgentRequest(r *http.Request, unversionedPath s
switch { switch {
case strings.HasPrefix(requestPath, "/browse"): case strings.HasPrefix(requestPath, "/browse"):
// host file browser request // Host file browser request
volumeIDParameter, found := r.URL.Query()["volumeID"] volumeIDParameter, found := r.URL.Query()["volumeID"]
if !found || len(volumeIDParameter) < 1 { if !found || len(volumeIDParameter) < 1 {
return transport.administratorOperation(r) return transport.administratorOperation(r)
@ -154,7 +156,7 @@ func (transport *Transport) proxyAgentRequest(r *http.Request, unversionedPath s
return nil, err return nil, err
} }
// volume browser request // Volume browser request
return transport.restrictedResourceOperation(r, resourceID, volumeName, portainer.VolumeResourceControl, true) return transport.restrictedResourceOperation(r, resourceID, volumeName, portainer.VolumeResourceControl, true)
case strings.HasPrefix(requestPath, "/dockerhub"): case strings.HasPrefix(requestPath, "/dockerhub"):
requestPath, registryIdString := path.Split(r.URL.Path) requestPath, registryIdString := path.Split(r.URL.Path)
@ -206,7 +208,7 @@ func (transport *Transport) proxyConfigRequest(request *http.Request, unversione
return transport.rewriteOperation(request, transport.configListOperation) return transport.rewriteOperation(request, transport.configListOperation)
default: default:
// assume /configs/{id} // Assume /configs/{id}
configID := path.Base(requestPath) configID := path.Base(requestPath)
if request.Method == http.MethodGet { if request.Method == http.MethodGet {
@ -242,6 +244,7 @@ func (transport *Transport) proxyContainerRequest(request *http.Request, unversi
if action == "json" { if action == "json" {
return transport.rewriteOperation(request, transport.containerInspectOperation) return transport.rewriteOperation(request, transport.containerInspectOperation)
} }
return transport.restrictedResourceOperation(request, containerID, containerID, portainer.ContainerResourceControl, false) return transport.restrictedResourceOperation(request, containerID, containerID, portainer.ContainerResourceControl, false)
} else if match, _ := path.Match("/containers/*", requestPath); match { } else if match, _ := path.Match("/containers/*", requestPath); match {
// Handle /containers/{id} requests // Handle /containers/{id} requests
@ -253,6 +256,7 @@ func (transport *Transport) proxyContainerRequest(request *http.Request, unversi
return transport.restrictedResourceOperation(request, containerID, containerID, portainer.ContainerResourceControl, false) return transport.restrictedResourceOperation(request, containerID, containerID, portainer.ContainerResourceControl, false)
} }
return transport.executeDockerRequest(request) return transport.executeDockerRequest(request)
} }
} }
@ -273,6 +277,7 @@ func (transport *Transport) proxyServiceRequest(request *http.Request, unversion
// Handle /services/{id}/{action} requests // Handle /services/{id}/{action} requests
serviceID := path.Base(path.Dir(requestPath)) serviceID := path.Base(path.Dir(requestPath))
transport.decorateRegistryAuthenticationHeader(request) transport.decorateRegistryAuthenticationHeader(request)
return transport.restrictedResourceOperation(request, serviceID, serviceID, portainer.ServiceResourceControl, false) return transport.restrictedResourceOperation(request, serviceID, serviceID, portainer.ServiceResourceControl, false)
} else if match, _ := path.Match("/services/*", requestPath); match { } else if match, _ := path.Match("/services/*", requestPath); match {
// Handle /services/{id} requests // Handle /services/{id} requests
@ -284,8 +289,10 @@ func (transport *Transport) proxyServiceRequest(request *http.Request, unversion
case http.MethodDelete: case http.MethodDelete:
return transport.executeGenericResourceDeletionOperation(request, serviceID, serviceID, portainer.ServiceResourceControl) return transport.executeGenericResourceDeletionOperation(request, serviceID, serviceID, portainer.ServiceResourceControl)
} }
return transport.restrictedResourceOperation(request, serviceID, serviceID, portainer.ServiceResourceControl, false) return transport.restrictedResourceOperation(request, serviceID, serviceID, portainer.ServiceResourceControl, false)
} }
return transport.executeDockerRequest(request) return transport.executeDockerRequest(request)
} }
} }
@ -304,7 +311,7 @@ func (transport *Transport) proxyVolumeRequest(request *http.Request, unversione
return transport.rewriteOperation(request, transport.volumeListOperation) return transport.rewriteOperation(request, transport.volumeListOperation)
default: default:
// assume /volumes/{name} // Assume /volumes/{name}
return transport.restrictedVolumeOperation(requestPath, request) return transport.restrictedVolumeOperation(requestPath, request)
} }
} }
@ -320,7 +327,7 @@ func (transport *Transport) proxyNetworkRequest(request *http.Request, unversion
return transport.rewriteOperation(request, transport.networkListOperation) return transport.rewriteOperation(request, transport.networkListOperation)
default: default:
// assume /networks/{id} // Assume /networks/{id}
networkID := path.Base(requestPath) networkID := path.Base(requestPath)
if request.Method == http.MethodGet { if request.Method == http.MethodGet {
@ -328,6 +335,7 @@ func (transport *Transport) proxyNetworkRequest(request *http.Request, unversion
} else if request.Method == http.MethodDelete { } else if request.Method == http.MethodDelete {
return transport.executeGenericResourceDeletionOperation(request, networkID, networkID, portainer.NetworkResourceControl) return transport.executeGenericResourceDeletionOperation(request, networkID, networkID, portainer.NetworkResourceControl)
} }
return transport.restrictedResourceOperation(request, networkID, networkID, portainer.NetworkResourceControl, false) return transport.restrictedResourceOperation(request, networkID, networkID, portainer.NetworkResourceControl, false)
} }
} }
@ -343,7 +351,7 @@ func (transport *Transport) proxySecretRequest(request *http.Request, unversione
return transport.rewriteOperation(request, transport.secretListOperation) return transport.rewriteOperation(request, transport.secretListOperation)
default: default:
// assume /secrets/{id} // Assume /secrets/{id}
secretID := path.Base(requestPath) secretID := path.Base(requestPath)
if request.Method == http.MethodGet { if request.Method == http.MethodGet {
@ -351,6 +359,7 @@ func (transport *Transport) proxySecretRequest(request *http.Request, unversione
} else if request.Method == http.MethodDelete { } else if request.Method == http.MethodDelete {
return transport.executeGenericResourceDeletionOperation(request, secretID, secretID, portainer.SecretResourceControl) return transport.executeGenericResourceDeletionOperation(request, secretID, secretID, portainer.SecretResourceControl)
} }
return transport.restrictedResourceOperation(request, secretID, secretID, portainer.SecretResourceControl, false) return transport.restrictedResourceOperation(request, secretID, secretID, portainer.SecretResourceControl, false)
} }
} }
@ -358,7 +367,7 @@ func (transport *Transport) proxySecretRequest(request *http.Request, unversione
func (transport *Transport) proxyNodeRequest(request *http.Request, unversionedPath string) (*http.Response, error) { func (transport *Transport) proxyNodeRequest(request *http.Request, unversionedPath string) (*http.Response, error) {
requestPath := unversionedPath requestPath := unversionedPath
// assume /nodes/{id} // Assume /nodes/{id}
if path.Base(requestPath) != "nodes" { if path.Base(requestPath) != "nodes" {
return transport.administratorOperation(request) return transport.administratorOperation(request)
} }
@ -373,7 +382,7 @@ func (transport *Transport) proxySwarmRequest(request *http.Request, unversioned
case "/swarm": case "/swarm":
return transport.rewriteOperation(request, swarmInspectOperation) return transport.rewriteOperation(request, swarmInspectOperation)
default: default:
// assume /swarm/{action} // Assume /swarm/{action}
return transport.administratorOperation(request) return transport.administratorOperation(request)
} }
} }
@ -385,34 +394,38 @@ func (transport *Transport) proxyTaskRequest(request *http.Request, unversionedP
case "/tasks": case "/tasks":
return transport.rewriteOperation(request, transport.taskListOperation) return transport.rewriteOperation(request, transport.taskListOperation)
default: default:
// assume /tasks/{id} // Assume /tasks/{id}
return transport.executeDockerRequest(request) return transport.executeDockerRequest(request)
} }
} }
func (transport *Transport) proxyBuildRequest(request *http.Request, _ string) (*http.Response, error) { func (transport *Transport) proxyBuildRequest(request *http.Request, _ string) (*http.Response, error) {
err := transport.updateDefaultGitBranch(request) if err := transport.updateDefaultGitBranch(request); err != nil {
if err != nil {
return nil, err return nil, err
} }
return transport.interceptAndRewriteRequest(request, buildOperation) return transport.interceptAndRewriteRequest(request, buildOperation)
} }
func (transport *Transport) updateDefaultGitBranch(request *http.Request) error { func (transport *Transport) updateDefaultGitBranch(request *http.Request) error {
remote := request.URL.Query().Get("remote") remote := request.URL.Query().Get("remote")
if strings.HasSuffix(remote, ".git") {
repositoryURL := remote[:len(remote)-4]
latestCommitID, err := transport.gitService.LatestCommitID(repositoryURL, "", "", "", false)
if err != nil {
return err
}
newRemote := fmt.Sprintf("%s#%s", remote, latestCommitID)
q := request.URL.Query() if !strings.HasSuffix(remote, ".git") {
q.Set("remote", newRemote) return nil
request.URL.RawQuery = q.Encode()
} }
repositoryURL := remote[:len(remote)-4]
latestCommitID, err := transport.gitService.LatestCommitID(repositoryURL, "", "", "", false)
if err != nil {
return err
}
newRemote := fmt.Sprintf("%s#%s", remote, latestCommitID)
q := request.URL.Query()
q.Set("remote", newRemote)
request.URL.RawQuery = q.Encode()
return nil return nil
} }
@ -426,6 +439,7 @@ func (transport *Transport) proxyImageRequest(request *http.Request, unversioned
if path.Base(requestPath) == "push" && request.Method == http.MethodPost { if path.Base(requestPath) == "push" && request.Method == http.MethodPost {
return transport.replaceRegistryAuthenticationHeader(request) return transport.replaceRegistryAuthenticationHeader(request)
} }
return transport.executeDockerRequest(request) return transport.executeDockerRequest(request)
} }
} }
@ -444,47 +458,46 @@ func (transport *Transport) decorateRegistryAuthenticationHeader(request *http.R
originalHeader := request.Header.Get("X-Registry-Auth") originalHeader := request.Header.Get("X-Registry-Auth")
if originalHeader != "" { if originalHeader == "" {
return nil
decodedHeaderData, err := base64.StdEncoding.DecodeString(originalHeader)
if err != nil {
return err
}
var originalHeaderData portainerRegistryAuthenticationHeader
err = json.Unmarshal(decodedHeaderData, &originalHeaderData)
if err != nil {
return err
}
// delete header and exist function without error if Front End
// passes empty json. This is to restore original behavior which
// never originally passed this header
if string(decodedHeaderData) == "{}" {
request.Header.Del("X-Registry-Auth")
return nil
}
// only set X-Registry-Auth if registryId is defined
if originalHeaderData.RegistryId == nil {
return nil
}
authenticationHeader, err := createRegistryAuthenticationHeader(transport.dataStore, *originalHeaderData.RegistryId, accessContext)
if err != nil {
return err
}
headerData, err := json.Marshal(authenticationHeader)
if err != nil {
return err
}
header := base64.URLEncoding.EncodeToString(headerData)
request.Header.Set("X-Registry-Auth", header)
} }
decodedHeaderData, err := base64.StdEncoding.DecodeString(originalHeader)
if err != nil {
return err
}
var originalHeaderData portainerRegistryAuthenticationHeader
if err := json.Unmarshal(decodedHeaderData, &originalHeaderData); err != nil {
return err
}
// Delete header and exist function without error if Front End
// passes empty json. This is to restore original behavior which
// never originally passed this header
if string(decodedHeaderData) == "{}" {
request.Header.Del("X-Registry-Auth")
return nil
}
// Only set X-Registry-Auth if registryId is defined
if originalHeaderData.RegistryId == nil {
return nil
}
authenticationHeader, err := createRegistryAuthenticationHeader(transport.dataStore, *originalHeaderData.RegistryId, accessContext)
if err != nil {
return err
}
headerData, err := json.Marshal(authenticationHeader)
if err != nil {
return err
}
request.Header.Set("X-Registry-Auth", base64.URLEncoding.EncodeToString(headerData))
return nil return nil
} }
@ -659,11 +672,9 @@ func (transport *Transport) executeGenericResourceDeletionOperation(request *htt
} }
resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType) resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
if err != nil { if dataservices.IsErrObjectNotFound(err) {
if dataservices.IsErrObjectNotFound(err) { return response, nil
return response, nil } else if err != nil {
}
return response, err return response, err
} }

View File

@ -44,41 +44,43 @@ func (transport *Transport) volumeListOperation(response *http.Response, executo
} }
// The "Volumes" field contains the list of volumes as an array of JSON objects // The "Volumes" field contains the list of volumes as an array of JSON objects
if responseObject["Volumes"] != nil { if responseObject["Volumes"] == nil {
volumeData := responseObject["Volumes"].([]any) return utils.RewriteResponse(response, responseObject, http.StatusOK)
if transport.snapshotService != nil {
// Filling snapshot data can improve the performance of getVolumeResourceID
if err = transport.snapshotService.FillSnapshotData(transport.endpoint); err != nil {
log.Info().Err(err).
Int("endpoint id", int(transport.endpoint.ID)).
Msg("snapshot is not filled into the endpoint.")
}
}
for _, volumeObject := range volumeData {
volume := volumeObject.(map[string]any)
if err := transport.decorateVolumeResponseWithResourceID(volume); err != nil {
return fmt.Errorf("failed decorating volume response: %w", err)
}
}
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
} }
volumeData := responseObject["Volumes"].([]any)
if transport.snapshotService != nil {
// Filling snapshot data can improve the performance of getVolumeResourceID
if err = transport.snapshotService.FillSnapshotData(transport.endpoint); err != nil {
log.Info().Err(err).
Int("endpoint id", int(transport.endpoint.ID)).
Msg("snapshot is not filled into the endpoint.")
}
}
for _, volumeObject := range volumeData {
volume := volumeObject.(map[string]any)
if err := transport.decorateVolumeResponseWithResourceID(volume); err != nil {
return fmt.Errorf("failed decorating volume response: %w", err)
}
}
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 utils.RewriteResponse(response, responseObject, http.StatusOK) return utils.RewriteResponse(response, responseObject, http.StatusOK)
} }