feat(settings): add setting to disable device mapping for regular users (#4017)
* feat(settings): introduce device mapping service * feat(containers): hide devices field when setting is on * feat(containers): prevent passing of devices when not allowed * feat(stacks): prevent non admin from device mapping * feat(stacks): disallow swarm stack creation for user * refactor(settings): replace disableDeviceMapping with allow * fix(stacks): remove check for disable device mappings from swarm * feat(settings): rename field to disable * feat(settings): supply default value for disableDeviceMapping * feat(container): check for endpoint admin
This commit is contained in:
@@ -1,6 +1,19 @@
|
||||
package migrator
|
||||
|
||||
import "github.com/portainer/portainer/api"
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateSettingsToDBVersion23() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.AllowDeviceMappingForRegularUsers = true
|
||||
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
|
||||
func (m *Migrator) updateTagsToDBVersion23() error {
|
||||
tags, err := m.tagService.Tags()
|
||||
|
||||
@@ -320,6 +320,11 @@ func (m *Migrator) Migrate() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateSettingsToDBVersion23()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
|
||||
@@ -273,6 +273,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
EnableHostManagementFeatures: false,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
SnapshotInterval: *flags.SnapshotInterval,
|
||||
|
||||
@@ -21,6 +21,7 @@ type publicSettingsResponse struct {
|
||||
OAuthLoginURI string `json:"OAuthLoginURI"`
|
||||
DisableStackManagementForRegularUsers bool `json:"DisableStackManagementForRegularUsers"`
|
||||
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
|
||||
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
|
||||
}
|
||||
|
||||
// GET request on /api/settings/public
|
||||
@@ -45,6 +46,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
||||
settings.OAuthSettings.ClientID,
|
||||
settings.OAuthSettings.RedirectURI,
|
||||
settings.OAuthSettings.Scopes),
|
||||
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
|
||||
DisableStackManagementForRegularUsers: settings.DisableStackManagementForRegularUsers,
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ type settingsUpdatePayload struct {
|
||||
EnableEdgeComputeFeatures *bool
|
||||
DisableStackManagementForRegularUsers *bool
|
||||
AllowHostNamespaceForRegularUsers *bool
|
||||
AllowDeviceMappingForRegularUsers *bool
|
||||
}
|
||||
|
||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
@@ -135,6 +136,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
settings.EdgeAgentCheckinInterval = *payload.EdgeAgentCheckinInterval
|
||||
}
|
||||
|
||||
if payload.AllowDeviceMappingForRegularUsers != nil {
|
||||
settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
|
||||
}
|
||||
|
||||
tlsError := handler.updateTLS(settings)
|
||||
if tlsError != nil {
|
||||
return tlsError
|
||||
|
||||
@@ -335,7 +335,11 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
|
||||
return err
|
||||
}
|
||||
|
||||
if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers || !settings.AllowHostNamespaceForRegularUsers) && !isAdminOrEndpointAdmin {
|
||||
if (!settings.AllowBindMountsForRegularUsers ||
|
||||
!settings.AllowPrivilegedModeForRegularUsers ||
|
||||
!settings.AllowHostNamespaceForRegularUsers ||
|
||||
!settings.AllowDeviceMappingForRegularUsers) && !isAdminOrEndpointAdmin {
|
||||
|
||||
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
||||
|
||||
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
|
||||
|
||||
@@ -341,6 +341,7 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
|
||||
}
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
|
||||
|
||||
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
||||
|
||||
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
|
||||
|
||||
@@ -160,6 +160,10 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port
|
||||
if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
|
||||
return errors.New("pid host disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
|
||||
return errors.New("device mapping disabled for non administrator users")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -156,8 +156,9 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB
|
||||
func (transport *Transport) decorateContainerCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||
type PartialContainer struct {
|
||||
HostConfig struct {
|
||||
Privileged bool `json:"Privileged"`
|
||||
PidMode string `json:"PidMode"`
|
||||
Privileged bool `json:"Privileged"`
|
||||
PidMode string `json:"PidMode"`
|
||||
Devices []interface{} `json:"Devices"`
|
||||
} `json:"HostConfig"`
|
||||
}
|
||||
|
||||
@@ -186,14 +187,18 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
|
||||
endpointResourceAccess = true
|
||||
}
|
||||
|
||||
if (rbacExtension != nil && !endpointResourceAccess && tokenData.Role != portainer.AdministratorRole) || (rbacExtension == nil && tokenData.Role != portainer.AdministratorRole) {
|
||||
isAdmin := (rbacExtension != nil && endpointResourceAccess) || tokenData.Role == portainer.AdministratorRole
|
||||
|
||||
if !isAdmin {
|
||||
settings, err := transport.settingsService.Settings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !settings.AllowPrivilegedModeForRegularUsers || !settings.AllowHostNamespaceForRegularUsers {
|
||||
if !settings.AllowPrivilegedModeForRegularUsers ||
|
||||
!settings.AllowHostNamespaceForRegularUsers ||
|
||||
!settings.AllowDeviceMappingForRegularUsers {
|
||||
|
||||
body, err := ioutil.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -213,6 +218,10 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
|
||||
return forbiddenResponse, errors.New("forbidden to use pid host namespace")
|
||||
}
|
||||
|
||||
if len(partialContainer.HostConfig.Devices) > 0 {
|
||||
return nil, errors.New("forbidden to use device mapping")
|
||||
}
|
||||
|
||||
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,6 +435,7 @@ type (
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||
DisableStackManagementForRegularUsers bool `json:"DisableStackManagementForRegularUsers"`
|
||||
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
|
||||
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
|
||||
|
||||
// Deprecated fields
|
||||
DisplayDonationHeader bool
|
||||
|
||||
@@ -30,6 +30,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
'SettingsService',
|
||||
'PluginService',
|
||||
'HttpRequestHelper',
|
||||
'ExtensionService',
|
||||
function (
|
||||
$q,
|
||||
$scope,
|
||||
@@ -55,7 +56,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
SystemService,
|
||||
SettingsService,
|
||||
PluginService,
|
||||
HttpRequestHelper
|
||||
HttpRequestHelper,
|
||||
ExtensionService
|
||||
) {
|
||||
$scope.create = create;
|
||||
|
||||
@@ -603,7 +605,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
});
|
||||
}
|
||||
|
||||
function initView() {
|
||||
async function initView() {
|
||||
var nodeName = $transition$.params().nodeName;
|
||||
$scope.formValues.NodeName = nodeName;
|
||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||
@@ -682,6 +684,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
});
|
||||
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
$scope.showDeviceMapping = await shouldShowDevices();
|
||||
}
|
||||
|
||||
function validateForm(accessControlData, isAdmin) {
|
||||
@@ -894,6 +897,19 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
||||
}
|
||||
}
|
||||
|
||||
async function shouldShowDevices() {
|
||||
const isAdmin = !$scope.applicationState.application.authentication || Authentication.isAdmin();
|
||||
const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application;
|
||||
|
||||
if (isAdmin || allowDeviceMappingForRegularUsers) {
|
||||
return true;
|
||||
}
|
||||
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
||||
if (rbacEnabled) {
|
||||
return Authentication.hasAuthorizations(['EndpointResourcesAccess']);
|
||||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -629,7 +629,7 @@
|
||||
</form>
|
||||
<form class="form-horizontal" style="margin-top: 15px;">
|
||||
<!-- devices -->
|
||||
<div class="form-group">
|
||||
<div ng-if="showDeviceMapping" class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Devices</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addDevice()">
|
||||
|
||||
@@ -15,6 +15,7 @@ export function SettingsViewModel(data) {
|
||||
this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures;
|
||||
this.DisableStackManagementForRegularUsers = data.DisableStackManagementForRegularUsers;
|
||||
this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers;
|
||||
this.AllowDeviceMappingForRegularUsers = data.AllowDeviceMappingForRegularUsers;
|
||||
}
|
||||
|
||||
export function PublicSettingsViewModel(settings) {
|
||||
@@ -28,6 +29,7 @@ export function PublicSettingsViewModel(settings) {
|
||||
this.LogoURL = settings.LogoURL;
|
||||
this.OAuthLoginURI = settings.OAuthLoginURI;
|
||||
this.DisableStackManagementForRegularUsers = settings.DisableStackManagementForRegularUsers;
|
||||
this.AllowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers;
|
||||
}
|
||||
|
||||
export function LDAPSettingsViewModel(data) {
|
||||
|
||||
@@ -81,6 +81,11 @@ angular.module('portainer.app').factory('StateManager', [
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateAllowDeviceMappingForRegularUsers = function updateAllowDeviceMappingForRegularUsers(allowDeviceMappingForRegularUsers) {
|
||||
state.application.allowDeviceMappingForRegularUsers = allowDeviceMappingForRegularUsers;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
};
|
||||
|
||||
manager.updateAllowHostNamespaceForRegularUsers = function (allowHostNamespaceForRegularUsers) {
|
||||
state.application.allowHostNamespaceForRegularUsers = allowHostNamespaceForRegularUsers;
|
||||
LocalStorage.storeApplicationState(state.application);
|
||||
@@ -98,6 +103,7 @@ angular.module('portainer.app').factory('StateManager', [
|
||||
state.application.enableVolumeBrowserForNonAdminUsers = settings.AllowVolumeBrowserForRegularUsers;
|
||||
state.application.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||
state.application.disableStackManagementForRegularUsers = settings.DisableStackManagementForRegularUsers;
|
||||
state.application.allowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers;
|
||||
state.application.validity = moment().unix();
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,17 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_disableDeviceMappingForRegularUsers" class="control-label text-left">
|
||||
Disable device mappings for non-administrators
|
||||
</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" name="toggle_disableDeviceMappingForRegularUsers" ng-model="formValues.disableDeviceMappingForRegularUsers" /><i></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !security -->
|
||||
<!-- edge -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
|
||||
@@ -35,6 +35,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
enableEdgeComputeFeatures: false,
|
||||
disableStackManagementForRegularUsers: false,
|
||||
restrictHostNamespaceForRegularUsers: false,
|
||||
allowDeviceMappingForRegularUsers: false,
|
||||
};
|
||||
|
||||
$scope.removeFilteredContainerLabel = function (index) {
|
||||
@@ -73,6 +74,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures;
|
||||
settings.DisableStackManagementForRegularUsers = $scope.formValues.disableStackManagementForRegularUsers;
|
||||
settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers;
|
||||
settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
updateSettings(settings);
|
||||
@@ -89,6 +91,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures);
|
||||
StateManager.updateDisableStackManagementForRegularUsers(settings.DisableStackManagementForRegularUsers);
|
||||
StateManager.updateAllowHostNamespaceForRegularUsers(settings.AllowHostNamespaceForRegularUsers);
|
||||
StateManager.updateAllowDeviceMappingForRegularUsers(settings.AllowDeviceMappingForRegularUsers);
|
||||
$state.reload();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
@@ -117,6 +120,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||
$scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||
$scope.formValues.disableStackManagementForRegularUsers = settings.DisableStackManagementForRegularUsers;
|
||||
$scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers;
|
||||
$scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
||||
|
||||
Reference in New Issue
Block a user