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
pull/4126/head
Chaim Lev-Ari 2020-07-29 12:10:46 +03:00 committed by GitHub
parent 7539f09f98
commit 93d8c179f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 132 additions and 27 deletions

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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 := &registryAccessContext{
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
}

View File

@ -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();
},
]);

View File

@ -334,8 +334,8 @@
</div>
<!-- !container-path -->
<!-- volume-type -->
<div class="input-group col-sm-5" style="margin-left: 5px;" ng-if="isAdmin || allowBindMounts">
<div class="btn-group btn-group-sm">
<div class="input-group col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm" ng-if="allowBindMounts">
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.name = ''">Bind</label>
</div>

View File

@ -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;
}
},
]);

View File

@ -305,7 +305,7 @@
<!-- !container-path -->
<!-- volume-type -->
<div class="input-group col-sm-5" style="margin-left: 5px;">
<div class="btn-group btn-group-sm" ng-if="isAdmin || allowBindMounts">
<div class="btn-group btn-group-sm" ng-if="allowBindMounts">
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Id = ''">Bind</label>
</div>