diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index df723668c..3f9ecf9a1 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -9,8 +9,7 @@ import ( "net/http" "github.com/docker/docker/client" - portainer "github.com/portainer/portainer/api" - bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy/factory/responseutils" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/authorization" @@ -163,6 +162,7 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req Devices []interface{} `json:"Devices"` CapAdd []string `json:"CapAdd"` CapDrop []string `json:"CapDrop"` + Binds []string `json:"Binds"` } `json:"HostConfig"` } @@ -175,25 +175,12 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return nil, err } - user, err := transport.dataStore.User().User(tokenData.ID) + isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request) if err != nil { return nil, err } - rbacExtension, err := transport.dataStore.Extension().Extension(portainer.RBACExtension) - if err != nil && err != bolterrors.ErrObjectNotFound { - return nil, err - } - - endpointResourceAccess := false - _, ok := user.EndpointAuthorizations[portainer.EndpointID(transport.endpoint.ID)][portainer.EndpointResourcesAccess] - if ok { - endpointResourceAccess = true - } - - isAdmin := (rbacExtension != nil && endpointResourceAccess) || tokenData.Role == portainer.AdministratorRole - - if !isAdmin { + if !isAdminOrEndpointAdmin { settings, err := transport.dataStore.Settings().Settings() if err != nil { return nil, err @@ -219,13 +206,17 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req } if !settings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 { - return nil, errors.New("forbidden to use device mapping") + return forbiddenResponse, errors.New("forbidden to use device mapping") } if !settings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) { return nil, errors.New("forbidden to use container capabilities") } + if !settings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) { + return forbiddenResponse, errors.New("forbidden to use bind mounts") + } + request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) } diff --git a/api/http/proxy/factory/docker/services.go b/api/http/proxy/factory/docker/services.go index 8863ea3fd..08f01a23c 100644 --- a/api/http/proxy/factory/docker/services.go +++ b/api/http/proxy/factory/docker/services.go @@ -1,7 +1,11 @@ package docker import ( + "bytes" "context" + "encoding/json" + "errors" + "io/ioutil" "net/http" "github.com/docker/docker/api/types" @@ -85,3 +89,54 @@ func selectorServiceLabels(responseObject map[string]interface{}) map[string]int } return nil } + +func (transport *Transport) decorateServiceCreationOperation(request *http.Request) (*http.Response, error) { + type PartialService struct { + TaskTemplate struct { + ContainerSpec struct { + Mounts []struct { + Type string + } + } + } + } + + forbiddenResponse := &http.Response{ + StatusCode: http.StatusForbidden, + } + + isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request) + if err != nil { + return nil, err + } + + if !isAdminOrEndpointAdmin { + settings, err := transport.dataStore.Settings().Settings() + if err != nil { + return nil, err + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + return nil, err + } + + partialService := &PartialService{} + err = json.Unmarshal(body, partialService) + if err != nil { + return nil, err + } + + if !settings.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 = ioutil.NopCloser(bytes.NewBuffer(body)) + } + + return transport.replaceRegistryAuthenticationHeader(request) +} diff --git a/api/http/proxy/factory/docker/transport.go b/api/http/proxy/factory/docker/transport.go index 6ecfb4615..604d05d65 100644 --- a/api/http/proxy/factory/docker/transport.go +++ b/api/http/proxy/factory/docker/transport.go @@ -225,7 +225,7 @@ func (transport *Transport) proxyContainerRequest(request *http.Request) (*http. func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Response, error) { switch requestPath := request.URL.Path; requestPath { case "/services/create": - return transport.replaceRegistryAuthenticationHeader(request) + return transport.decorateServiceCreationOperation(request) case "/services": return transport.rewriteOperation(request, transport.serviceListOperation) @@ -629,7 +629,6 @@ func (transport *Transport) createRegistryAccessContext(request *http.Request) ( return nil, err } - accessContext := ®istryAccessContext{ isAdmin: true, userID: tokenData.ID, @@ -707,3 +706,32 @@ func (transport *Transport) createOperationContext(request *http.Request) (*rest return operationContext, nil } + +func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return false, err + } + + if tokenData.Role == portainer.AdministratorRole { + return true, nil + } + + user, err := transport.dataStore.User().User(tokenData.ID) + if err != nil { + return false, err + } + + rbacExtension, err := transport.dataStore.Extension().Extension(portainer.RBACExtension) + if err != nil && err != bolterrors.ErrObjectNotFound { + return false, err + } + + if rbacExtension == nil { + return false, nil + } + + _, endpointResourceAccess := user.EndpointAuthorizations[portainer.EndpointID(transport.endpoint.ID)][portainer.EndpointResourcesAccess] + + return endpointResourceAccess, nil +} diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 4ce386800..2d69e3fb2 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -614,6 +614,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.isAdmin = Authentication.isAdmin(); $scope.showDeviceMapping = await shouldShowDevices(); $scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled(); + $scope.isAdminOrEndpointAdmin = await checkIfAdminOrEndpointAdmin(); Volume.query( {}, @@ -678,7 +679,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ SettingsService.publicSettings() .then(function success(data) { - $scope.allowBindMounts = data.AllowBindMountsForRegularUsers; + $scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || data.AllowBindMountsForRegularUsers; $scope.allowPrivilegedMode = data.AllowPrivilegedModeForRegularUsers; }) .catch(function error(err) { @@ -922,6 +923,15 @@ angular.module('portainer.docker').controller('CreateContainerController', [ return allowContainerCapabilitiesForRegularUsers || isAdminOrEndpointAdmin(); } + async function checkIfAdminOrEndpointAdmin() { + if (Authentication.isAdmin()) { + return true; + } + + const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); + return rbacEnabled ? Authentication.hasAuthorizations(['EndpointResourcesAccess']) : false; + } + initView(); }, ]); diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index eb1d70aa4..14e0a89ef 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -334,8 +334,8 @@ -