fix(stacks): validate the build context EE-6211

pull/10639/head
andres-portainer 2023-11-15 16:54:59 -03:00
parent e43d076269
commit 9a41726bc4
4 changed files with 73 additions and 22 deletions

View File

@ -2,13 +2,14 @@ package deployments
import ( import (
"fmt" "fmt"
"log"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/stacks/stackutils" "github.com/portainer/portainer/api/stacks/stackutils"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
) )
type ComposeStackDeploymentConfig struct { type ComposeStackDeploymentConfig struct {
@ -55,15 +56,22 @@ func (config *ComposeStackDeploymentConfig) GetUsername() string {
if config.user != nil { if config.user != nil {
return config.user.Username return config.user.Username
} }
return "" return ""
} }
func (config *ComposeStackDeploymentConfig) Deploy() error { func (config *ComposeStackDeploymentConfig) Deploy() error {
if config.FileService == nil || config.StackDeployer == nil { if config.FileService == nil || config.StackDeployer == nil {
log.Println("[deployment, compose] file service or stack deployer is not initialised") log.Error().Msg("file service or stack deployer is not initialised")
return errors.New("file service or stack deployer cannot be nil") return errors.New("file service or stack deployer cannot be nil")
} }
err := stackutils.ValidateStackFiles(config.stack, stackutils.IsValidBuildContext, config.FileService)
if err != nil {
return err
}
isAdminOrEndpointAdmin, err := stackutils.UserIsAdminOrEndpointAdmin(config.user, config.endpoint.ID) isAdminOrEndpointAdmin, err := stackutils.UserIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to validate user admin privileges") return errors.Wrap(err, "failed to validate user admin privileges")
@ -79,11 +87,14 @@ func (config *ComposeStackDeploymentConfig) Deploy() error {
!securitySettings.AllowContainerCapabilitiesForRegularUsers) && !securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
!isAdminOrEndpointAdmin { !isAdminOrEndpointAdmin {
err = stackutils.ValidateStackFiles(config.stack, securitySettings, config.FileService) validStackFn := stackutils.IsValidStackFileAdapter(securitySettings)
err = stackutils.ValidateStackFiles(config.stack, validStackFn, config.FileService)
if err != nil { if err != nil {
return err return err
} }
} }
if stackutils.IsRelativePathStack(config.stack) { if stackutils.IsRelativePathStack(config.stack) {
return config.StackDeployer.DeployRemoteComposeStack(config.stack, config.endpoint, config.registries, config.forcePullImage, config.ForceCreate) return config.StackDeployer.DeployRemoteComposeStack(config.stack, config.endpoint, config.registries, config.forcePullImage, config.ForceCreate)
} }

View File

@ -2,13 +2,14 @@ package deployments
import ( import (
"fmt" "fmt"
"log"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/stacks/stackutils" "github.com/portainer/portainer/api/stacks/stackutils"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
) )
type SwarmStackDeploymentConfig struct { type SwarmStackDeploymentConfig struct {
@ -60,10 +61,16 @@ func (config *SwarmStackDeploymentConfig) GetUsername() string {
func (config *SwarmStackDeploymentConfig) Deploy() error { func (config *SwarmStackDeploymentConfig) Deploy() error {
if config.FileService == nil || config.StackDeployer == nil { if config.FileService == nil || config.StackDeployer == nil {
log.Println("[deployment, swarm] file service or stack deployer is not initialised") log.Error().Msg("file service or stack deployer is not initialised")
return errors.New("file service or stack deployer cannot be nil") return errors.New("file service or stack deployer cannot be nil")
} }
err := stackutils.ValidateStackFiles(config.stack, stackutils.IsValidBuildContext, config.FileService)
if err != nil {
return err
}
isAdminOrEndpointAdmin, err := stackutils.UserIsAdminOrEndpointAdmin(config.user, config.endpoint.ID) isAdminOrEndpointAdmin, err := stackutils.UserIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to validate user admin privileges") return errors.Wrap(err, "failed to validate user admin privileges")
@ -72,7 +79,9 @@ func (config *SwarmStackDeploymentConfig) Deploy() error {
settings := &config.endpoint.SecuritySettings settings := &config.endpoint.SecuritySettings
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin { if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
err = stackutils.ValidateStackFiles(config.stack, settings, config.FileService) validStackFn := stackutils.IsValidStackFileAdapter(settings)
err = stackutils.ValidateStackFiles(config.stack, validStackFn, config.FileService)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,16 +1,19 @@
package stackutils package stackutils
import ( import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/docker/cli/cli/compose/loader" "github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/types" "github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors" "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
) )
func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error { func loadComposeConfig(stackFileContent []byte) (*types.Config, error) {
composeConfigYAML, err := loader.ParseYAML(stackFileContent) composeConfigYAML, err := loader.ParseYAML(stackFileContent)
if err != nil { if err != nil {
return err return nil, err
} }
composeConfigFile := types.ConfigFile{ composeConfigFile := types.ConfigFile{
@ -22,14 +25,19 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo
Environment: map[string]string{}, Environment: map[string]string{},
} }
composeConfig, err := loader.Load(composeConfigDetails, func(options *loader.Options) { return loader.Load(composeConfigDetails, func(options *loader.Options) {
options.SkipValidation = true options.SkipValidation = true
options.SkipInterpolation = true options.SkipInterpolation = true
}) })
if err != nil { }
return err
}
func IsValidStackFileAdapter(securitySettings *portainer.EndpointSecuritySettings) func(*types.Config) error {
return func(composeConfig *types.Config) error {
return IsValidStackFile(composeConfig, securitySettings)
}
}
func IsValidStackFile(composeConfig *types.Config, securitySettings *portainer.EndpointSecuritySettings) error {
for key := range composeConfig.Services { for key := range composeConfig.Services {
service := composeConfig.Services[key] service := composeConfig.Services[key]
if !securitySettings.AllowBindMountsForRegularUsers { if !securitySettings.AllowBindMountsForRegularUsers {
@ -64,17 +72,42 @@ func IsValidStackFile(stackFileContent []byte, securitySettings *portainer.Endpo
return nil return nil
} }
func ValidateStackFiles(stack *portainer.Stack, securitySettings *portainer.EndpointSecuritySettings, fileService portainer.FileService) error { func ValidateStackFiles(stack *portainer.Stack, isValidFn func(content *types.Config) error, fileService portainer.FileService) error {
for _, file := range GetStackFilePaths(stack, false) { for _, file := range GetStackFilePaths(stack, false) {
stackContent, err := fileService.GetFileContent(stack.ProjectPath, file) stackContent, err := fileService.GetFileContent(stack.ProjectPath, file)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to get stack file content") return errors.Wrap(err, "failed to get stack file content")
} }
err = IsValidStackFile(stackContent, securitySettings) composeConfig, err := loadComposeConfig(stackContent)
if err != nil {
return err
}
err = isValidFn(composeConfig)
if err != nil { if err != nil {
return errors.Wrap(err, "stack config file is invalid") return errors.Wrap(err, "stack config file is invalid")
} }
} }
return nil
}
func IsValidBuildContext(composeConfig *types.Config) error {
for key := range composeConfig.Services {
service := composeConfig.Services[key]
if strings.HasPrefix(service.Build.Context, "/") || strings.Contains(service.Build.Context, "..") {
return errors.New("invalid build context")
}
driveLetter, _, ok := strings.Cut(service.Build.Context, ":")
driveLetter = strings.ToUpper(driveLetter)
if ok && len(driveLetter) == 1 && driveLetter >= "A" && driveLetter <= "Z" {
return errors.New("invalid build context")
}
}
return nil return nil
} }

View File

@ -126,10 +126,7 @@ func (wrapper *PluginWrapper) command(command composeCommand, options libstack.O
command.WithHost(options.Host) command.WithHost(options.Host)
} }
var stderr bytes.Buffer args := append([]string{}, command.ToArgs()...)
args := []string{}
args = append(args, command.ToArgs()...)
cmd := exec.Command(program, args...) cmd := exec.Command(program, args...)
cmd.Dir = options.WorkingDir cmd.Dir = options.WorkingDir
@ -150,7 +147,8 @@ func (wrapper *PluginWrapper) command(command composeCommand, options libstack.O
Interface("env", cmd.Env). Interface("env", cmd.Env).
Msg("run command") Msg("run command")
cmd.Stderr = &stderr stderr := &bytes.Buffer{}
cmd.Stderr = stderr
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { if err != nil {