1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 05:45:22 +02:00

feat(endpoint): relocate docker security settings (#4657)

* feat(endpoint): migrate security settings to endpoint

* feat(endpoint): check for specific endpoint settings

* feat(endpoint): check security settings

* feat(docker): add config page

* feat(endpoint): save settings page

* feat(endpoints): disable features when not agent

* feat(sidebar): hide docker settings for regular user

* fix(docker): small fixes in configs

* fix(volumes): hide browse button for non admins

* refactor(docker): introduce switch component

* refactor(components/switch): seprate label from switch

* feat(app/components): align switch label

* refactor(app/components): move switch css

* fix(docker/settings): add ngijnect

* feat(endpoints): set default security values

* style(portainer): sort types

* fix(endpoint): rename security heading

* fix(endpoints): update endpoints settings
This commit is contained in:
Chaim Lev-Ari 2021-02-09 10:09:06 +02:00 committed by GitHub
parent e401724d43
commit 46dec01fe3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 714 additions and 461 deletions

View file

@ -40,18 +40,11 @@ func (store *Store) Init() error {
portainer.LDAPGroupSearchSettings{},
},
},
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,
OAuthSettings: portainer.OAuthSettings{},
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
}
err = store.SettingsService.UpdateSettings(defaultSettings)

View file

@ -0,0 +1,51 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
)
func (m *Migrator) updateEndpointSettingsToDB25() error {
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for i := range endpoints {
endpoint := endpoints[i]
securitySettings := portainer.EndpointSecuritySettings{}
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment ||
endpoint.Type == portainer.AgentOnDockerEnvironment ||
endpoint.Type == portainer.DockerEnvironment {
securitySettings = portainer.EndpointSecuritySettings{
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers,
}
if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
securitySettings.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers
securitySettings.EnableHostManagementFeatures = settings.EnableHostManagementFeatures
}
}
endpoint.SecuritySettings = securitySettings
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}

View file

@ -342,5 +342,13 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 2.1.0
if m.currentDBVersion < 26 {
err := m.updateEndpointSettingsToDB25()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}

View file

@ -248,6 +248,18 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
SecuritySettings: portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
},
}
if strings.HasPrefix(endpoint.URL, "tcp://") {
@ -297,6 +309,18 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
SecuritySettings: portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
},
}
err := snapshotService.SnapshotEndpoint(endpoint)

View file

@ -14,7 +14,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/internal/edge"
@ -440,6 +440,18 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint)
}
func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.Endpoint) error {
endpoint.SecuritySettings = portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
}
err := handler.DataStore.Endpoint().CreateEndpoint(endpoint)
if err != nil {
return err

View file

@ -0,0 +1,90 @@
package endpoints
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
)
type endpointSettingsUpdatePayload struct {
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"`
}
func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
return nil
}
// PUT request on /api/endpoints/:id/settings
func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
var payload endpointSettingsUpdatePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == errors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
securitySettings := endpoint.SecuritySettings
if payload.AllowBindMountsForRegularUsers != nil {
securitySettings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
}
if payload.AllowContainerCapabilitiesForRegularUsers != nil {
securitySettings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers
}
if payload.AllowDeviceMappingForRegularUsers != nil {
securitySettings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
}
if payload.AllowHostNamespaceForRegularUsers != nil {
securitySettings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers
}
if payload.AllowPrivilegedModeForRegularUsers != nil {
securitySettings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
}
if payload.AllowStackManagementForRegularUsers != nil {
securitySettings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers
}
if payload.AllowVolumeBrowserForRegularUsers != nil {
securitySettings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
}
if payload.EnableHostManagementFeatures != nil {
securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
}
endpoint.SecuritySettings = securitySettings
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting endpoint in database", err}
}
return response.JSON(w, endpoint)
}

View file

