diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index ebdcea252..feaab135e 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -11,7 +11,7 @@ import ( "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/http/security" ) @@ -283,6 +283,7 @@ type composeStackDeploymentConfig struct { dockerhub *portainer.DockerHub registries []portainer.Registry isAdmin bool + user *portainer.User } func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) { @@ -302,12 +303,18 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai } filteredRegistries := security.FilterRegistries(registries, securityContext) + user, err := handler.UserService.User(securityContext.UserID) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err} + } + config := &composeStackDeploymentConfig{ stack: stack, endpoint: endpoint, dockerhub: dockerhub, registries: filteredRegistries, isAdmin: securityContext.IsAdmin, + user: user, } return config, nil @@ -324,7 +331,29 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) return err } - if !settings.AllowBindMountsForRegularUsers && !config.isAdmin { + rbacExtension, err := handler.ExtensionService.Extension(portainer.RBACExtension) + if err != nil && err != portainer.ErrObjectNotFound { + return errors.New("Unable to verify if RBAC extension is loaded") + } + + endpointResourceAccess := false + _, ok := config.user.EndpointAuthorizations[portainer.EndpointID(config.endpoint.ID)][portainer.EndpointResourcesAccess] + if ok { + endpointResourceAccess = true + } + + mustBeChecked := false + if rbacExtension != nil { + if !config.isAdmin && !endpointResourceAccess { + mustBeChecked = true + } + } else { + if !config.isAdmin { + mustBeChecked = true + } + } + + if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers) && mustBeChecked { composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) stackContent, err := handler.FileService.GetFileContent(composeFilePath) @@ -332,13 +361,10 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) return err } - valid, err := handler.isValidStackFile(stackContent) + err = handler.isValidStackFile(stackContent, settings) if err != nil { return err } - if !valid { - return errors.New("bind-mount disabled for non administrator users") - } } handler.stackCreationMutex.Lock() diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index 143292ea9..0dbd6e183 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -1,7 +1,6 @@ package stacks import ( - "errors" "net/http" "path" "strconv" @@ -10,7 +9,7 @@ import ( "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/http/security" ) @@ -337,13 +336,10 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err return err } - valid, err := handler.isValidStackFile(stackContent) + err = handler.isValidStackFile(stackContent, settings) if err != nil { return err } - if !valid { - return errors.New("bind-mount disabled for non administrator users") - } } handler.stackCreationMutex.Lock() diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index dc374c4b1..43831a200 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -10,7 +10,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" ) @@ -97,10 +97,10 @@ 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) { +func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *portainer.Settings) error { composeConfigYAML, err := loader.ParseYAML(stackFileContent) if err != nil { - return false, err + return err } composeConfigFile := types.ConfigFile{ @@ -117,19 +117,25 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error) options.SkipInterpolation = true }) if err != nil { - return false, err + return err } for key := range composeConfig.Services { service := composeConfig.Services[key] - for _, volume := range service.Volumes { - if volume.Type == "bind" { - return false, nil + if !settings.AllowBindMountsForRegularUsers { + for _, volume := range service.Volumes { + if volume.Type == "bind" { + return errors.New("bind-mount disabled for non administrator users") + } } } + + if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true { + return errors.New("privileged mode disabled for non administrator users") + } } - return true, nil + return nil } func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError { diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index c0587d9c8..2a80d788f 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -1,12 +1,17 @@ package docker import ( + "bytes" "context" + "encoding/json" + "errors" + "io/ioutil" "net/http" "github.com/docker/docker/client" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" + "github.com/portainer/portainer/api/http/security" ) const ( @@ -147,3 +152,70 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB return false } + +func (transport *Transport) decorateContainerCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) { + type PartialContainer struct { + HostConfig struct { + Privileged bool `json:"Privileged"` + } `json:"HostConfig"` + } + + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return nil, err + } + + user, err := transport.userService.User(tokenData.ID) + if err != nil { + return nil, err + } + + rbacExtension, err := transport.extensionService.Extension(portainer.RBACExtension) + if err != nil && err != portainer.ErrObjectNotFound { + return nil, err + } + + endpointResourceAccess := false + _, ok := user.EndpointAuthorizations[portainer.EndpointID(transport.endpoint.ID)][portainer.EndpointResourcesAccess] + if ok { + endpointResourceAccess = true + } + + if (rbacExtension != nil && !endpointResourceAccess && tokenData.Role != portainer.AdministratorRole) || (rbacExtension == nil && tokenData.Role != portainer.AdministratorRole) { + + settings, err := transport.settingsService.Settings() + if err != nil { + return nil, err + } + + if !settings.AllowPrivilegedModeForRegularUsers { + body, err := ioutil.ReadAll(request.Body) + if err != nil { + return nil, err + } + + partialContainer := &PartialContainer{} + err = json.Unmarshal(body, partialContainer) + if err != nil { + return nil, err + } + + if partialContainer.HostConfig.Privileged { + return nil, errors.New("forbidden to use privileged mode") + } + + request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + } + } + + response, err := transport.executeDockerRequest(request) + if err != nil { + return response, err + } + + if response.StatusCode == http.StatusCreated { + err = transport.decorateGenericResourceCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID) + } + + return response, err +} diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index c511948fa..2138d0135 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -209,7 +209,7 @@ func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Res func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.Response, error) { switch requestPath := request.URL.Path; requestPath { case "/containers/create": - return transport.decorateGenericResourceCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl) + return transport.decorateContainerCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl) case "/containers/prune": return transport.administratorOperation(request) @@ -656,6 +656,7 @@ func (transport *Transport) createRegistryAccessContext(request *http.Request) ( return nil, err } + accessContext := ®istryAccessContext{ isAdmin: true, userID: tokenData.ID,