1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 08:19:40 +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

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