fix(api): prevent the use of bind mounts in stacks if setting enabled (#3232)

pull/3091/head
Anthony Lapenna 2019-10-07 16:12:21 +13:00 committed by GitHub
parent f7480c4ad4
commit fb6f6738d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 97 additions and 5 deletions

View File

@ -1,7 +1,9 @@
package stacks
import (
"errors"
"net/http"
"path"
"strconv"
"strings"
@ -238,7 +240,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
}
stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, payload.StackFileContent)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
}
@ -271,6 +273,7 @@ type composeStackDeploymentConfig struct {
endpoint *portainer.Endpoint
dockerhub *portainer.DockerHub
registries []portainer.Registry
isAdmin bool
}
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
@ -295,6 +298,7 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
endpoint: endpoint,
dockerhub: dockerhub,
registries: filteredRegistries,
isAdmin: securityContext.IsAdmin,
}
return config, nil
@ -306,12 +310,34 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
// clean it. Hence the use of the mutex.
// We should contribute to libcompose to support authentication without using the config.json file.
func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error {
settings, err := handler.SettingsService.Settings()
if err != nil {
return err
}
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return err
}
valid, err := handler.isValidStackFile(stackContent)
if err != nil {
return err
}
if !valid {
return errors.New("bind-mount disabled for non administrator users")
}
}
handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock()
handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
err := handler.ComposeStackManager.Up(config.stack, config.endpoint)
err = handler.ComposeStackManager.Up(config.stack, config.endpoint)
if err != nil {
return err
}

View File

@ -1,7 +1,9 @@
package stacks
import (
"errors"
"net/http"
"path"
"strconv"
"strings"
@ -290,6 +292,7 @@ type swarmStackDeploymentConfig struct {
dockerhub *portainer.DockerHub
registries []portainer.Registry
prune bool
isAdmin bool
}
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
@ -315,18 +318,41 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
dockerhub: dockerhub,
registries: filteredRegistries,
prune: prune,
isAdmin: securityContext.IsAdmin,
}
return config, nil
}
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error {
settings, err := handler.SettingsService.Settings()
if err != nil {
return err
}
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
if err != nil {
return err
}
valid, err := handler.isValidStackFile(stackContent)
if err != nil {
return err
}
if !valid {
return errors.New("bind-mount disabled for non administrator users")
}
}
handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock()
handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
err := handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint)
err = handler.SwarmStackManager.Deploy(config.stack, config.prune, config.endpoint)
if err != nil {
return err
}

View File

@ -25,6 +25,7 @@ type Handler struct {
DockerHubService portainer.DockerHubService
SwarmStackManager portainer.SwarmStackManager
ComposeStackManager portainer.ComposeStackManager
SettingsService portainer.SettingsService
}
// NewHandler creates a handler to manage stack operations.

View File

@ -5,6 +5,9 @@ import (
"log"
"net/http"
"github.com/docker/cli/cli/compose/types"
"github.com/docker/cli/cli/compose/loader"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api"
@ -87,3 +90,38 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request,
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) isValidStackFile(stackFileContent []byte) (bool, error) {
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
if err != nil {
return false, err
}
composeConfigFile := types.ConfigFile{
Config: composeConfigYAML,
}
composeConfigDetails := types.ConfigDetails{
ConfigFiles: []types.ConfigFile{composeConfigFile},
Environment: map[string]string{},
}
composeConfig, err := loader.Load(composeConfigDetails, func(options *loader.Options) {
options.SkipValidation = true
options.SkipInterpolation = true
})
if err != nil {
return false, err
}
for key := range composeConfig.Services {
service := composeConfig.Services[key]
for _, volume := range service.Volumes {
if volume.Type == "bind" {
return false, nil
}
}
}
return true, nil
}

View File

@ -207,6 +207,7 @@ func (server *Server) Start() error {
stackHandler.GitService = server.GitService
stackHandler.RegistryService = server.RegistryService
stackHandler.DockerHubService = server.DockerHubService
stackHandler.SettingsService = server.SettingsService
var tagHandler = tags.NewHandler(requestBouncer)
tagHandler.TagService = server.TagService

View File

@ -1,4 +1,4 @@
import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel';
import {AccessControlFormData} from '../../../components/accessControlForm/porAccessControlFormModel';
angular.module('portainer.app')
.controller('CreateStackController', ['$scope', '$state', 'StackService', 'Authentication', 'Notifications', 'FormValidator', 'ResourceControlService', 'FormHelper', 'EndpointProvider',
@ -124,7 +124,7 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid
$state.go('portainer.stacks');
})
.catch(function error(err) {
Notifications.warning('Deployment error', type === 1 ? err.err.data.err : err.data.err);
Notifications.error('Deployment error', err, 'Unable to deploy stack');
})
.finally(function final() {
$scope.state.actionInProgress = false;