diff --git a/api/bolt/migrator/migrate_dbversion24.go b/api/bolt/migrator/migrate_dbversion24.go new file mode 100644 index 000000000..b312d1037 --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion24.go @@ -0,0 +1,12 @@ +package migrator + +func (m *Migrator) updateSettingsToDBVersion25() error { + legacySettings, err := m.settingsService.Settings() + if err != nil { + return err + } + + legacySettings.AllowContainerCapabilitiesForRegularUsers = true + + return m.settingsService.UpdateSettings(legacySettings) +} diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index d8afd0e77..9ef3cecad 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -2,7 +2,7 @@ package migrator import ( "github.com/boltdb/bolt" - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/bolt/endpoint" "github.com/portainer/portainer/api/bolt/endpointgroup" "github.com/portainer/portainer/api/bolt/endpointrelation" @@ -330,5 +330,13 @@ func (m *Migrator) Migrate() error { } } + // Portainer 1.24.2 + if m.currentDBVersion < 25 { + err := m.updateSettingsToDBVersion25() + if err != nil { + return err + } + } + return m.versionService.StoreDBVersion(portainer.DBVersion) } diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index e5a4589c7..567ccc6ec 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -269,16 +269,17 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL portainer.LDAPGroupSearchSettings{}, }, }, - OAuthSettings: portainer.OAuthSettings{}, - AllowBindMountsForRegularUsers: true, - AllowPrivilegedModeForRegularUsers: true, - AllowVolumeBrowserForRegularUsers: false, - AllowDeviceMappingForRegularUsers: true, - AllowStackManagementForRegularUsers: true, - EnableHostManagementFeatures: false, - AllowHostNamespaceForRegularUsers: true, - SnapshotInterval: *flags.SnapshotInterval, - EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, + OAuthSettings: portainer.OAuthSettings{}, + AllowBindMountsForRegularUsers: true, + AllowPrivilegedModeForRegularUsers: true, + AllowVolumeBrowserForRegularUsers: false, + AllowDeviceMappingForRegularUsers: true, + AllowStackManagementForRegularUsers: true, + AllowContainerCapabilitiesForRegularUsers: true, + EnableHostManagementFeatures: false, + AllowHostNamespaceForRegularUsers: true, + SnapshotInterval: *flags.SnapshotInterval, + EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds, } if *flags.Templates != "" { diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index 7d23319dc..cbcb8614c 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -10,18 +10,19 @@ 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"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - ExternalTemplates bool `json:"ExternalTemplates"` - OAuthLoginURI string `json:"OAuthLoginURI"` - AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + LogoURL string `json:"LogoURL"` + AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"` + AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"` + AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"` + AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + ExternalTemplates bool `json:"ExternalTemplates"` + OAuthLoginURI string `json:"OAuthLoginURI"` + AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"` } // GET request on /api/settings/public @@ -32,16 +33,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, - EnableHostManagementFeatures: settings.EnableHostManagementFeatures, - EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, - AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, - AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers, - ExternalTemplates: false, + LogoURL: settings.LogoURL, + AuthenticationMethod: settings.AuthenticationMethod, + AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers, + AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers, + AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers, + EnableHostManagementFeatures: settings.EnableHostManagementFeatures, + EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures, + AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers, + AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers, + ExternalTemplates: false, + AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers, 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 308253b1e..2e5fa347b 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -12,22 +12,23 @@ import ( ) type settingsUpdatePayload struct { - LogoURL *string - BlackListedLabels []portainer.Pair - AuthenticationMethod *int - LDAPSettings *portainer.LDAPSettings - OAuthSettings *portainer.OAuthSettings - AllowBindMountsForRegularUsers *bool - AllowPrivilegedModeForRegularUsers *bool - AllowVolumeBrowserForRegularUsers *bool - EnableHostManagementFeatures *bool - SnapshotInterval *string - TemplatesURL *string - EdgeAgentCheckinInterval *int - EnableEdgeComputeFeatures *bool - AllowStackManagementForRegularUsers *bool - AllowHostNamespaceForRegularUsers *bool - AllowDeviceMappingForRegularUsers *bool + LogoURL *string + BlackListedLabels []portainer.Pair + AuthenticationMethod *int + LDAPSettings *portainer.LDAPSettings + OAuthSettings *portainer.OAuthSettings + AllowBindMountsForRegularUsers *bool + AllowPrivilegedModeForRegularUsers *bool + AllowVolumeBrowserForRegularUsers *bool + EnableHostManagementFeatures *bool + SnapshotInterval *string + TemplatesURL *string + EdgeAgentCheckinInterval *int + EnableEdgeComputeFeatures *bool + AllowStackManagementForRegularUsers *bool + AllowHostNamespaceForRegularUsers *bool + AllowDeviceMappingForRegularUsers *bool + AllowContainerCapabilitiesForRegularUsers *bool } func (payload *settingsUpdatePayload) Validate(r *http.Request) error { @@ -125,6 +126,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) * settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers } + 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 f5e424385..fdadf9c5f 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -338,7 +338,9 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) if (!settings.AllowBindMountsForRegularUsers || !settings.AllowPrivilegedModeForRegularUsers || !settings.AllowHostNamespaceForRegularUsers || - !settings.AllowDeviceMappingForRegularUsers) && !isAdminOrEndpointAdmin { + !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 4ac2b3638..2d7465cdb 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -164,6 +164,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 bda4db4d6..b4463afa1 100644 --- a/api/http/proxy/factory/docker/containers.go +++ b/api/http/proxy/factory/docker/containers.go @@ -159,6 +159,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"` } @@ -197,7 +199,8 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req if !settings.AllowPrivilegedModeForRegularUsers || !settings.AllowHostNamespaceForRegularUsers || - !settings.AllowDeviceMappingForRegularUsers { + !settings.AllowDeviceMappingForRegularUsers || + !settings.AllowContainerCapabilitiesForRegularUsers { body, err := ioutil.ReadAll(request.Body) if err != nil { @@ -210,18 +213,22 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req return nil, err } - if partialContainer.HostConfig.Privileged { + if !settings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged { return forbiddenResponse, errors.New("forbidden to use privileged mode") } - if partialContainer.HostConfig.PidMode == "host" { + if !settings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" { return forbiddenResponse, errors.New("forbidden to use pid host namespace") } - if len(partialContainer.HostConfig.Devices) > 0 { + if !settings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 { 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 243915d82..13348813d 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -420,22 +420,23 @@ 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"` - SnapshotInterval string `json:"SnapshotInterval"` - TemplatesURL string `json:"TemplatesURL"` - EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` - EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` - EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` - AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` - AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` - AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + 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"` + SnapshotInterval string `json:"SnapshotInterval"` + TemplatesURL string `json:"TemplatesURL"` + EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"` + EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"` + EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"` + AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"` + AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"` + AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"` + AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"` // Deprecated fields DisplayDonationHeader bool diff --git a/app/docker/views/containers/create/createContainerController.js b/app/docker/views/containers/create/createContainerController.js index 22889b928..17b85b2d0 100644 --- a/app/docker/views/containers/create/createContainerController.js +++ b/app/docker/views/containers/create/createContainerController.js @@ -610,6 +610,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) { @@ -645,7 +649,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) { @@ -682,9 +686,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) { @@ -897,17 +898,26 @@ 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) { + async function isAdminOrEndpointAdmin() { + const isAdmin = Authentication.isAdmin(); + 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 266ee7e40..559e2253b 100644 --- a/app/docker/views/containers/create/createcontainer.html +++ b/app/docker/views/containers/create/createcontainer.html @@ -185,7 +185,7 @@