diff --git a/api/bolt/init.go b/api/bolt/init.go index 885966bbe..29793a823 100644 --- a/api/bolt/init.go +++ b/api/bolt/init.go @@ -24,17 +24,18 @@ func (store *Store) Init() error { portainer.LDAPGroupSearchSettings{}, }, }, - OAuthSettings: portainer.OAuthSettings{}, - AllowBindMountsForRegularUsers: true, - AllowPrivilegedModeForRegularUsers: true, - AllowVolumeBrowserForRegularUsers: false, - AllowHostNamespaceForRegularUsers: true, - AllowDeviceMappingForRegularUsers: true, - AllowStackManagementForRegularUsers: true, - EnableHostManagementFeatures: false, - EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, - TemplatesURL: portainer.DefaultTemplatesURL, - UserSessionTimeout: portainer.DefaultUserSessionTimeout, + OAuthSettings: portainer.OAuthSettings{}, + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowVolumeBrowserForRegularUsers: false, + AllowHostNamespaceForRegularUsers: true, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + AllowContainerCapabilitiesForRegularUsers: true, + EnableHostManagementFeatures: false, + EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, + TemplatesURL: portainer.DefaultTemplatesURL, + UserSessionTimeout: portainer.DefaultUserSessionTimeout, } err = store.SettingsService.UpdateSettings(defaultSettings) diff --git a/api/bolt/migrator/migrate_dbversion24.go b/api/bolt/migrator/migrate_dbversion24.go index d1dc5f0cf..4749607c5 100644 --- a/api/bolt/migrator/migrate_dbversion24.go +++ b/api/bolt/migrator/migrate_dbversion24.go @@ -16,5 +16,7 @@ func (m *Migrator) updateSettingsToDB25() error { legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout + legacySettings.AllowContainerCapabilitiesForRegularUsers = true + return m.settingsService.UpdateSettings(legacySettings) } diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 6bd3a581b..f0d4f422a 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -10,17 +10,18 @@ import ( ) type publicSettingsResponse struct { - LogoURL string `json:"LogoURL"` - AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` - AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` - AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` - AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` - AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - OAuthLoginURI string `json:"OAuthLoginURI"` + LogoURL string `json:"LogoURL"` + AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` + AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + OAuthLoginURI string `json:"OAuthLoginURI"` } // GET request on /api/settings/public @@ -31,16 +32,17 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) * } publicSettings := &publicSettingsResponse{ - LogoURL: settings.LogoURL, - AuthenticationMethod: settings.AuthenticationMethod, - AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, - AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, - AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, - AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, - AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, - AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers, - EnableHostManagementFeatures: settings.EnableHostManagementFeatures, - EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, + LogoURL: settings.LogoURL, + AuthenticationMethod: settings.AuthenticationMethod, + AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, + AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, + AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, + AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, + AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers, + AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers, + AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers, + EnableHostManagementFeatures: settings.EnableHostManagementFeatures, + EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login", settings.OAuthSettings.AuthorizationURI, settings.OAuthSettings.ClientID, diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 59fbffd28..7b2a74f0e 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -15,23 +15,24 @@ import ( ) type settingsUpdatePayload struct { - LogoURL *string - BlackListedLabels []portainer.Pair - AuthenticationMethod *int - LDAPSettings *portainer.LDAPSettings - OAuthSettings *portainer.OAuthSettings - AllowBindMountsForRegularUsers *bool - AllowPrivilegedModeForRegularUsers *bool - AllowHostNamespaceForRegularUsers *bool - AllowVolumeBrowserForRegularUsers *bool - AllowDeviceMappingForRegularUsers *bool - AllowStackManagementForRegularUsers *bool - EnableHostManagementFeatures *bool - SnapshotInterval *string - TemplatesURL *string - EdgeAgentCheckinInterval *int - EnableEdgeComputeFeatures *bool - UserSessionTimeout *string + LogoURL *string + BlackListedLabels []portainer.Pair + AuthenticationMethod *int + LDAPSettings *portainer.LDAPSettings + OAuthSettings *portainer.OAuthSettings + AllowBindMountsForRegularUsers *bool + AllowPrivilegedModeForRegularUsers *bool + AllowHostNamespaceForRegularUsers *bool + AllowVolumeBrowserForRegularUsers *bool + AllowDeviceMappingForRegularUsers *bool + AllowStackManagementForRegularUsers *bool + AllowContainerCapabilitiesForRegularUsers *bool + EnableHostManagementFeatures *bool + SnapshotInterval *string + TemplatesURL *string + EdgeAgentCheckinInterval *int + EnableEdgeComputeFeatures *bool + UserSessionTimeout *string } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -136,6 +137,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers } + if payload.AllowContainerCapabilitiesForRegularUsers != nil { + settings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers + } + if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval { err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval) if err != nil { diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index af79889f4..aa47eacd7 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -339,7 +339,8 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers || !settings.AllowHostNamespaceForRegularUsers || - !settings.AllowDeviceMappingForRegularUsers) && + !settings.AllowDeviceMappingForRegularUsers || + !settings.AllowContainerCapabilitiesForRegularUsers) && !isAdminOrEndpointAdmin { composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint) diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 23e18bf5c..daec00366 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -173,6 +173,10 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 { return errors.New("device mapping disabled for non administrator users") } + + if !settings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) { + return errors.New("container capabilities disabled for non administrator users") + } } return nil diff --git a/api/http/proxy/factory/docker/containers.go b/api/http/proxy/factory/docker/containers.go index d957be030..df723668c 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -161,6 +161,8 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req Privileged bool `json:"Privileged"` PidMode string `json:"PidMode"` Devices []interface{} `json:"Devices"` + CapAdd []string `json:"CapAdd"` + CapDrop []string `json:"CapDrop"` } `json:"HostConfig"` } @@ -220,6 +222,10 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return nil, 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") + } + request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) } diff --git a/api/portainer.go b/api/portainer.go index 4f17208a3..c5a0db217 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -510,23 +510,24 @@ type ( // Settings represents the application settings Settings struct { - LogoURL string `json:"LogoURL"` - BlackListedLabels []Pair `json:"BlackListedLabels"` - AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` - LDAPSettings LDAPSettings `json:"LDAPSettings"` - OAuthSettings OAuthSettings `json:"OAuthSettings"` - AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` - AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` - AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` - AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` - SnapshotInterval string `json:"SnapshotInterval"` - TemplatesURL string `json:"TemplatesURL"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - UserSessionTimeout string `json:"UserSessionTimeout"` + LogoURL string `json:"LogoURL"` + BlackListedLabels []Pair `json:"BlackListedLabels"` + AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"` + LDAPSettings LDAPSettings `json:"LDAPSettings"` + OAuthSettings OAuthSettings `json:"OAuthSettings"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` + AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"` + SnapshotInterval string `json:"SnapshotInterval"` + TemplatesURL string `json:"TemplatesURL"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + UserSessionTimeout string `json:"UserSessionTimeout"` // Deprecated fields DisplayDonationHeader bool diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 412cfdadf..4ce386800 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -611,6 +611,10 @@ angular.module('portainer.docker').controller('CreateContainerController', [ $scope.formValues.NodeName = nodeName; HttpRequestHelper.setPortainerAgentTargetHeader(nodeName); + $scope.isAdmin = Authentication.isAdmin(); + $scope.showDeviceMapping = await shouldShowDevices(); + $scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled(); + Volume.query( {}, function (d) { @@ -647,7 +651,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [ loadFromContainerSpec(); } else { $scope.fromContainer = {}; - $scope.formValues.capabilities = new ContainerCapabilities(); + $scope.formValues.capabilities = $scope.areContainerCapabilitiesEnabled ? new ContainerCapabilities() : []; } }, function (e) { @@ -684,9 +688,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [ PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) { $scope.availableLoggingDrivers = loggingDrivers; }); - - $scope.isAdmin = Authentication.isAdmin(); - $scope.showDeviceMapping = await shouldShowDevices(); } function validateForm(accessControlData, isAdmin) { @@ -899,17 +900,26 @@ angular.module('portainer.docker').controller('CreateContainerController', [ } } - async function shouldShowDevices() { + async function isAdminOrEndpointAdmin() { const isAdmin = Authentication.isAdmin(); - const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application; - - if (isAdmin || allowDeviceMappingForRegularUsers) { + if (isAdmin) { return true; } + const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC); - if (rbacEnabled) { - return Authentication.hasAuthorizations(['EndpointResourcesAccess']); - } + return rbacEnabled ? Authentication.hasAuthorizations(['EndpointResourcesAccess']) : false; + } + + async function shouldShowDevices() { + const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application; + + return allowDeviceMappingForRegularUsers || isAdminOrEndpointAdmin(); + } + + async function checkIfContainerCapabilitiesEnabled() { + const { allowContainerCapabilitiesForRegularUsers } = $scope.applicationState.application; + + return allowContainerCapabilitiesForRegularUsers || isAdminOrEndpointAdmin(); } initView(); diff --git a/app/docker/views/containers/create/createcontainer.html b/app/docker/views/containers/create/createcontainer.html index 5ee68a229..eb1d70aa4 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -181,7 +181,7 @@