@ -39,6 +39,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/endpoints",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
h.Handle("/endpoints/{id}/settings",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSettingsUpdate))).Methods(http.MethodPut)
h.Handle("/endpoints/snapshot",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
h.Handle("/endpoints",

View file

@ -10,19 +10,11 @@ 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"`
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
OAuthLoginURI string `json:"OAuthLoginURI"`
EnableTelemetry bool `json:"EnableTelemetry"`
LogoURL string `json:"LogoURL"`
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
OAuthLoginURI string `json:"OAuthLoginURI"`
EnableTelemetry bool `json:"EnableTelemetry"`
}
// GET request on /api/settings/public
@ -33,18 +25,10 @@ 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,
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
EnableTelemetry: settings.EnableTelemetry,
LogoURL: settings.LogoURL,
AuthenticationMethod: settings.AuthenticationMethod,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
EnableTelemetry: settings.EnableTelemetry,
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
settings.OAuthSettings.AuthorizationURI,
settings.OAuthSettings.ClientID,

View file

@ -14,25 +14,17 @@ 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
AllowContainerCapabilitiesForRegularUsers *bool
EnableHostManagementFeatures *bool
SnapshotInterval *string
TemplatesURL *string
EdgeAgentCheckinInterval *int
EnableEdgeComputeFeatures *bool
UserSessionTimeout *string
EnableTelemetry *bool
LogoURL *string
BlackListedLabels []portainer.Pair
AuthenticationMethod *int
LDAPSettings *portainer.LDAPSettings
OAuthSettings *portainer.OAuthSettings
SnapshotInterval *string
TemplatesURL *string
EdgeAgentCheckinInterval *int
EnableEdgeComputeFeatures *bool
UserSessionTimeout *string
EnableTelemetry *bool
}
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
@ -107,38 +99,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.OAuthSettings.ClientSecret = clientSecret
}
if payload.AllowBindMountsForRegularUsers != nil {
settings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
}
if payload.AllowPrivilegedModeForRegularUsers != nil {
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
}
if payload.AllowVolumeBrowserForRegularUsers != nil {
settings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
}
if payload.EnableHostManagementFeatures != nil {
settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
}
if payload.EnableEdgeComputeFeatures != nil {
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
}
if payload.AllowHostNamespaceForRegularUsers != nil {
settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers
}
if payload.AllowStackManagementForRegularUsers != nil {
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 {
@ -158,10 +122,6 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
handler.JWTService.SetUserSessionDuration(userSessionDuration)
}
if payload.AllowDeviceMappingForRegularUsers != nil {
settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
}
if payload.EnableTelemetry != nil {
settings.EnableTelemetry = *payload.EnableTelemetry
}

View file

@ -339,21 +339,18 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
// clean it. Hence the use of the mutex.
// We should contribute to libcompose to support authentication without using the config.json file.
func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return err
}
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return err
}
if (!settings.AllowBindMountsForRegularUsers ||
!settings.AllowPrivilegedModeForRegularUsers ||
!settings.AllowHostNamespaceForRegularUsers ||
!settings.AllowDeviceMappingForRegularUsers ||
!settings.AllowContainerCapabilitiesForRegularUsers) &&
securitySettings := &config.endpoint.SecuritySettings
if (!securitySettings.AllowBindMountsForRegularUsers ||
!securitySettings.AllowPrivilegedModeForRegularUsers ||
!securitySettings.AllowHostNamespaceForRegularUsers ||
!securitySettings.AllowDeviceMappingForRegularUsers ||
!securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
!isAdminOrEndpointAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
@ -362,7 +359,7 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
return err
}
err = handler.isValidStackFile(stackContent, settings)
err = handler.isValidStackFile(stackContent, securitySettings)
if err != nil {
return err
}

View file

@ -344,16 +344,13 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
}
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return err
}
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return err
}
settings := &config.endpoint.SecuritySettings
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)

View file

@ -46,12 +46,14 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
if !settings.AllowStackManagementForRegularUsers {
if !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
@ -69,13 +71,6 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
@ -129,7 +124,7 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request,
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
}
func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *portainer.Settings) error {
func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error {
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
if err != nil {
return err
@ -154,7 +149,7 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port
for key := range composeConfig.Services {
service := composeConfig.Services[key]
if !settings.AllowBindMountsForRegularUsers {
if !securitySettings.AllowBindMountsForRegularUsers {
for _, volume := range service.Volumes {
if volume.Type == "bind" {
return errors.New("bind-mount disabled for non administrator users")
@ -162,19 +157,19 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *port
}
}
if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
if !securitySettings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
return errors.New("privileged mode disabled for non administrator users")
}
if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
if !securitySettings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
return errors.New("pid host disabled for non administrator users")
}
if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
if !securitySettings.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) {
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
return errors.New("container capabilities disabled for non administrator users")
}
}

View file

@ -9,7 +9,7 @@ import (
"net/http"
"github.com/docker/docker/client"
"github.com/portainer/portainer/api"
portainer "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"
@ -181,7 +181,7 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
}
if !isAdminOrEndpointAdmin {
settings, err := transport.dataStore.Settings().Settings()
securitySettings, err := transport.fetchEndpointSecuritySettings()
if err != nil {
return nil, err
}
@ -197,23 +197,23 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
return nil, err
}
if !settings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
if !securitySettings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
return forbiddenResponse, errors.New("forbidden to use privileged mode")
}
if !settings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
if !securitySettings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
return forbiddenResponse, errors.New("forbidden to use pid host namespace")
}
if !settings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 {
if !securitySettings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 {
return forbiddenResponse, errors.New("forbidden to use device mapping")
}
if !settings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
if !securitySettings.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) {
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
return forbiddenResponse, errors.New("forbidden to use bind mounts")
}

View file

@ -11,7 +11,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/internal/authorization"
)
@ -111,7 +111,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque
}
if !isAdminOrEndpointAdmin {
settings, err := transport.dataStore.Settings().Settings()
securitySettings, err := transport.fetchEndpointSecuritySettings()
if err != nil {
return nil, err
}
@ -127,7 +127,7 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque
return nil, err
}
if !settings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) {
if !securitySettings.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")

View file

@ -11,7 +11,7 @@ import (
"strings"
"github.com/docker/docker/client"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/http/security"
@ -407,12 +407,12 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
if tokenData.Role != portainer.AdministratorRole {
if volumeBrowseRestrictionCheck {
settings, err := transport.dataStore.Settings().Settings()
securitySettings, err := transport.fetchEndpointSecuritySettings()
if err != nil {
return nil, err
}
if !settings.AllowVolumeBrowserForRegularUsers {
if !securitySettings.AllowVolumeBrowserForRegularUsers {
return responseutils.WriteAccessDeniedResponse()
}
}
@ -682,3 +682,12 @@ func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool,
return tokenData.Role == portainer.AdministratorRole, nil
}
func (transport *Transport) fetchEndpointSecuritySettings() (*portainer.EndpointSecuritySettings, error) {
endpoint, err := transport.dataStore.Endpoint().Endpoint(portainer.EndpointID(transport.endpoint.ID))
if err != nil {
return nil, err
}
return &endpoint.SecuritySettings, nil
}

View file

@ -209,6 +209,7 @@ type (
EdgeCheckinInterval int `json:"EdgeCheckinInterval"`
Kubernetes KubernetesData `json:"Kubernetes"`
ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion"`
SecuritySettings EndpointSecuritySettings
// Deprecated fields
// Deprecated in DBVersion == 4
@ -272,6 +273,18 @@ type (
// Deprecated
EndpointSyncJob struct{}
// EndpointSecuritySettings represents settings for an endpoint
EndpointSecuritySettings struct {
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"`
}
// EndpointType represents the type of an endpoint
EndpointType int
@ -516,29 +529,31 @@ 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"`
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"`
EnableTelemetry bool `json:"EnableTelemetry"`
LogoURL string `json:"LogoURL"`
BlackListedLabels []Pair `json:"BlackListedLabels"`
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
LDAPSettings LDAPSettings `json:"LDAPSettings"`
OAuthSettings OAuthSettings `json:"OAuthSettings"`
SnapshotInterval string `json:"SnapshotInterval"`
TemplatesURL string `json:"TemplatesURL"`
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
UserSessionTimeout string `json:"UserSessionTimeout"`
EnableTelemetry bool `json:"EnableTelemetry"`
// Deprecated fields
DisplayDonationHeader bool
DisplayExternalContributors bool
// Deprecated fields v26
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
}
// SnapshotJob represents a scheduled job that can create endpoint snapshots
@ -1127,7 +1142,7 @@ const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.1.0"
// DBVersion is the version number of the Portainer database
DBVersion = 25
DBVersion = 26
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server