mirror of https://github.com/portainer/portainer
chore(docker): clean up the code EE-7325 (#11997)
parent
faca64442f
commit
340830d121
|
@ -7,6 +7,7 @@ import (
|
|||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
@ -23,7 +24,8 @@ const (
|
|||
|
||||
type (
|
||||
resourceLabelsObjectSelector func(map[string]any) map[string]any
|
||||
resourceOperationParameters struct {
|
||||
|
||||
resourceOperationParameters struct {
|
||||
resourceIdentifierAttribute string
|
||||
resourceType portainer.ResourceControlType
|
||||
labelsObjectSelector resourceLabelsObjectSelector
|
||||
|
@ -31,28 +33,18 @@ type (
|
|||
)
|
||||
|
||||
func getUniqueElements(items string) []string {
|
||||
result := []string{}
|
||||
seen := make(map[string]struct{})
|
||||
for _, item := range strings.Split(items, ",") {
|
||||
v := strings.TrimSpace(item)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[v]; !ok {
|
||||
result = append(result, v)
|
||||
seen[v] = struct{}{}
|
||||
}
|
||||
}
|
||||
xs := strings.Split(items, ",")
|
||||
xs = slicesx.Map(xs, strings.TrimSpace)
|
||||
xs = slicesx.Filter(xs, func(x string) bool { return len(x) > 0 })
|
||||
|
||||
return result
|
||||
return slicesx.Unique(xs)
|
||||
}
|
||||
|
||||
func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject map[string]any, resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||
if labelsObject[resourceLabelForPortainerPublicResourceControl] != nil {
|
||||
resourceControl := authorization.NewPublicResourceControl(resourceID, resourceType)
|
||||
|
||||
err := transport.dataStore.ResourceControl().Create(resourceControl)
|
||||
if err != nil {
|
||||
if err := transport.dataStore.ResourceControl().Create(resourceControl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -61,6 +53,7 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m
|
|||
|
||||
teamNames := make([]string, 0)
|
||||
userNames := make([]string, 0)
|
||||
|
||||
if labelsObject[resourceLabelForPortainerTeamResourceControl] != nil {
|
||||
concatenatedTeamNames := labelsObject[resourceLabelForPortainerTeamResourceControl].(string)
|
||||
teamNames = getUniqueElements(concatenatedTeamNames)
|
||||
|
@ -71,48 +64,48 @@ func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject m
|
|||
userNames = getUniqueElements(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.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
|
||||
if len(teamNames) == 0 && len(userNames) == 0 {
|
||||
return nil, 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) {
|
||||
|
@ -298,40 +291,40 @@ func (transport *Transport) findResourceControl(resourceIdentifier string, resou
|
|||
return resourceControl, nil
|
||||
}
|
||||
|
||||
if resourceLabelsObject != 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)
|
||||
if resourceLabelsObject == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -19,6 +19,8 @@ type postDockerfileRequest struct {
|
|||
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.
|
||||
//
|
||||
// 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":
|
||||
err := request.ParseMultipartForm(32 * OneMegabyte) // limit parser memory to 32MB
|
||||
if err != nil {
|
||||
if err := request.ParseMultipartForm(32 * OneMegabyte); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if request.MultipartForm == nil || request.MultipartForm.File == nil {
|
||||
return errors.New("uploaded files not found to build image")
|
||||
return ErrUploadedFilesNotFound
|
||||
}
|
||||
|
||||
tfb := archive.NewTarFileInBuffer()
|
||||
|
|
|
@ -19,6 +19,15 @@ import (
|
|||
|
||||
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) {
|
||||
container, err := dockerClient.ContainerInspect(context.Background(), containerID)
|
||||
if err != nil {
|
||||
|
@ -108,6 +117,7 @@ func selectorContainerLabelsFromContainerInspectOperation(responseObject map[str
|
|||
containerLabelsObject := utils.GetJSONObject(containerConfigObject, "Labels")
|
||||
return containerLabelsObject
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -117,6 +127,7 @@ func selectorContainerLabelsFromContainerInspectOperation(responseObject map[str
|
|||
// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||
func selectorContainerLabelsFromContainerListOperation(responseObject map[string]any) map[string]any {
|
||||
containerLabelsObject := utils.GetJSONObject(responseObject, "Labels")
|
||||
|
||||
return containerLabelsObject
|
||||
}
|
||||
|
||||
|
@ -129,13 +140,12 @@ func filterContainersWithBlackListedLabels(containerData []any, labelBlackList [
|
|||
containerObject := container.(map[string]any)
|
||||
|
||||
containerLabels := selectorContainerLabelsFromContainerListOperation(containerObject)
|
||||
if containerLabels != nil {
|
||||
if !containerHasBlackListedLabel(containerLabels, labelBlackList) {
|
||||
filteredContainerData = append(filteredContainerData, containerObject)
|
||||
}
|
||||
} else {
|
||||
filteredContainerData = append(filteredContainerData, containerObject)
|
||||
|
||||
if containerHasBlackListedLabel(containerLabels, labelBlackList) {
|
||||
continue
|
||||
}
|
||||
|
||||
filteredContainerData = append(filteredContainerData, containerObject)
|
||||
}
|
||||
|
||||
return filteredContainerData, nil
|
||||
|
@ -195,35 +205,34 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
|
|||
}
|
||||
|
||||
partialContainer := &PartialContainer{}
|
||||
err = json.Unmarshal(body, partialContainer)
|
||||
if err != nil {
|
||||
if err := json.Unmarshal(body, partialContainer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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" {
|
||||
return forbiddenResponse, errors.New("forbidden to use pid host namespace")
|
||||
return forbiddenResponse, ErrPIDHostNamespaceForbidden
|
||||
}
|
||||
|
||||
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 {
|
||||
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) {
|
||||
return nil, errors.New("forbidden to use container capabilities")
|
||||
return nil, ErrContainerCapabilitiesForbidden
|
||||
}
|
||||
|
||||
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
|
||||
for _, bind := range partialContainer.HostConfig.Binds {
|
||||
if strings.HasPrefix(bind, "/") {
|
||||
return forbiddenResponse, errors.New("forbidden to use bind mounts")
|
||||
return forbiddenResponse, ErrBindMountsForbidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,12 +17,15 @@ func init() {
|
|||
|
||||
func (transport *Transport) applyPortainerContainers(resources []any) ([]any, error) {
|
||||
decoratedResourceData := make([]any, 0)
|
||||
|
||||
for _, resource := range resources {
|
||||
responseObject, ok := resource.(map[string]any)
|
||||
if !ok {
|
||||
decoratedResourceData = append(decoratedResourceData, resource)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
responseObject, _ = transport.applyPortainerContainer(responseObject)
|
||||
decoratedResourceData = append(decoratedResourceData, responseObject)
|
||||
}
|
||||
|
|
|
@ -29,31 +29,36 @@ type (
|
|||
|
||||
func createRegistryAuthenticationHeader(
|
||||
dataStore dataservices.DataStore,
|
||||
registryId portainer.RegistryID,
|
||||
registryID portainer.RegistryID,
|
||||
accessContext *registryAccessContext,
|
||||
) (authenticationHeader registryAuthenticationHeader, err error) {
|
||||
if registryId == 0 { // dockerhub (anonymous)
|
||||
if registryID == 0 { // dockerhub (anonymous)
|
||||
authenticationHeader.Serveraddress = "docker.io"
|
||||
} else { // any "custom" registry
|
||||
var matchingRegistry *portainer.Registry
|
||||
for _, registry := range accessContext.registries {
|
||||
registry := registry
|
||||
if registry.ID == registryId &&
|
||||
(accessContext.isAdmin ||
|
||||
security.AuthorizedRegistryAccess(®istry, accessContext.user, accessContext.teamMemberships, accessContext.endpointID)) {
|
||||
matchingRegistry = ®istry
|
||||
break
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Any "custom" registry
|
||||
var matchingRegistry *portainer.Registry
|
||||
|
||||
for _, registry := range accessContext.registries {
|
||||
registry := registry
|
||||
if registry.ID == registryID &&
|
||||
(accessContext.isAdmin ||
|
||||
security.AuthorizedRegistryAccess(®istry, accessContext.user, accessContext.teamMemberships, accessContext.endpointID)) {
|
||||
matchingRegistry = ®istry
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matchingRegistry != nil {
|
||||
if err = registryutils.EnsureRegTokenValid(dataStore, matchingRegistry); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if matchingRegistry != nil {
|
||||
err = registryutils.EnsureRegTokenValid(dataStore, matchingRegistry)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
authenticationHeader.Serveraddress = matchingRegistry.URL
|
||||
authenticationHeader.Username, authenticationHeader.Password, err = registryutils.GetRegEffectiveCredential(matchingRegistry)
|
||||
}
|
||||
authenticationHeader.Serveraddress = matchingRegistry.URL
|
||||
authenticationHeader.Username, authenticationHeader.Password, err = registryutils.GetRegEffectiveCredential(matchingRegistry)
|
||||
}
|
||||
|
||||
return
|
||||
|
|
|
@ -59,8 +59,8 @@ func (transport *Transport) serviceListOperation(response *http.Response, execut
|
|||
// 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
|
||||
// ServiceInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
||||
responseObject, err := utils.GetResponseAsJSONObject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -85,6 +85,7 @@ func selectorServiceLabels(responseObject map[string]any) map[string]any {
|
|||
if serviceSpecObject != nil {
|
||||
return utils.GetJSONObject(serviceSpecObject, "Labels")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -108,33 +109,34 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if !isAdminOrEndpointAdmin {
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isAdminOrEndpointAdmin {
|
||||
return transport.replaceRegistryAuthenticationHeader(request)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
partialService := &PartialService{}
|
||||
err = json.Unmarshal(body, partialService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
partialService := &PartialService{}
|
||||
if err := json.Unmarshal(body, partialService); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@ type (
|
|||
operationContext *restrictedDockerOperationContext
|
||||
labelBlackList []portainer.Pair
|
||||
}
|
||||
|
||||
restrictedOperationRequest func(*http.Response, *operationExecutor) 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){
|
||||
"build": (*Transport).proxyBuildRequest,
|
||||
"configs": (*Transport).proxyConfigRequest,
|
||||
"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,
|
||||
"networks": (*Transport).proxyNetworkRequest,
|
||||
"nodes": (*Transport).proxyNodeRequest,
|
||||
"secrets": (*Transport).proxySecretRequest,
|
||||
"services": (*Transport).proxyServiceRequest,
|
||||
"swarm": (*Transport).proxySwarmRequest,
|
||||
"tasks": (*Transport).proxyTaskRequest,
|
||||
"v2": (*Transport).proxyAgentRequest,
|
||||
"volumes": (*Transport).proxyVolumeRequest,
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return proxyFunc(transport, request, unversionedPath)
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
|
@ -141,7 +143,7 @@ func (transport *Transport) proxyAgentRequest(r *http.Request, unversionedPath s
|
|||
|
||||
switch {
|
||||
case strings.HasPrefix(requestPath, "/browse"):
|
||||
// host file browser request
|
||||
// Host file browser request
|
||||
volumeIDParameter, found := r.URL.Query()["volumeID"]
|
||||
if !found || len(volumeIDParameter) < 1 {
|
||||
return transport.administratorOperation(r)
|
||||
|
@ -154,7 +156,7 @@ func (transport *Transport) proxyAgentRequest(r *http.Request, unversionedPath s
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// volume browser request
|
||||
// Volume browser request
|
||||
return transport.restrictedResourceOperation(r, resourceID, volumeName, portainer.VolumeResourceControl, true)
|
||||
case strings.HasPrefix(requestPath, "/dockerhub"):
|
||||
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)
|
||||
|
||||
default:
|
||||
// assume /configs/{id}
|
||||
// Assume /configs/{id}
|
||||
configID := path.Base(requestPath)
|
||||
|
||||
if request.Method == http.MethodGet {
|
||||
|
@ -242,6 +244,7 @@ func (transport *Transport) proxyContainerRequest(request *http.Request, unversi
|
|||
if action == "json" {
|
||||
return transport.rewriteOperation(request, transport.containerInspectOperation)
|
||||
}
|
||||
|
||||
return transport.restrictedResourceOperation(request, containerID, containerID, portainer.ContainerResourceControl, false)
|
||||
} else if match, _ := path.Match("/containers/*", requestPath); match {
|
||||
// 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.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
@ -273,6 +277,7 @@ func (transport *Transport) proxyServiceRequest(request *http.Request, unversion
|
|||
// Handle /services/{id}/{action} requests
|
||||
serviceID := path.Base(path.Dir(requestPath))
|
||||
transport.decorateRegistryAuthenticationHeader(request)
|
||||
|
||||
return transport.restrictedResourceOperation(request, serviceID, serviceID, portainer.ServiceResourceControl, false)
|
||||
} else if match, _ := path.Match("/services/*", requestPath); match {
|
||||
// Handle /services/{id} requests
|
||||
|
@ -284,8 +289,10 @@ func (transport *Transport) proxyServiceRequest(request *http.Request, unversion
|
|||
case http.MethodDelete:
|
||||
return transport.executeGenericResourceDeletionOperation(request, serviceID, serviceID, portainer.ServiceResourceControl)
|
||||
}
|
||||
|
||||
return transport.restrictedResourceOperation(request, serviceID, serviceID, portainer.ServiceResourceControl, false)
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
@ -304,7 +311,7 @@ func (transport *Transport) proxyVolumeRequest(request *http.Request, unversione
|
|||
return transport.rewriteOperation(request, transport.volumeListOperation)
|
||||
|
||||
default:
|
||||
// assume /volumes/{name}
|
||||
// Assume /volumes/{name}
|
||||
return transport.restrictedVolumeOperation(requestPath, request)
|
||||
}
|
||||
}
|
||||
|
@ -320,7 +327,7 @@ func (transport *Transport) proxyNetworkRequest(request *http.Request, unversion
|
|||
return transport.rewriteOperation(request, transport.networkListOperation)
|
||||
|
||||
default:
|
||||
// assume /networks/{id}
|
||||
// Assume /networks/{id}
|
||||
networkID := path.Base(requestPath)
|
||||
|
||||
if request.Method == http.MethodGet {
|
||||
|
@ -328,6 +335,7 @@ func (transport *Transport) proxyNetworkRequest(request *http.Request, unversion
|
|||
} else if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, networkID, networkID, portainer.NetworkResourceControl)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
default:
|
||||
// assume /secrets/{id}
|
||||
// Assume /secrets/{id}
|
||||
secretID := path.Base(requestPath)
|
||||
|
||||
if request.Method == http.MethodGet {
|
||||
|
@ -351,6 +359,7 @@ func (transport *Transport) proxySecretRequest(request *http.Request, unversione
|
|||
} else if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, secretID, secretID, portainer.SecretResourceControl)
|
||||
}
|
||||
|
||||
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) {
|
||||
requestPath := unversionedPath
|
||||
|
||||
// assume /nodes/{id}
|
||||
// Assume /nodes/{id}
|
||||
if path.Base(requestPath) != "nodes" {
|
||||
return transport.administratorOperation(request)
|
||||
}
|
||||
|
@ -373,7 +382,7 @@ func (transport *Transport) proxySwarmRequest(request *http.Request, unversioned
|
|||
case "/swarm":
|
||||
return transport.rewriteOperation(request, swarmInspectOperation)
|
||||
default:
|
||||
// assume /swarm/{action}
|
||||
// Assume /swarm/{action}
|
||||
return transport.administratorOperation(request)
|
||||
}
|
||||
}
|
||||
|
@ -385,34 +394,38 @@ func (transport *Transport) proxyTaskRequest(request *http.Request, unversionedP
|
|||
case "/tasks":
|
||||
return transport.rewriteOperation(request, transport.taskListOperation)
|
||||
default:
|
||||
// assume /tasks/{id}
|
||||
// Assume /tasks/{id}
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (transport *Transport) proxyBuildRequest(request *http.Request, _ string) (*http.Response, error) {
|
||||
err := transport.updateDefaultGitBranch(request)
|
||||
if err != nil {
|
||||
if err := transport.updateDefaultGitBranch(request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transport.interceptAndRewriteRequest(request, buildOperation)
|
||||
}
|
||||
|
||||
func (transport *Transport) updateDefaultGitBranch(request *http.Request) error {
|
||||
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()
|
||||
q.Set("remote", newRemote)
|
||||
request.URL.RawQuery = q.Encode()
|
||||
if !strings.HasSuffix(remote, ".git") {
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -426,6 +439,7 @@ func (transport *Transport) proxyImageRequest(request *http.Request, unversioned
|
|||
if path.Base(requestPath) == "push" && request.Method == http.MethodPost {
|
||||
return transport.replaceRegistryAuthenticationHeader(request)
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
@ -444,47 +458,46 @@ func (transport *Transport) decorateRegistryAuthenticationHeader(request *http.R
|
|||
|
||||
originalHeader := request.Header.Get("X-Registry-Auth")
|
||||
|
||||
if originalHeader != "" {
|
||||
|
||||
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)
|
||||
if originalHeader == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -659,11 +672,9 @@ func (transport *Transport) executeGenericResourceDeletionOperation(request *htt
|
|||
}
|
||||
|
||||
resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
|
||||
if err != nil {
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
if dataservices.IsErrObjectNotFound(err) {
|
||||
return response, nil
|
||||
} else if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
if responseObject["Volumes"] != nil {
|
||||
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
|
||||
if responseObject["Volumes"] == nil {
|
||||
return utils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue