From 93d8c179f1b7483d42c95ad946cc1cc7297c9947 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 29 Jul 2020 12:10:46 +0300 Subject: [PATCH] feat(containers): enforce disable bind mounts (#4110) * feat(containers): enforce disable bind mounts * refactor(docker): move check for endpoint admin to a function * feat(docker): check if service has bind mounts * feat(services): allow bind mounts for endpoint admin * feat(container): enable bind mounts for endpoint admin * fix(services): fix typo --- api/http/proxy/factory/docker/containers.go | 27 +++------ api/http/proxy/factory/docker/services.go | 55 +++++++++++++++++++ api/http/proxy/factory/docker/transport.go | 32 ++++++++++- .../create/createContainerController.js | 12 +++- .../containers/create/createcontainer.html | 4 +- .../create/createServiceController.js | 27 ++++++++- .../views/services/create/createservice.html | 2 +- 7 files changed, 132 insertions(+), 27 deletions(-) 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 @@ -
-
+
+
diff --git a/app/docker/views/services/create/createServiceController.js b/app/docker/views/services/create/createServiceController.js index 7500f5371..facba38b5 100644 --- a/app/docker/views/services/create/createServiceController.js +++ b/app/docker/views/services/create/createServiceController.js @@ -33,6 +33,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [ 'SettingsService', 'WebhookService', 'EndpointProvider', + 'ExtensionService', function ( $q, $scope, @@ -58,7 +59,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [ NodeService, SettingsService, WebhookService, - EndpointProvider + EndpointProvider, + ExtensionService ) { $scope.formValues = { Name: '', @@ -106,6 +108,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [ actionInProgress: false, }; + $scope.allowBindMounts = false; + $scope.refreshSlider = function () { $timeout(function () { $scope.$broadcast('rzSliderForceRender'); @@ -562,8 +566,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [ secrets: apiVersion >= 1.25 ? SecretService.secrets() : [], configs: apiVersion >= 1.3 ? ConfigService.configs() : [], nodes: NodeService.nodes(), - settings: SettingsService.publicSettings(), availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25), + allowBindMounts: checkIfAllowedBindMounts(), }) .then(function success(data) { $scope.availableVolumes = data.volumes; @@ -572,8 +576,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [ $scope.availableConfigs = data.configs; $scope.availableLoggingDrivers = data.availableLoggingDrivers; initSlidersMaxValuesBasedOnNodeData(data.nodes); - $scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers; $scope.isAdmin = Authentication.isAdmin(); + $scope.allowBindMounts = data.allowBindMounts; }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to initialize view'); @@ -581,5 +585,22 @@ angular.module('portainer.docker').controller('CreateServiceController', [ } initView(); + + async function checkIfAllowedBindMounts() { + const isAdmin = Authentication.isAdmin(); + + const settings = await SettingsService.publicSettings(); + const { AllowBindMountsForRegularUsers } = settings; + + if (isAdmin || AllowBindMountsForRegularUsers) { + return true; + } + const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); + if (rbacEnabled) { + return Authentication.hasAuthorizations(['EndpointResourcesAccess']); + } + + return false; + } }, ]); diff --git a/app/docker/views/services/create/createservice.html b/app/docker/views/services/create/createservice.html index 639274cc8..cc304b3e1 100644 --- a/app/docker/views/services/create/createservice.html +++ b/app/docker/views/services/create/createservice.html @@ -305,7 +305,7 @@
-
+