mirror of
https://github.com/portainer/portainer.git
synced 2025-07-21 14:29:40 +02:00
fix(api): prevent the use of bind mounts in stacks if setting enabled (#3232)
This commit is contained in:
parent
f7480c4ad4
commit
fb6f6738d9
6 changed files with 97 additions and 5 deletions
|
@ -1,7 +1,9 @@
|
||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -238,7 +240,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFolder := strconv.Itoa(int(stack.ID))
|
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 {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist Compose file on disk", err}
|
||||||
}
|
}
|
||||||
|
@ -271,6 +273,7 @@ type composeStackDeploymentConfig struct {
|
||||||
endpoint *portainer.Endpoint
|
endpoint *portainer.Endpoint
|
||||||
dockerhub *portainer.DockerHub
|
dockerhub *portainer.DockerHub
|
||||||
registries []portainer.Registry
|
registries []portainer.Registry
|
||||||
|
isAdmin bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
|
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,
|
endpoint: endpoint,
|
||||||
dockerhub: dockerhub,
|
dockerhub: dockerhub,
|
||||||
registries: filteredRegistries,
|
registries: filteredRegistries,
|
||||||
|
isAdmin: securityContext.IsAdmin,
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
|
@ -306,12 +310,34 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
|
||||||
// clean it. Hence the use of the mutex.
|
// clean it. Hence the use of the mutex.
|
||||||
// We should contribute to libcompose to support authentication without using the config.json file.
|
// We should contribute to libcompose to support authentication without using the config.json file.
|
||||||
func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error {
|
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()
|
handler.stackCreationMutex.Lock()
|
||||||
defer handler.stackCreationMutex.Unlock()
|
defer handler.stackCreationMutex.Unlock()
|
||||||
|
|
||||||
handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -290,6 +292,7 @@ type swarmStackDeploymentConfig struct {
|
||||||
dockerhub *portainer.DockerHub
|
dockerhub *portainer.DockerHub
|
||||||
registries []portainer.Registry
|
registries []portainer.Registry
|
||||||
prune bool
|
prune bool
|
||||||
|
isAdmin bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
|
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,
|
dockerhub: dockerhub,
|
||||||
registries: filteredRegistries,
|
registries: filteredRegistries,
|
||||||
prune: prune,
|
prune: prune,
|
||||||
|
isAdmin: securityContext.IsAdmin,
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error {
|
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()
|
handler.stackCreationMutex.Lock()
|
||||||
defer handler.stackCreationMutex.Unlock()
|
defer handler.stackCreationMutex.Unlock()
|
||||||
|
|
||||||
handler.SwarmStackManager.Login(config.dockerhub, config.registries, config.endpoint)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ type Handler struct {
|
||||||
DockerHubService portainer.DockerHubService
|
DockerHubService portainer.DockerHubService
|
||||||
SwarmStackManager portainer.SwarmStackManager
|
SwarmStackManager portainer.SwarmStackManager
|
||||||
ComposeStackManager portainer.ComposeStackManager
|
ComposeStackManager portainer.ComposeStackManager
|
||||||
|
SettingsService portainer.SettingsService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage stack operations.
|
// NewHandler creates a handler to manage stack operations.
|
||||||
|
|
|
@ -5,6 +5,9 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/compose/types"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/portainer/api"
|
"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)}
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -207,6 +207,7 @@ func (server *Server) Start() error {
|
||||||
stackHandler.GitService = server.GitService
|
stackHandler.GitService = server.GitService
|
||||||
stackHandler.RegistryService = server.RegistryService
|
stackHandler.RegistryService = server.RegistryService
|
||||||
stackHandler.DockerHubService = server.DockerHubService
|
stackHandler.DockerHubService = server.DockerHubService
|
||||||
|
stackHandler.SettingsService = server.SettingsService
|
||||||
|
|
||||||
var tagHandler = tags.NewHandler(requestBouncer)
|
var tagHandler = tags.NewHandler(requestBouncer)
|
||||||
tagHandler.TagService = server.TagService
|
tagHandler.TagService = server.TagService
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel';
|
import {AccessControlFormData} from '../../../components/accessControlForm/porAccessControlFormModel';
|
||||||
|
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('CreateStackController', ['$scope', '$state', 'StackService', 'Authentication', 'Notifications', 'FormValidator', 'ResourceControlService', 'FormHelper', 'EndpointProvider',
|
.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');
|
$state.go('portainer.stacks');
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.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() {
|
.finally(function final() {
|
||||||
$scope.state.actionInProgress = false;
|
$scope.state.actionInProgress = false;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue