Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39e9dca7b8 | |||
| cfdd38c55e | |||
| a12a0b61dc | |||
| d2cdbf789e | |||
| 06db4e0ad4 | |||
| 9f92e0aee3 | |||
| f347d97daf | |||
| d5cee5b8b1 | |||
| 4da6824bc7 | |||
| 80b6b6e300 | |||
| 484dab5932 | |||
| f8bd075ce4 | |||
| cd58c16b4e | |||
| 5ebb03cb4e | |||
| dffcd3fdfd | |||
| 3f7687e78a | |||
| 0f58ece899 | |||
| b0ad212858 | |||
| 7eb2fd3424 | |||
| 4c0d8ce732 | |||
| e1cc4bc9a1 |
@@ -1,6 +1,8 @@
|
|||||||
package migrator
|
package migrator
|
||||||
|
|
||||||
import "github.com/portainer/portainer/api"
|
import (
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
func (m *Migrator) updateTagsToDBVersion23() error {
|
func (m *Migrator) updateTagsToDBVersion23() error {
|
||||||
tags, err := m.tagService.Tags()
|
tags, err := m.tagService.Tags()
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package migrator
|
||||||
|
|
||||||
|
func (m *Migrator) updateSettingsToDBVersion24() error {
|
||||||
|
legacySettings, err := m.settingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
legacySettings.AllowDeviceMappingForRegularUsers = true
|
||||||
|
legacySettings.AllowStackManagementForRegularUsers = true
|
||||||
|
legacySettings.AllowHostNamespaceForRegularUsers = true
|
||||||
|
|
||||||
|
return m.settingsService.UpdateSettings(legacySettings)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package migrator
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/boltdb/bolt"
|
"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/endpoint"
|
||||||
"github.com/portainer/portainer/api/bolt/endpointgroup"
|
"github.com/portainer/portainer/api/bolt/endpointgroup"
|
||||||
"github.com/portainer/portainer/api/bolt/endpointrelation"
|
"github.com/portainer/portainer/api/bolt/endpointrelation"
|
||||||
@@ -322,5 +322,21 @@ func (m *Migrator) Migrate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Portainer 1.24.1
|
||||||
|
if m.currentDBVersion < 24 {
|
||||||
|
err := m.updateSettingsToDBVersion24()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Portainer 1.24.2
|
||||||
|
if m.currentDBVersion < 25 {
|
||||||
|
err := m.updateSettingsToDBVersion25()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/portainer/portainer/api/chisel"
|
"github.com/portainer/portainer/api/chisel"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/bolt"
|
"github.com/portainer/portainer/api/bolt"
|
||||||
"github.com/portainer/portainer/api/cli"
|
"github.com/portainer/portainer/api/cli"
|
||||||
"github.com/portainer/portainer/api/cron"
|
"github.com/portainer/portainer/api/cron"
|
||||||
@@ -269,13 +269,17 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
|||||||
portainer.LDAPGroupSearchSettings{},
|
portainer.LDAPGroupSearchSettings{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
OAuthSettings: portainer.OAuthSettings{},
|
OAuthSettings: portainer.OAuthSettings{},
|
||||||
AllowBindMountsForRegularUsers: true,
|
AllowBindMountsForRegularUsers: true,
|
||||||
AllowPrivilegedModeForRegularUsers: true,
|
AllowPrivilegedModeForRegularUsers: true,
|
||||||
AllowVolumeBrowserForRegularUsers: false,
|
AllowVolumeBrowserForRegularUsers: false,
|
||||||
EnableHostManagementFeatures: false,
|
AllowDeviceMappingForRegularUsers: true,
|
||||||
SnapshotInterval: *flags.SnapshotInterval,
|
AllowStackManagementForRegularUsers: true,
|
||||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
AllowContainerCapabilitiesForRegularUsers: true,
|
||||||
|
EnableHostManagementFeatures: false,
|
||||||
|
AllowHostNamespaceForRegularUsers: true,
|
||||||
|
SnapshotInterval: *flags.SnapshotInterval,
|
||||||
|
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||||
}
|
}
|
||||||
|
|
||||||
if *flags.Templates != "" {
|
if *flags.Templates != "" {
|
||||||
|
|||||||
@@ -6,6 +6,24 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CreateTLSConfiguration creates a basic tls.Config to be used by servers with recommended TLS settings
|
||||||
|
func CreateServerTLSConfiguration() *tls.Config {
|
||||||
|
return &tls.Config{
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
CipherSuites: []uint16{
|
||||||
|
tls.TLS_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
|
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
|
||||||
// loaded from memory.
|
// loaded from memory.
|
||||||
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
|
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"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/crypto"
|
||||||
"github.com/portainer/portainer/api/http/client"
|
"github.com/portainer/portainer/api/http/client"
|
||||||
)
|
)
|
||||||
@@ -261,13 +261,13 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
|||||||
TLSConfig: portainer.TLSConfiguration{
|
TLSConfig: portainer.TLSConfiguration{
|
||||||
TLS: false,
|
TLS: false,
|
||||||
},
|
},
|
||||||
AuthorizedUsers: []portainer.UserID{},
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
TagIDs: payload.TagIDs,
|
TagIDs: payload.TagIDs,
|
||||||
Status: portainer.EndpointStatusUp,
|
Status: portainer.EndpointStatusUp,
|
||||||
Snapshots: []portainer.Snapshot{},
|
Snapshots: []portainer.Snapshot{},
|
||||||
EdgeKey: edgeKey,
|
EdgeKey: edgeKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
err = handler.saveEndpointAndUpdateAuthorizations(endpoint)
|
||||||
|
|||||||
@@ -41,16 +41,21 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request)
|
|||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions status from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions status from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, existingExtension := range extensions {
|
|
||||||
if existingExtension.ID == extensionID && existingExtension.Enabled {
|
|
||||||
return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", portainer.ErrExtensionAlreadyEnabled}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension := &portainer.Extension{
|
extension := &portainer.Extension{
|
||||||
ID: extensionID,
|
ID: extensionID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, existingExtension := range extensions {
|
||||||
|
if existingExtension.ID == extensionID && (existingExtension.Enabled || !existingExtension.License.Valid) {
|
||||||
|
if existingExtension.License.LicenseKey == payload.License {
|
||||||
|
return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", portainer.ErrExtensionAlreadyEnabled}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = handler.ExtensionManager.DisableExtension(&existingExtension)
|
||||||
|
extension.Enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extensionDefinitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
extensionDefinitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension definitions", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension definitions", err}
|
||||||
@@ -68,15 +73,14 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request)
|
|||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to enable extension", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to enable extension", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension.Enabled = true
|
if extension.ID == portainer.RBACExtension && !extension.Enabled {
|
||||||
|
|
||||||
if extension.ID == portainer.RBACExtension {
|
|
||||||
err = handler.upgradeRBACData()
|
err = handler.upgradeRBACData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension.Enabled = true
|
||||||
err = handler.ExtensionService.Persist(extension)
|
err = handler.ExtensionService.Persist(extension)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
||||||
|
|||||||
@@ -46,10 +46,21 @@ func (handler *Handler) extensionUpload(w http.ResponseWriter, r *http.Request)
|
|||||||
}
|
}
|
||||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||||
|
|
||||||
|
extensions, err := handler.ExtensionService.Extensions()
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions status from the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
extension := &portainer.Extension{
|
extension := &portainer.Extension{
|
||||||
ID: extensionID,
|
ID: extensionID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, existingExtension := range extensions {
|
||||||
|
if existingExtension.ID == extensionID && (existingExtension.Enabled || !existingExtension.License.Valid) {
|
||||||
|
extension.Enabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ = handler.ExtensionManager.DisableExtension(extension)
|
_ = handler.ExtensionManager.DisableExtension(extension)
|
||||||
|
|
||||||
err = handler.ExtensionManager.InstallExtension(extension, payload.License, payload.ArchiveFileName, payload.ExtensionArchive)
|
err = handler.ExtensionManager.InstallExtension(extension, payload.License, payload.ArchiveFileName, payload.ExtensionArchive)
|
||||||
@@ -57,15 +68,15 @@ func (handler *Handler) extensionUpload(w http.ResponseWriter, r *http.Request)
|
|||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to install extension", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to install extension", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension.Enabled = true
|
if extension.ID == portainer.RBACExtension && !extension.Enabled {
|
||||||
|
|
||||||
if extension.ID == portainer.RBACExtension {
|
|
||||||
err = handler.upgradeRBACData()
|
err = handler.upgradeRBACData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension.Enabled = true
|
||||||
|
|
||||||
err = handler.ExtensionService.Persist(extension)
|
err = handler.ExtensionService.Persist(extension)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
||||||
|
|||||||
@@ -6,19 +6,23 @@ import (
|
|||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type publicSettingsResponse struct {
|
type publicSettingsResponse struct {
|
||||||
LogoURL string `json:"LogoURL"`
|
LogoURL string `json:"LogoURL"`
|
||||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||||
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
|
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
|
||||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||||
ExternalTemplates bool `json:"ExternalTemplates"`
|
ExternalTemplates bool `json:"ExternalTemplates"`
|
||||||
OAuthLoginURI string `json:"OAuthLoginURI"`
|
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
|
// GET request on /api/settings/public
|
||||||
@@ -29,19 +33,23 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
|||||||
}
|
}
|
||||||
|
|
||||||
publicSettings := &publicSettingsResponse{
|
publicSettings := &publicSettingsResponse{
|
||||||
LogoURL: settings.LogoURL,
|
LogoURL: settings.LogoURL,
|
||||||
AuthenticationMethod: settings.AuthenticationMethod,
|
AuthenticationMethod: settings.AuthenticationMethod,
|
||||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||||
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
|
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
|
||||||
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
|
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
|
||||||
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||||
ExternalTemplates: false,
|
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",
|
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
|
||||||
settings.OAuthSettings.AuthorizationURI,
|
settings.OAuthSettings.AuthorizationURI,
|
||||||
settings.OAuthSettings.ClientID,
|
settings.OAuthSettings.ClientID,
|
||||||
settings.OAuthSettings.RedirectURI,
|
settings.OAuthSettings.RedirectURI,
|
||||||
settings.OAuthSettings.Scopes),
|
settings.OAuthSettings.Scopes),
|
||||||
|
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.TemplatesURL != "" {
|
if settings.TemplatesURL != "" {
|
||||||
|
|||||||
@@ -7,24 +7,28 @@ import (
|
|||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
)
|
)
|
||||||
|
|
||||||
type settingsUpdatePayload struct {
|
type settingsUpdatePayload struct {
|
||||||
LogoURL *string
|
LogoURL *string
|
||||||
BlackListedLabels []portainer.Pair
|
BlackListedLabels []portainer.Pair
|
||||||
AuthenticationMethod *int
|
AuthenticationMethod *int
|
||||||
LDAPSettings *portainer.LDAPSettings
|
LDAPSettings *portainer.LDAPSettings
|
||||||
OAuthSettings *portainer.OAuthSettings
|
OAuthSettings *portainer.OAuthSettings
|
||||||
AllowBindMountsForRegularUsers *bool
|
AllowBindMountsForRegularUsers *bool
|
||||||
AllowPrivilegedModeForRegularUsers *bool
|
AllowPrivilegedModeForRegularUsers *bool
|
||||||
AllowVolumeBrowserForRegularUsers *bool
|
AllowVolumeBrowserForRegularUsers *bool
|
||||||
EnableHostManagementFeatures *bool
|
EnableHostManagementFeatures *bool
|
||||||
SnapshotInterval *string
|
SnapshotInterval *string
|
||||||
TemplatesURL *string
|
TemplatesURL *string
|
||||||
EdgeAgentCheckinInterval *int
|
EdgeAgentCheckinInterval *int
|
||||||
EnableEdgeComputeFeatures *bool
|
EnableEdgeComputeFeatures *bool
|
||||||
|
AllowStackManagementForRegularUsers *bool
|
||||||
|
AllowHostNamespaceForRegularUsers *bool
|
||||||
|
AllowDeviceMappingForRegularUsers *bool
|
||||||
|
AllowContainerCapabilitiesForRegularUsers *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||||
@@ -114,6 +118,18 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
|||||||
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
|
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.AllowStackManagementForRegularUsers != nil {
|
||||||
|
settings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.AllowHostNamespaceForRegularUsers != nil {
|
||||||
|
settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.AllowContainerCapabilitiesForRegularUsers != nil {
|
||||||
|
settings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers
|
||||||
|
}
|
||||||
|
|
||||||
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
|
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
|
||||||
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
|
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -125,6 +141,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
|||||||
settings.EdgeAgentCheckinInterval = *payload.EdgeAgentCheckinInterval
|
settings.EdgeAgentCheckinInterval = *payload.EdgeAgentCheckinInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.AllowDeviceMappingForRegularUsers != nil {
|
||||||
|
settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
|
||||||
|
}
|
||||||
|
|
||||||
tlsError := handler.updateTLS(settings)
|
tlsError := handler.updateTLS(settings)
|
||||||
if tlsError != nil {
|
if tlsError != nil {
|
||||||
return tlsError
|
return tlsError
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -11,7 +10,7 @@ import (
|
|||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
@@ -283,6 +282,7 @@ type composeStackDeploymentConfig struct {
|
|||||||
dockerhub *portainer.DockerHub
|
dockerhub *portainer.DockerHub
|
||||||
registries []portainer.Registry
|
registries []portainer.Registry
|
||||||
isAdmin bool
|
isAdmin bool
|
||||||
|
user *portainer.User
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
|
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
|
||||||
@@ -302,12 +302,21 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
|
|||||||
}
|
}
|
||||||
filteredRegistries := security.FilterRegistries(registries, securityContext)
|
filteredRegistries := security.FilterRegistries(registries, securityContext)
|
||||||
|
|
||||||
|
var user *portainer.User
|
||||||
|
if !handler.authDisabled {
|
||||||
|
user, err = handler.UserService.User(securityContext.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config := &composeStackDeploymentConfig{
|
config := &composeStackDeploymentConfig{
|
||||||
stack: stack,
|
stack: stack,
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
dockerhub: dockerhub,
|
dockerhub: dockerhub,
|
||||||
registries: filteredRegistries,
|
registries: filteredRegistries,
|
||||||
isAdmin: securityContext.IsAdmin,
|
isAdmin: securityContext.IsAdmin,
|
||||||
|
user: user,
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
@@ -324,20 +333,29 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
|
if !handler.authDisabled {
|
||||||
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
|
||||||
|
|
||||||
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
valid, err := handler.isValidStackFile(stackContent)
|
if (!settings.AllowBindMountsForRegularUsers ||
|
||||||
if err != nil {
|
!settings.AllowPrivilegedModeForRegularUsers ||
|
||||||
return err
|
!settings.AllowHostNamespaceForRegularUsers ||
|
||||||
}
|
!settings.AllowDeviceMappingForRegularUsers ||
|
||||||
if !valid {
|
!settings.AllowContainerCapabilitiesForRegularUsers) && !isAdminOrEndpointAdmin {
|
||||||
return errors.New("bind-mount disabled for non administrator users")
|
|
||||||
|
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
||||||
|
|
||||||
|
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.isValidStackFile(stackContent, settings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -10,7 +9,7 @@ import (
|
|||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
@@ -292,6 +291,7 @@ type swarmStackDeploymentConfig struct {
|
|||||||
registries []portainer.Registry
|
registries []portainer.Registry
|
||||||
prune bool
|
prune bool
|
||||||
isAdmin bool
|
isAdmin bool
|
||||||
|
user *portainer.User
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
|
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
|
||||||
@@ -311,6 +311,14 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
|
|||||||
}
|
}
|
||||||
filteredRegistries := security.FilterRegistries(registries, securityContext)
|
filteredRegistries := security.FilterRegistries(registries, securityContext)
|
||||||
|
|
||||||
|
var user *portainer.User
|
||||||
|
if !handler.authDisabled {
|
||||||
|
user, err = handler.UserService.User(securityContext.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config := &swarmStackDeploymentConfig{
|
config := &swarmStackDeploymentConfig{
|
||||||
stack: stack,
|
stack: stack,
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
@@ -318,6 +326,7 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
|
|||||||
registries: filteredRegistries,
|
registries: filteredRegistries,
|
||||||
prune: prune,
|
prune: prune,
|
||||||
isAdmin: securityContext.IsAdmin,
|
isAdmin: securityContext.IsAdmin,
|
||||||
|
user: user,
|
||||||
}
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
@@ -329,20 +338,25 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
|
if !handler.authDisabled {
|
||||||
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
|
||||||
|
|
||||||
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
valid, err := handler.isValidStackFile(stackContent)
|
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
|
||||||
if err != nil {
|
|
||||||
return err
|
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
||||||
}
|
|
||||||
if !valid {
|
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
|
||||||
return errors.New("bind-mount disabled for non administrator users")
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.isValidStackFile(stackContent, settings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package stacks
|
package stacks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ type Handler struct {
|
|||||||
stackCreationMutex *sync.Mutex
|
stackCreationMutex *sync.Mutex
|
||||||
stackDeletionMutex *sync.Mutex
|
stackDeletionMutex *sync.Mutex
|
||||||
requestBouncer *security.RequestBouncer
|
requestBouncer *security.RequestBouncer
|
||||||
|
authDisabled bool
|
||||||
|
|
||||||
*mux.Router
|
*mux.Router
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
GitService portainer.GitService
|
GitService portainer.GitService
|
||||||
@@ -31,9 +34,10 @@ type Handler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage stack operations.
|
// NewHandler creates a handler to manage stack operations.
|
||||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
func NewHandler(bouncer *security.RequestBouncer, authDisabled bool) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
|
authDisabled: authDisabled,
|
||||||
stackCreationMutex: &sync.Mutex{},
|
stackCreationMutex: &sync.Mutex{},
|
||||||
stackDeletionMutex: &sync.Mutex{},
|
stackDeletionMutex: &sync.Mutex{},
|
||||||
requestBouncer: bouncer,
|
requestBouncer: bouncer,
|
||||||
@@ -56,7 +60,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID, resourceControl *portainer.ResourceControl) (bool, error) {
|
func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID, resourceControl *portainer.ResourceControl) (bool, error) {
|
||||||
if securityContext.IsAdmin {
|
if securityContext.IsAdmin || handler.authDisabled {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,3 +91,54 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR
|
|||||||
}
|
}
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID) (bool, error) {
|
||||||
|
if securityContext.IsAdmin || handler.authDisabled {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := handler.ExtensionService.Extension(portainer.RBACExtension)
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
return false, nil
|
||||||
|
} else if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := handler.UserService.User(securityContext.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := user.EndpointAuthorizations[endpointID][portainer.EndpointResourcesAccess]
|
||||||
|
if ok {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) {
|
||||||
|
isAdmin := user.Role == portainer.AdministratorRole
|
||||||
|
|
||||||
|
rbacExtension, err := handler.ExtensionService.Extension(portainer.RBACExtension)
|
||||||
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
|
return false, errors.New("Unable to verify if RBAC extension is loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointResourceAccess := false
|
||||||
|
_, ok := user.EndpointAuthorizations[portainer.EndpointID(endpointID)][portainer.EndpointResourcesAccess]
|
||||||
|
if ok {
|
||||||
|
endpointResourceAccess = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if rbacExtension != nil {
|
||||||
|
if isAdmin || endpointResourceAccess {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if isAdmin {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -43,6 +43,29 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
|||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
settings, err := handler.SettingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.AllowStackManagementForRegularUsers {
|
||||||
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpointID))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack creation", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !canCreate {
|
||||||
|
errMsg := "Stack creation is disabled for non-admin users"
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, errMsg, errors.New(errMsg)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||||
if err == portainer.ErrObjectNotFound {
|
if err == portainer.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
@@ -97,10 +120,10 @@ 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)}
|
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) (bool, error) {
|
func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *portainer.Settings) error {
|
||||||
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
|
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
composeConfigFile := types.ConfigFile{
|
composeConfigFile := types.ConfigFile{
|
||||||
@@ -117,19 +140,37 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error)
|
|||||||
options.SkipInterpolation = true
|
options.SkipInterpolation = true
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for key := range composeConfig.Services {
|
for key := range composeConfig.Services {
|
||||||
service := composeConfig.Services[key]
|
service := composeConfig.Services[key]
|
||||||
for _, volume := range service.Volumes {
|
if !settings.AllowBindMountsForRegularUsers {
|
||||||
if volume.Type == "bind" {
|
for _, volume := range service.Volumes {
|
||||||
return false, nil
|
if volume.Type == "bind" {
|
||||||
|
return errors.New("bind-mount disabled for non administrator users")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
|
||||||
|
return errors.New("privileged mode disabled for non administrator users")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
|
||||||
|
return errors.New("container capabilities disabled for non administrator users")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {
|
func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h
|
|||||||
ExtensionService: factory.extensionService,
|
ExtensionService: factory.extensionService,
|
||||||
SignatureService: factory.signatureService,
|
SignatureService: factory.signatureService,
|
||||||
DockerClientFactory: factory.dockerClientFactory,
|
DockerClientFactory: factory.dockerClientFactory,
|
||||||
|
AuthDisabled: factory.authDisabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
dockerTransport, err := docker.NewTransport(transportParameters, httpTransport)
|
dockerTransport, err := docker.NewTransport(transportParameters, httpTransport)
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
portainer "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/proxy/factory/responseutils"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -147,3 +152,88 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
Devices []interface{} `json:"Devices"`
|
||||||
|
CapAdd []string `json:"CapAdd"`
|
||||||
|
CapDrop []string `json:"CapDrop"`
|
||||||
|
Binds []string `json:"Binds"`
|
||||||
|
} `json:"HostConfig"`
|
||||||
|
}
|
||||||
|
|
||||||
|
forbiddenResponse := &http.Response{
|
||||||
|
StatusCode: http.StatusForbidden,
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenData, err := security.RetrieveTokenData(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAdminOrEndpointAdmin {
|
||||||
|
settings, err := transport.settingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.AllowPrivilegedModeForRegularUsers ||
|
||||||
|
!settings.AllowHostNamespaceForRegularUsers ||
|
||||||
|
!settings.AllowDeviceMappingForRegularUsers ||
|
||||||
|
!settings.AllowContainerCapabilitiesForRegularUsers ||
|
||||||
|
!settings.AllowBindMountsForRegularUsers {
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
partialContainer := &PartialContainer{}
|
||||||
|
err = json.Unmarshal(body, partialContainer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
|
||||||
|
return forbiddenResponse, errors.New("forbidden to use privileged mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
|
||||||
|
return forbiddenResponse, errors.New("forbidden to use pid host namespace")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
|
||||||
|
return forbiddenResponse, errors.New("forbidden to use bind mounts")
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := transport.executeDockerRequest(request)
|
||||||
|
if err != nil {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode == http.StatusCreated {
|
||||||
|
err = transport.decorateGenericResourceCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
@@ -84,3 +88,54 @@ func selectorServiceLabels(responseObject map[string]interface{}) map[string]int
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) decorateServiceCreationOperation(request *http.Request) (*http.Response, error) {
|
||||||
|
type PartialService struct {
|
||||||
|
TaskTemplate struct {
|
||||||
|
ContainerSpec struct {
|
||||||
|
Mounts []struct {
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forbiddenResponse := &http.Response{
|
||||||
|
StatusCode: http.StatusForbidden,
|
||||||
|
}
|
||||||
|
|
||||||
|
isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAdminOrEndpointAdmin {
|
||||||
|
settings, err := transport.settingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
partialService := &PartialService{}
|
||||||
|
err = json.Unmarshal(body, partialService)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !settings.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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.replaceRegistryAuthenticationHeader(request)
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type (
|
|||||||
extensionService portainer.ExtensionService
|
extensionService portainer.ExtensionService
|
||||||
dockerClient *client.Client
|
dockerClient *client.Client
|
||||||
dockerClientFactory *docker.ClientFactory
|
dockerClientFactory *docker.ClientFactory
|
||||||
|
authDisabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// TransportParameters is used to create a new Transport
|
// TransportParameters is used to create a new Transport
|
||||||
@@ -54,6 +55,7 @@ type (
|
|||||||
ReverseTunnelService portainer.ReverseTunnelService
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
ExtensionService portainer.ExtensionService
|
ExtensionService portainer.ExtensionService
|
||||||
DockerClientFactory *docker.ClientFactory
|
DockerClientFactory *docker.ClientFactory
|
||||||
|
AuthDisabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
restrictedDockerOperationContext struct {
|
restrictedDockerOperationContext struct {
|
||||||
@@ -94,6 +96,7 @@ func NewTransport(parameters *TransportParameters, httpTransport *http.Transport
|
|||||||
dockerClientFactory: parameters.DockerClientFactory,
|
dockerClientFactory: parameters.DockerClientFactory,
|
||||||
HTTPTransport: httpTransport,
|
HTTPTransport: httpTransport,
|
||||||
dockerClient: dockerClient,
|
dockerClient: dockerClient,
|
||||||
|
authDisabled: parameters.AuthDisabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
return transport, nil
|
return transport, nil
|
||||||
@@ -209,7 +212,7 @@ func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Res
|
|||||||
func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
|
func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
|
||||||
switch requestPath := request.URL.Path; requestPath {
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
case "/containers/create":
|
case "/containers/create":
|
||||||
return transport.decorateGenericResourceCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
|
return transport.decorateContainerCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
|
||||||
|
|
||||||
case "/containers/prune":
|
case "/containers/prune":
|
||||||
return transport.administratorOperation(request)
|
return transport.administratorOperation(request)
|
||||||
@@ -245,7 +248,7 @@ func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.
|
|||||||
func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
|
func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
|
||||||
switch requestPath := request.URL.Path; requestPath {
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
case "/services/create":
|
case "/services/create":
|
||||||
return transport.replaceRegistryAuthenticationHeader(request)
|
return transport.decorateServiceCreationOperation(request)
|
||||||
|
|
||||||
case "/services":
|
case "/services":
|
||||||
return transport.rewriteOperation(request, transport.serviceListOperation)
|
return transport.rewriteOperation(request, transport.serviceListOperation)
|
||||||
@@ -733,3 +736,36 @@ func (transport *Transport) createOperationContext(request *http.Request) (*rest
|
|||||||
|
|
||||||
return operationContext, nil
|
return operationContext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool, error) {
|
||||||
|
if transport.authDisabled {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenData, err := security.RetrieveTokenData(request)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.Role == portainer.AdministratorRole {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := transport.userService.User(tokenData.ID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rbacExtension, err := transport.extensionService.Extension(portainer.RBACExtension)
|
||||||
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rbacExtension == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, endpointResourceAccess := user.EndpointAuthorizations[portainer.EndpointID(transport.endpoint.ID)][portainer.EndpointResourcesAccess]
|
||||||
|
|
||||||
|
return endpointResourceAccess, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portaine
|
|||||||
ExtensionService: factory.extensionService,
|
ExtensionService: factory.extensionService,
|
||||||
SignatureService: factory.signatureService,
|
SignatureService: factory.signatureService,
|
||||||
DockerClientFactory: factory.dockerClientFactory,
|
DockerClientFactory: factory.dockerClientFactory,
|
||||||
|
AuthDisabled: factory.authDisabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy := &dockerLocalProxy{}
|
proxy := &dockerLocalProxy{}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portaine
|
|||||||
ExtensionService: factory.extensionService,
|
ExtensionService: factory.extensionService,
|
||||||
SignatureService: factory.signatureService,
|
SignatureService: factory.signatureService,
|
||||||
DockerClientFactory: factory.dockerClientFactory,
|
DockerClientFactory: factory.dockerClientFactory,
|
||||||
|
AuthDisabled: factory.authDisabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy := &dockerLocalProxy{}
|
proxy := &dockerLocalProxy{}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/docker"
|
"github.com/portainer/portainer/api/docker"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,6 +32,7 @@ type (
|
|||||||
reverseTunnelService portainer.ReverseTunnelService
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
extensionService portainer.ExtensionService
|
extensionService portainer.ExtensionService
|
||||||
dockerClientFactory *docker.ClientFactory
|
dockerClientFactory *docker.ClientFactory
|
||||||
|
authDisabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyFactoryParameters is used to create a new ProxyFactory
|
// ProxyFactoryParameters is used to create a new ProxyFactory
|
||||||
@@ -47,6 +48,7 @@ type (
|
|||||||
ReverseTunnelService portainer.ReverseTunnelService
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
ExtensionService portainer.ExtensionService
|
ExtensionService portainer.ExtensionService
|
||||||
DockerClientFactory *docker.ClientFactory
|
DockerClientFactory *docker.ClientFactory
|
||||||
|
AuthDisabled bool
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,6 +66,7 @@ func NewProxyFactory(parameters *ProxyFactoryParameters) *ProxyFactory {
|
|||||||
reverseTunnelService: parameters.ReverseTunnelService,
|
reverseTunnelService: parameters.ReverseTunnelService,
|
||||||
extensionService: parameters.ExtensionService,
|
extensionService: parameters.ExtensionService,
|
||||||
dockerClientFactory: parameters.DockerClientFactory,
|
dockerClientFactory: parameters.DockerClientFactory,
|
||||||
|
authDisabled: parameters.AuthDisabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/orcaman/concurrent-map"
|
cmap "github.com/orcaman/concurrent-map"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/docker"
|
"github.com/portainer/portainer/api/docker"
|
||||||
"github.com/portainer/portainer/api/http/proxy/factory"
|
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||||
)
|
)
|
||||||
@@ -34,6 +34,7 @@ type (
|
|||||||
ReverseTunnelService portainer.ReverseTunnelService
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
ExtensionService portainer.ExtensionService
|
ExtensionService portainer.ExtensionService
|
||||||
DockerClientFactory *docker.ClientFactory
|
DockerClientFactory *docker.ClientFactory
|
||||||
|
AuthDisabled bool
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ func NewManager(parameters *ManagerParams) *Manager {
|
|||||||
ReverseTunnelService: parameters.ReverseTunnelService,
|
ReverseTunnelService: parameters.ReverseTunnelService,
|
||||||
ExtensionService: parameters.ExtensionService,
|
ExtensionService: parameters.ExtensionService,
|
||||||
DockerClientFactory: parameters.DockerClientFactory,
|
DockerClientFactory: parameters.DockerClientFactory,
|
||||||
|
AuthDisabled: parameters.AuthDisabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Manager{
|
return &Manager{
|
||||||
|
|||||||
+12
-4
@@ -3,6 +3,7 @@ package http
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/crypto"
|
||||||
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
"github.com/portainer/portainer/api/http/handler/edgegroups"
|
||||||
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
"github.com/portainer/portainer/api/http/handler/edgestacks"
|
||||||
"github.com/portainer/portainer/api/http/handler/edgetemplates"
|
"github.com/portainer/portainer/api/http/handler/edgetemplates"
|
||||||
@@ -103,6 +104,7 @@ func (server *Server) Start() error {
|
|||||||
ReverseTunnelService: server.ReverseTunnelService,
|
ReverseTunnelService: server.ReverseTunnelService,
|
||||||
ExtensionService: server.ExtensionService,
|
ExtensionService: server.ExtensionService,
|
||||||
DockerClientFactory: server.DockerClientFactory,
|
DockerClientFactory: server.DockerClientFactory,
|
||||||
|
AuthDisabled: server.AuthDisabled,
|
||||||
}
|
}
|
||||||
proxyManager := proxy.NewManager(proxyManagerParameters)
|
proxyManager := proxy.NewManager(proxyManagerParameters)
|
||||||
|
|
||||||
@@ -246,7 +248,7 @@ func (server *Server) Start() error {
|
|||||||
settingsHandler.ExtensionService = server.ExtensionService
|
settingsHandler.ExtensionService = server.ExtensionService
|
||||||
settingsHandler.AuthorizationService = authorizationService
|
settingsHandler.AuthorizationService = authorizationService
|
||||||
|
|
||||||
var stackHandler = stacks.NewHandler(requestBouncer)
|
var stackHandler = stacks.NewHandler(requestBouncer, server.AuthDisabled)
|
||||||
stackHandler.FileService = server.FileService
|
stackHandler.FileService = server.FileService
|
||||||
stackHandler.StackService = server.StackService
|
stackHandler.StackService = server.StackService
|
||||||
stackHandler.EndpointService = server.EndpointService
|
stackHandler.EndpointService = server.EndpointService
|
||||||
@@ -338,8 +340,14 @@ func (server *Server) Start() error {
|
|||||||
SchedulesHanlder: schedulesHandler,
|
SchedulesHanlder: schedulesHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
if server.SSL {
|
httpServer := &http.Server{
|
||||||
return http.ListenAndServeTLS(server.BindAddress, server.SSLCert, server.SSLKey, server.Handler)
|
Addr: server.BindAddress,
|
||||||
|
Handler: server.Handler,
|
||||||
}
|
}
|
||||||
return http.ListenAndServe(server.BindAddress, server.Handler)
|
|
||||||
|
if server.SSL {
|
||||||
|
httpServer.TLSConfig = crypto.CreateServerTLSConfiguration()
|
||||||
|
return httpServer.ListenAndServeTLS(server.SSLCert, server.SSLKey)
|
||||||
|
}
|
||||||
|
return httpServer.ListenAndServe()
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-15
@@ -420,19 +420,23 @@ type (
|
|||||||
|
|
||||||
// Settings represents the application settings
|
// Settings represents the application settings
|
||||||
Settings struct {
|
Settings struct {
|
||||||
LogoURL string `json:"LogoURL"`
|
LogoURL string `json:"LogoURL"`
|
||||||
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
||||||
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
||||||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||||
OAuthSettings OAuthSettings `json:"OAuthSettings"`
|
OAuthSettings OAuthSettings `json:"OAuthSettings"`
|
||||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||||
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
|
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
|
||||||
SnapshotInterval string `json:"SnapshotInterval"`
|
SnapshotInterval string `json:"SnapshotInterval"`
|
||||||
TemplatesURL string `json:"TemplatesURL"`
|
TemplatesURL string `json:"TemplatesURL"`
|
||||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||||
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
|
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
|
||||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||||
|
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
|
||||||
|
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
|
||||||
|
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
|
||||||
|
AllowContainerCapabilitiesForRegularUsers bool `json:"AllowContainerCapabilitiesForRegularUsers"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
DisplayDonationHeader bool
|
DisplayDonationHeader bool
|
||||||
@@ -1007,9 +1011,9 @@ type (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "1.24.0"
|
APIVersion = "1.24.2"
|
||||||
// DBVersion is the version number of the Portainer database
|
// DBVersion is the version number of the Portainer database
|
||||||
DBVersion = 23
|
DBVersion = 24
|
||||||
// AssetsServerURL represents the URL of the Portainer asset server
|
// AssetsServerURL represents the URL of the Portainer asset server
|
||||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||||
|
|||||||
+2
-2
@@ -54,7 +54,7 @@ info:
|
|||||||
|
|
||||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||||
|
|
||||||
version: "1.24.0"
|
version: "1.24.1"
|
||||||
title: "Portainer API"
|
title: "Portainer API"
|
||||||
contact:
|
contact:
|
||||||
email: "info@portainer.io"
|
email: "info@portainer.io"
|
||||||
@@ -3174,7 +3174,7 @@ definitions:
|
|||||||
description: "Is analytics enabled"
|
description: "Is analytics enabled"
|
||||||
Version:
|
Version:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "1.24.0"
|
example: "1.24.1"
|
||||||
description: "Portainer API version"
|
description: "Portainer API version"
|
||||||
PublicSettingsInspectResponse:
|
PublicSettingsInspectResponse:
|
||||||
type: "object"
|
type: "object"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"packageName": "portainer",
|
"packageName": "portainer",
|
||||||
"packageVersion": "1.24.0",
|
"packageVersion": "1.24.1",
|
||||||
"projectName": "portainer"
|
"projectName": "portainer"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,11 +120,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="menuContent">
|
<div class="menuContent">
|
||||||
<div class="md-checkbox">
|
<div class="md-checkbox">
|
||||||
<input id="filter_usage_usedImages" type="checkbox" ng-model="$ctrl.filters.state.showUsedImages" ng-change="$ctrl.onUsageFilterChange()" />
|
<input id="filter_usage_usedImages" type="checkbox" ng-model="$ctrl.filters.state.showUsedImages" ng-change="$ctrl.onstateFilterChange()" />
|
||||||
<label for="filter_usage_usedImages">Used images</label>
|
<label for="filter_usage_usedImages">Used images</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="md-checkbox">
|
<div class="md-checkbox">
|
||||||
<input id="filter_usage_unusedImages" type="checkbox" ng-model="$ctrl.filters.state.showUnusedImages" ng-change="$ctrl.onUsageFilterChange()" />
|
<input id="filter_usage_unusedImages" type="checkbox" ng-model="$ctrl.filters.state.showUnusedImages" ng-change="$ctrl.onstateFilterChange()" />
|
||||||
<label for="filter_usage_unusedImages">Unused images</label>
|
<label for="filter_usage_unusedImages">Unused images</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,11 +93,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="menuContent">
|
<div class="menuContent">
|
||||||
<div class="md-checkbox">
|
<div class="md-checkbox">
|
||||||
<input id="filter_usage_usedImages" type="checkbox" ng-model="$ctrl.filters.state.showUsedVolumes" ng-change="$ctrl.onUsageFilterChange()" />
|
<input id="filter_usage_usedImages" type="checkbox" ng-model="$ctrl.filters.state.showUsedVolumes" ng-change="$ctrl.onstateFilterChange()" />
|
||||||
<label for="filter_usage_usedImages">Used volumes</label>
|
<label for="filter_usage_usedImages">Used volumes</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="md-checkbox">
|
<div class="md-checkbox">
|
||||||
<input id="filter_usage_unusedImages" type="checkbox" ng-model="$ctrl.filters.state.showUnusedVolumes" ng-change="$ctrl.onUsageFilterChange()" />
|
<input id="filter_usage_unusedImages" type="checkbox" ng-model="$ctrl.filters.state.showUnusedVolumes" ng-change="$ctrl.onstateFilterChange()" />
|
||||||
<label for="filter_usage_unusedImages">Unused volumes</label>
|
<label for="filter_usage_unusedImages">Unused volumes</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ angular.module('portainer.docker').component('dockerSidebarContent', {
|
|||||||
standaloneManagement: '<',
|
standaloneManagement: '<',
|
||||||
adminAccess: '<',
|
adminAccess: '<',
|
||||||
offlineMode: '<',
|
offlineMode: '<',
|
||||||
|
showStacks: '<',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<li class="sidebar-list" ng-if="!$ctrl.offlineMode" authorization="DockerContainerCreate, PortainerStackCreate">
|
<li class="sidebar-list" ng-if="!$ctrl.offlineMode" authorization="DockerContainerCreate, PortainerStackCreate">
|
||||||
<a ui-sref="portainer.templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
|
<a ui-sref="portainer.templates" ui-sref-active="active">App Templates <span class="menu-icon fa fa-rocket fa-fw"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list">
|
<li class="sidebar-list" ng-if="$ctrl.showStacks">
|
||||||
<a ui-sref="portainer.stacks" ui-sref-active="active">Stacks <span class="menu-icon fa fa-th-list fa-fw"></span></a>
|
<a ui-sref="portainer.stacks" ui-sref-active="active">Stacks <span class="menu-icon fa fa-th-list fa-fw"></span></a>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
|
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||||||
'SettingsService',
|
'SettingsService',
|
||||||
'PluginService',
|
'PluginService',
|
||||||
'HttpRequestHelper',
|
'HttpRequestHelper',
|
||||||
|
'ExtensionService',
|
||||||
function (
|
function (
|
||||||
$q,
|
$q,
|
||||||
$scope,
|
$scope,
|
||||||
@@ -55,7 +56,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||||||
SystemService,
|
SystemService,
|
||||||
SettingsService,
|
SettingsService,
|
||||||
PluginService,
|
PluginService,
|
||||||
HttpRequestHelper
|
HttpRequestHelper,
|
||||||
|
ExtensionService
|
||||||
) {
|
) {
|
||||||
$scope.create = create;
|
$scope.create = create;
|
||||||
|
|
||||||
@@ -603,11 +605,16 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initView() {
|
async function initView() {
|
||||||
var nodeName = $transition$.params().nodeName;
|
var nodeName = $transition$.params().nodeName;
|
||||||
$scope.formValues.NodeName = nodeName;
|
$scope.formValues.NodeName = nodeName;
|
||||||
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
|
||||||
|
|
||||||
|
$scope.isAdmin = Authentication.isAdmin();
|
||||||
|
$scope.isAdminOrEndpointAdmin = await checkIfAdminOrEndpointAdmin();
|
||||||
|
$scope.showDeviceMapping = await shouldShowDevices($scope.isAdminOrEndpointAdmin);
|
||||||
|
$scope.areContainerCapabilitiesEnabled = await checkIfContainerCapabilitiesEnabled($scope.isAdminOrEndpointAdmin);
|
||||||
|
|
||||||
Volume.query(
|
Volume.query(
|
||||||
{},
|
{},
|
||||||
function (d) {
|
function (d) {
|
||||||
@@ -643,7 +650,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||||||
loadFromContainerSpec();
|
loadFromContainerSpec();
|
||||||
} else {
|
} else {
|
||||||
$scope.fromContainer = {};
|
$scope.fromContainer = {};
|
||||||
$scope.formValues.capabilities = new ContainerCapabilities();
|
$scope.formValues.capabilities = $scope.areContainerCapabilitiesEnabled ? new ContainerCapabilities() : [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function (e) {
|
function (e) {
|
||||||
@@ -670,7 +677,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||||||
|
|
||||||
SettingsService.publicSettings()
|
SettingsService.publicSettings()
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.allowBindMounts = data.AllowBindMountsForRegularUsers;
|
$scope.allowBindMounts = $scope.isAdminOrEndpointAdmin || data.AllowBindMountsForRegularUsers;
|
||||||
$scope.allowPrivilegedMode = data.AllowPrivilegedModeForRegularUsers;
|
$scope.allowPrivilegedMode = data.AllowPrivilegedModeForRegularUsers;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
@@ -680,8 +687,6 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||||||
PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) {
|
PluginService.loggingPlugins(apiVersion < 1.25).then(function success(loggingDrivers) {
|
||||||
$scope.availableLoggingDrivers = loggingDrivers;
|
$scope.availableLoggingDrivers = loggingDrivers;
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.isAdmin = Authentication.isAdmin();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateForm(accessControlData, isAdmin) {
|
function validateForm(accessControlData, isAdmin) {
|
||||||
@@ -894,6 +899,27 @@ angular.module('portainer.docker').controller('CreateContainerController', [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function shouldShowDevices(isAdminOrEndpointAdmin) {
|
||||||
|
const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application;
|
||||||
|
|
||||||
|
return allowDeviceMappingForRegularUsers || isAdminOrEndpointAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkIfContainerCapabilitiesEnabled(isAdminOrEndpointAdmin) {
|
||||||
|
const { allowContainerCapabilitiesForRegularUsers } = $scope.applicationState.application;
|
||||||
|
|
||||||
|
return allowContainerCapabilitiesForRegularUsers || isAdminOrEndpointAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkIfAdminOrEndpointAdmin() {
|
||||||
|
if (Authentication.isAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
||||||
|
return rbacEnabled ? Authentication.hasAuthorizations(['EndpointResourcesAccess']) : false;
|
||||||
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -185,7 +185,7 @@
|
|||||||
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
|
<li class="interactive"><a data-target="#labels" data-toggle="tab">Labels</a></li>
|
||||||
<li class="interactive"><a data-target="#restart-policy" data-toggle="tab">Restart policy</a></li>
|
<li class="interactive"><a data-target="#restart-policy" data-toggle="tab">Restart policy</a></li>
|
||||||
<li class="interactive"><a data-target="#runtime-resources" ng-click="refreshSlider()" data-toggle="tab">Runtime & Resources</a></li>
|
<li class="interactive"><a data-target="#runtime-resources" ng-click="refreshSlider()" data-toggle="tab">Runtime & Resources</a></li>
|
||||||
<li class="interactive"><a data-target="#container-capabilities" data-toggle="tab">Capabilities</a></li>
|
<li ng-if="areContainerCapabilitiesEnabled" class="interactive"><a data-target="#container-capabilities" data-toggle="tab">Capabilities</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<!-- tab-content -->
|
<!-- tab-content -->
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
@@ -338,8 +338,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- !container-path -->
|
<!-- !container-path -->
|
||||||
<!-- volume-type -->
|
<!-- volume-type -->
|
||||||
<div class="input-group col-sm-5" style="margin-left: 5px;" ng-if="isAdmin || allowBindMounts">
|
<div class="input-group col-sm-5" style="margin-left: 5px;">
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm" ng-if="allowBindMounts">
|
||||||
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
|
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
|
||||||
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.name = ''">Bind</label>
|
<label class="btn btn-primary" ng-model="volume.type" uib-btn-radio="'bind'" ng-click="volume.name = ''">Bind</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -629,7 +629,7 @@
|
|||||||
</form>
|
</form>
|
||||||
<form class="form-horizontal" style="margin-top: 15px;">
|
<form class="form-horizontal" style="margin-top: 15px;">
|
||||||
<!-- devices -->
|
<!-- devices -->
|
||||||
<div class="form-group">
|
<div ng-if="showDeviceMapping" class="form-group">
|
||||||
<div class="col-sm-12" style="margin-top: 5px;">
|
<div class="col-sm-12" style="margin-top: 5px;">
|
||||||
<label class="control-label text-left">Devices</label>
|
<label class="control-label text-left">Devices</label>
|
||||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addDevice()">
|
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addDevice()">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ angular.module('portainer.docker').controller('ContainerController', [
|
|||||||
'ImageService',
|
'ImageService',
|
||||||
'HttpRequestHelper',
|
'HttpRequestHelper',
|
||||||
'Authentication',
|
'Authentication',
|
||||||
|
'StateManager',
|
||||||
function (
|
function (
|
||||||
$q,
|
$q,
|
||||||
$scope,
|
$scope,
|
||||||
@@ -40,7 +41,8 @@ angular.module('portainer.docker').controller('ContainerController', [
|
|||||||
RegistryService,
|
RegistryService,
|
||||||
ImageService,
|
ImageService,
|
||||||
HttpRequestHelper,
|
HttpRequestHelper,
|
||||||
Authentication
|
Authentication,
|
||||||
|
StateManager
|
||||||
) {
|
) {
|
||||||
$scope.activityTime = 0;
|
$scope.activityTime = 0;
|
||||||
$scope.portBindings = [];
|
$scope.portBindings = [];
|
||||||
@@ -94,9 +96,13 @@ angular.module('portainer.docker').controller('ContainerController', [
|
|||||||
const inSwarm = $scope.container.Config.Labels['com.docker.swarm.service.id'];
|
const inSwarm = $scope.container.Config.Labels['com.docker.swarm.service.id'];
|
||||||
const autoRemove = $scope.container.HostConfig.AutoRemove;
|
const autoRemove = $scope.container.HostConfig.AutoRemove;
|
||||||
const admin = Authentication.isAdmin();
|
const admin = Authentication.isAdmin();
|
||||||
|
const appState = StateManager.getState();
|
||||||
|
const { allowHostNamespaceForRegularUsers, allowDeviceMappingForRegularUsers, allowBindMountsForRegularUsers, allowPrivilegedModeForRegularUsers } = appState.application;
|
||||||
|
const settingRestrictsRegularUsers =
|
||||||
|
!allowBindMountsForRegularUsers || !allowDeviceMappingForRegularUsers || !allowHostNamespaceForRegularUsers || !allowPrivilegedModeForRegularUsers;
|
||||||
|
|
||||||
ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC).then((rbacEnabled) => {
|
ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC).then((rbacEnabled) => {
|
||||||
$scope.displayRecreateButton = !inSwarm && !autoRemove && (rbacEnabled ? admin : true);
|
$scope.displayRecreateButton = !inSwarm && !autoRemove && (settingRestrictsRegularUsers || rbacEnabled ? admin : true);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-6">
|
<div class="col-xs-12 col-md-6" ng-if="showStacks">
|
||||||
<a ui-sref="portainer.stacks">
|
<a ui-sref="portainer.stacks">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
angular.module('portainer.docker').controller('DashboardController', [
|
angular.module('portainer.docker').controller('DashboardController', [
|
||||||
'$scope',
|
'$scope',
|
||||||
'$q',
|
'$q',
|
||||||
|
'Authentication',
|
||||||
'ContainerService',
|
'ContainerService',
|
||||||
'ImageService',
|
'ImageService',
|
||||||
'NetworkService',
|
'NetworkService',
|
||||||
@@ -11,10 +12,12 @@ angular.module('portainer.docker').controller('DashboardController', [
|
|||||||
'EndpointService',
|
'EndpointService',
|
||||||
'Notifications',
|
'Notifications',
|
||||||
'EndpointProvider',
|
'EndpointProvider',
|
||||||
|
'ExtensionService',
|
||||||
'StateManager',
|
'StateManager',
|
||||||
function (
|
function (
|
||||||
$scope,
|
$scope,
|
||||||
$q,
|
$q,
|
||||||
|
Authentication,
|
||||||
ContainerService,
|
ContainerService,
|
||||||
ImageService,
|
ImageService,
|
||||||
NetworkService,
|
NetworkService,
|
||||||
@@ -25,6 +28,7 @@ angular.module('portainer.docker').controller('DashboardController', [
|
|||||||
EndpointService,
|
EndpointService,
|
||||||
Notifications,
|
Notifications,
|
||||||
EndpointProvider,
|
EndpointProvider,
|
||||||
|
ExtensionService,
|
||||||
StateManager
|
StateManager
|
||||||
) {
|
) {
|
||||||
$scope.dismissInformationPanel = function (id) {
|
$scope.dismissInformationPanel = function (id) {
|
||||||
@@ -32,11 +36,14 @@ angular.module('portainer.docker').controller('DashboardController', [
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.offlineMode = false;
|
$scope.offlineMode = false;
|
||||||
|
$scope.showStacks = false;
|
||||||
|
|
||||||
function initView() {
|
async function initView() {
|
||||||
var endpointMode = $scope.applicationState.endpoint.mode;
|
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||||
var endpointId = EndpointProvider.endpointID();
|
var endpointId = EndpointProvider.endpointID();
|
||||||
|
|
||||||
|
$scope.showStacks = await shouldShowStacks();
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
containers: ContainerService.containers(1),
|
containers: ContainerService.containers(1),
|
||||||
images: ImageService.images(false),
|
images: ImageService.images(false),
|
||||||
@@ -63,6 +70,19 @@ angular.module('portainer.docker').controller('DashboardController', [
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function shouldShowStacks() {
|
||||||
|
const isAdmin = !$scope.applicationState.application.authentication || Authentication.isAdmin();
|
||||||
|
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
|
||||||
|
|
||||||
|
if (isAdmin || allowStackManagementForRegularUsers) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
||||||
|
if (rbacEnabled) {
|
||||||
|
return Authentication.hasAuthorizations(['EndpointResourcesAccess']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
|||||||
'SettingsService',
|
'SettingsService',
|
||||||
'WebhookService',
|
'WebhookService',
|
||||||
'EndpointProvider',
|
'EndpointProvider',
|
||||||
|
'ExtensionService',
|
||||||
function (
|
function (
|
||||||
$q,
|
$q,
|
||||||
$scope,
|
$scope,
|
||||||
@@ -58,7 +59,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
|||||||
NodeService,
|
NodeService,
|
||||||
SettingsService,
|
SettingsService,
|
||||||
WebhookService,
|
WebhookService,
|
||||||
EndpointProvider
|
EndpointProvider,
|
||||||
|
ExtensionService
|
||||||
) {
|
) {
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
Name: '',
|
Name: '',
|
||||||
@@ -106,6 +108,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
|||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.allowBindMounts = false;
|
||||||
|
|
||||||
$scope.refreshSlider = function () {
|
$scope.refreshSlider = function () {
|
||||||
$timeout(function () {
|
$timeout(function () {
|
||||||
$scope.$broadcast('rzSliderForceRender');
|
$scope.$broadcast('rzSliderForceRender');
|
||||||
@@ -562,8 +566,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
|||||||
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
|
secrets: apiVersion >= 1.25 ? SecretService.secrets() : [],
|
||||||
configs: apiVersion >= 1.3 ? ConfigService.configs() : [],
|
configs: apiVersion >= 1.3 ? ConfigService.configs() : [],
|
||||||
nodes: NodeService.nodes(),
|
nodes: NodeService.nodes(),
|
||||||
settings: SettingsService.publicSettings(),
|
|
||||||
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25),
|
availableLoggingDrivers: PluginService.loggingPlugins(apiVersion < 1.25),
|
||||||
|
allowBindMounts: checkIfAllowedBindMounts(),
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.availableVolumes = data.volumes;
|
$scope.availableVolumes = data.volumes;
|
||||||
@@ -572,8 +576,8 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
|||||||
$scope.availableConfigs = data.configs;
|
$scope.availableConfigs = data.configs;
|
||||||
$scope.availableLoggingDrivers = data.availableLoggingDrivers;
|
$scope.availableLoggingDrivers = data.availableLoggingDrivers;
|
||||||
initSlidersMaxValuesBasedOnNodeData(data.nodes);
|
initSlidersMaxValuesBasedOnNodeData(data.nodes);
|
||||||
$scope.allowBindMounts = data.settings.AllowBindMountsForRegularUsers;
|
|
||||||
$scope.isAdmin = Authentication.isAdmin();
|
$scope.isAdmin = Authentication.isAdmin();
|
||||||
|
$scope.allowBindMounts = data.allowBindMounts;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to initialize view');
|
Notifications.error('Failure', err, 'Unable to initialize view');
|
||||||
@@ -581,5 +585,22 @@ angular.module('portainer.docker').controller('CreateServiceController', [
|
|||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
|
|
||||||
|
async function checkIfAllowedBindMounts() {
|
||||||
|
const isAdmin = Authentication.isAdmin();
|
||||||
|
|
||||||
|
const settings = await SettingsService.publicSettings();
|
||||||
|
const { AllowBindMountsForRegularUsers } = settings;
|
||||||
|
|
||||||
|
if (isAdmin || AllowBindMountsForRegularUsers) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
||||||
|
if (rbacEnabled) {
|
||||||
|
return Authentication.hasAuthorizations(['EndpointResourcesAccess']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -305,7 +305,7 @@
|
|||||||
<!-- !container-path -->
|
<!-- !container-path -->
|
||||||
<!-- volume-type -->
|
<!-- volume-type -->
|
||||||
<div class="input-group col-sm-5" style="margin-left: 5px;">
|
<div class="input-group col-sm-5" style="margin-left: 5px;">
|
||||||
<div class="btn-group btn-group-sm" ng-if="isAdmin || allowBindMounts">
|
<div class="btn-group btn-group-sm" ng-if="allowBindMounts">
|
||||||
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
|
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'volume'" ng-click="volume.name = ''">Volume</label>
|
||||||
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Id = ''">Bind</label>
|
<label class="btn btn-primary" ng-model="volume.Type" uib-btn-radio="'bind'" ng-click="volume.Id = ''">Bind</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,14 +73,16 @@ angular.module('portainer.docker').controller('VolumesController', [
|
|||||||
|
|
||||||
$scope.showBrowseAction = $scope.applicationState.endpoint.mode.agentProxy;
|
$scope.showBrowseAction = $scope.applicationState.endpoint.mode.agentProxy;
|
||||||
|
|
||||||
ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC).then(function success(extensionEnabled) {
|
if ($scope.applicationState.application.authentication) {
|
||||||
if (!extensionEnabled) {
|
ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC).then(function success(extensionEnabled) {
|
||||||
var isAdmin = Authentication.isAdmin();
|
if (!extensionEnabled) {
|
||||||
if (!$scope.applicationState.application.enableVolumeBrowserForNonAdminUsers && !isAdmin) {
|
var isAdmin = Authentication.isAdmin();
|
||||||
$scope.showBrowseAction = false;
|
if (!$scope.applicationState.application.enableVolumeBrowserForNonAdminUsers && !isAdmin) {
|
||||||
|
$scope.showBrowseAction = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
|
|||||||
@@ -42,7 +42,8 @@ angular.module('portainer.extensions.registrymanagement').factory('RegistryGitla
|
|||||||
|
|
||||||
async function _getRepositoriesPage(params, repositories) {
|
async function _getRepositoriesPage(params, repositories) {
|
||||||
const response = await Gitlab().repositories(params).$promise;
|
const response = await Gitlab().repositories(params).$promise;
|
||||||
repositories = _.concat(repositories, response.data);
|
const filteredRepositories = _.filter(response.data, (repository) => repository.tags && repository.tags.length > 0);
|
||||||
|
repositories = _.concat(repositories, filteredRepositories);
|
||||||
if (response.next) {
|
if (response.next) {
|
||||||
params.page = response.next;
|
params.page = response.next;
|
||||||
repositories = await _getRepositoriesPage(params, repositories);
|
repositories = await _getRepositoriesPage(params, repositories);
|
||||||
|
|||||||
+2
-2
@@ -54,8 +54,8 @@ angular.module('portainer.extensions.registrymanagement').controller('RegistryRe
|
|||||||
.then(function success() {
|
.then(function success() {
|
||||||
return RegistryServiceSelector.repositories($scope.registry);
|
return RegistryServiceSelector.repositories($scope.registry);
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(repositories) {
|
||||||
$scope.repositories = data;
|
$scope.repositories = repositories;
|
||||||
})
|
})
|
||||||
.catch(function error() {
|
.catch(function error() {
|
||||||
$scope.state.displayInvalidConfigurationMessage = true;
|
$scope.state.displayInvalidConfigurationMessage = true;
|
||||||
|
|||||||
@@ -42,12 +42,11 @@ angular.module('portainer.app').controller('GenericDatatableController', [
|
|||||||
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.changeOrderBy = changeOrderBy.bind(this);
|
this.changeOrderBy = function changeOrderBy(orderField) {
|
||||||
function changeOrderBy(orderField) {
|
|
||||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||||
this.state.orderBy = orderField;
|
this.state.orderBy = orderField;
|
||||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
||||||
}
|
};
|
||||||
|
|
||||||
this.selectItem = function (item, event) {
|
this.selectItem = function (item, event) {
|
||||||
// Handle range select using shift
|
// Handle range select using shift
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body classes="no-padding">
|
<rd-widget-body classes="no-padding">
|
||||||
<div class="toolBar">
|
<div class="toolBar">
|
||||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
<div class="toolBarTitle"><i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
|
||||||
<div class="settings">
|
<div class="settings">
|
||||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
|
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
>
|
>
|
||||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.stacks.newstack" authorization="PortainerStackCreate">
|
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.stacks.newstack" ng-disabled="!$ctrl.createEnabled" authorization="PortainerStackCreate">
|
||||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack
|
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -144,7 +144,10 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer" ng-if="$ctrl.dataset">
|
<div class="footer" ng-if="$ctrl.dataset">
|
||||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
|
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
||||||
|
{{ $ctrl.state.selectedItemCount }}
|
||||||
|
item(s) selected
|
||||||
|
</div>
|
||||||
<div class="paginationControls">
|
<div class="paginationControls">
|
||||||
<form class="form-inline">
|
<form class="form-inline">
|
||||||
<span class="limitSelector">
|
<span class="limitSelector">
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ angular.module('portainer.app').component('stacksDatatable', {
|
|||||||
removeAction: '<',
|
removeAction: '<',
|
||||||
offlineMode: '<',
|
offlineMode: '<',
|
||||||
refreshCallback: '<',
|
refreshCallback: '<',
|
||||||
|
createEnabled: '<',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export function SettingsViewModel(data) {
|
|||||||
this.EnableHostManagementFeatures = data.EnableHostManagementFeatures;
|
this.EnableHostManagementFeatures = data.EnableHostManagementFeatures;
|
||||||
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
|
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
|
||||||
this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures;
|
this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures;
|
||||||
|
this.AllowStackManagementForRegularUsers = data.AllowStackManagementForRegularUsers;
|
||||||
|
this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers;
|
||||||
|
this.AllowDeviceMappingForRegularUsers = data.AllowDeviceMappingForRegularUsers;
|
||||||
|
this.AllowContainerCapabilitiesForRegularUsers = data.AllowContainerCapabilitiesForRegularUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PublicSettingsViewModel(settings) {
|
export function PublicSettingsViewModel(settings) {
|
||||||
@@ -25,6 +29,10 @@ export function PublicSettingsViewModel(settings) {
|
|||||||
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||||
this.LogoURL = settings.LogoURL;
|
this.LogoURL = settings.LogoURL;
|
||||||
this.OAuthLoginURI = settings.OAuthLoginURI;
|
this.OAuthLoginURI = settings.OAuthLoginURI;
|
||||||
|
this.AllowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers;
|
||||||
|
this.AllowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers;
|
||||||
|
this.AllowHostNamespaceForRegularUsers = settings.AllowHostNamespaceForRegularUsers;
|
||||||
|
this.AllowContainerCapabilitiesForRegularUsers = settings.AllowContainerCapabilitiesForRegularUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LDAPSettingsViewModel(data) {
|
export function LDAPSettingsViewModel(data) {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ angular.module('portainer.app').factory('Authentication', [
|
|||||||
function logout() {
|
function logout() {
|
||||||
StateManager.clean();
|
StateManager.clean();
|
||||||
EndpointProvider.clean();
|
EndpointProvider.clean();
|
||||||
LocalStorage.clean();
|
LocalStorage.cleanAuthData();
|
||||||
LocalStorage.storeLoginStateUUID('');
|
LocalStorage.storeLoginStateUUID('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ angular.module('portainer.app').factory('EndpointProvider', [
|
|||||||
};
|
};
|
||||||
|
|
||||||
service.clean = function () {
|
service.clean = function () {
|
||||||
|
LocalStorage.cleanEndpointData();
|
||||||
endpoint = {};
|
endpoint = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
angular.module('portainer.app').factory('LocalStorage', [
|
angular.module('portainer.app').factory('LocalStorage', [
|
||||||
'localStorageService',
|
'localStorageService',
|
||||||
function LocalStorageFactory(localStorageService) {
|
function LocalStorageFactory(localStorageService) {
|
||||||
'use strict';
|
|
||||||
return {
|
return {
|
||||||
storeEndpointID: function (id) {
|
storeEndpointID: function (id) {
|
||||||
localStorageService.set('ENDPOINT_ID', id);
|
localStorageService.set('ENDPOINT_ID', id);
|
||||||
@@ -16,10 +15,10 @@ angular.module('portainer.app').factory('LocalStorage', [
|
|||||||
return localStorageService.get('ENDPOINT_PUBLIC_URL');
|
return localStorageService.get('ENDPOINT_PUBLIC_URL');
|
||||||
},
|
},
|
||||||
storeLoginStateUUID: function (uuid) {
|
storeLoginStateUUID: function (uuid) {
|
||||||
localStorageService.cookie.set('LOGIN_STATE_UUID', uuid);
|
localStorageService.set('LOGIN_STATE_UUID', uuid);
|
||||||
},
|
},
|
||||||
getLoginStateUUID: function () {
|
getLoginStateUUID: function () {
|
||||||
return localStorageService.cookie.get('LOGIN_STATE_UUID');
|
return localStorageService.get('LOGIN_STATE_UUID');
|
||||||
},
|
},
|
||||||
storeOfflineMode: function (isOffline) {
|
storeOfflineMode: function (isOffline) {
|
||||||
localStorageService.set('ENDPOINT_OFFLINE_MODE', isOffline);
|
localStorageService.set('ENDPOINT_OFFLINE_MODE', isOffline);
|
||||||
@@ -46,10 +45,10 @@ angular.module('portainer.app').factory('LocalStorage', [
|
|||||||
return localStorageService.get('APPLICATION_STATE');
|
return localStorageService.get('APPLICATION_STATE');
|
||||||
},
|
},
|
||||||
storeUIState: function (state) {
|
storeUIState: function (state) {
|
||||||
localStorageService.cookie.set('UI_STATE', state);
|
localStorageService.set('UI_STATE', state);
|
||||||
},
|
},
|
||||||
getUIState: function () {
|
getUIState: function () {
|
||||||
return localStorageService.cookie.get('UI_STATE');
|
return localStorageService.get('UI_STATE');
|
||||||
},
|
},
|
||||||
storeExtensionState: function (state) {
|
storeExtensionState: function (state) {
|
||||||
localStorageService.set('EXTENSION_STATE', state);
|
localStorageService.set('EXTENSION_STATE', state);
|
||||||
@@ -67,40 +66,40 @@ angular.module('portainer.app').factory('LocalStorage', [
|
|||||||
localStorageService.remove('JWT');
|
localStorageService.remove('JWT');
|
||||||
},
|
},
|
||||||
storePaginationLimit: function (key, count) {
|
storePaginationLimit: function (key, count) {
|
||||||
localStorageService.cookie.set('datatable_pagination_' + key, count);
|
localStorageService.set('datatable_pagination_' + key, count);
|
||||||
},
|
},
|
||||||
getPaginationLimit: function (key) {
|
getPaginationLimit: function (key) {
|
||||||
return localStorageService.cookie.get('datatable_pagination_' + key);
|
return localStorageService.get('datatable_pagination_' + key);
|
||||||
},
|
},
|
||||||
getDataTableOrder: function (key) {
|
getDataTableOrder: function (key) {
|
||||||
return localStorageService.cookie.get('datatable_order_' + key);
|
return localStorageService.get('datatable_order_' + key);
|
||||||
},
|
},
|
||||||
storeDataTableOrder: function (key, data) {
|
storeDataTableOrder: function (key, data) {
|
||||||
localStorageService.cookie.set('datatable_order_' + key, data);
|
localStorageService.set('datatable_order_' + key, data);
|
||||||
},
|
},
|
||||||
getDataTableTextFilters: function (key) {
|
getDataTableTextFilters: function (key) {
|
||||||
return localStorageService.cookie.get('datatable_text_filter_' + key);
|
return localStorageService.get('datatable_text_filter_' + key);
|
||||||
},
|
},
|
||||||
storeDataTableTextFilters: function (key, data) {
|
storeDataTableTextFilters: function (key, data) {
|
||||||
localStorageService.cookie.set('datatable_text_filter_' + key, data);
|
localStorageService.set('datatable_text_filter_' + key, data);
|
||||||
},
|
},
|
||||||
getDataTableFilters: function (key) {
|
getDataTableFilters: function (key) {
|
||||||
return localStorageService.cookie.get('datatable_filters_' + key);
|
return localStorageService.get('datatable_filters_' + key);
|
||||||
},
|
},
|
||||||
storeDataTableFilters: function (key, data) {
|
storeDataTableFilters: function (key, data) {
|
||||||
localStorageService.cookie.set('datatable_filters_' + key, data);
|
localStorageService.set('datatable_filters_' + key, data);
|
||||||
},
|
},
|
||||||
getDataTableSettings: function (key) {
|
getDataTableSettings: function (key) {
|
||||||
return localStorageService.cookie.get('datatable_settings_' + key);
|
return localStorageService.get('datatable_settings_' + key);
|
||||||
},
|
},
|
||||||
storeDataTableSettings: function (key, data) {
|
storeDataTableSettings: function (key, data) {
|
||||||
localStorageService.cookie.set('datatable_settings_' + key, data);
|
localStorageService.set('datatable_settings_' + key, data);
|
||||||
},
|
},
|
||||||
getDataTableExpandedItems: function (key) {
|
getDataTableExpandedItems: function (key) {
|
||||||
return localStorageService.cookie.get('datatable_expandeditems_' + key);
|
return localStorageService.get('datatable_expandeditems_' + key);
|
||||||
},
|
},
|
||||||
storeDataTableExpandedItems: function (key, data) {
|
storeDataTableExpandedItems: function (key, data) {
|
||||||
localStorageService.cookie.set('datatable_expandeditems_' + key, data);
|
localStorageService.set('datatable_expandeditems_' + key, data);
|
||||||
},
|
},
|
||||||
getDataTableSelectedItems: function (key) {
|
getDataTableSelectedItems: function (key) {
|
||||||
return localStorageService.get('datatable_selecteditems_' + key);
|
return localStorageService.get('datatable_selecteditems_' + key);
|
||||||
@@ -109,16 +108,16 @@ angular.module('portainer.app').factory('LocalStorage', [
|
|||||||
localStorageService.set('datatable_selecteditems_' + key, data);
|
localStorageService.set('datatable_selecteditems_' + key, data);
|
||||||
},
|
},
|
||||||
storeSwarmVisualizerSettings: function (key, data) {
|
storeSwarmVisualizerSettings: function (key, data) {
|
||||||
localStorageService.cookie.set('swarmvisualizer_' + key, data);
|
localStorageService.set('swarmvisualizer_' + key, data);
|
||||||
},
|
},
|
||||||
getSwarmVisualizerSettings: function (key) {
|
getSwarmVisualizerSettings: function (key) {
|
||||||
return localStorageService.cookie.get('swarmvisualizer_' + key);
|
return localStorageService.get('swarmvisualizer_' + key);
|
||||||
},
|
},
|
||||||
storeColumnVisibilitySettings: function (key, data) {
|
storeColumnVisibilitySettings: function (key, data) {
|
||||||
localStorageService.cookie.set('col_visibility_' + key, data);
|
localStorageService.set('col_visibility_' + key, data);
|
||||||
},
|
},
|
||||||
getColumnVisibilitySettings: function (key) {
|
getColumnVisibilitySettings: function (key) {
|
||||||
return localStorageService.cookie.get('col_visibility_' + key);
|
return localStorageService.get('col_visibility_' + key);
|
||||||
},
|
},
|
||||||
storeJobImage: function (data) {
|
storeJobImage: function (data) {
|
||||||
localStorageService.set('job_image', data);
|
localStorageService.set('job_image', data);
|
||||||
@@ -126,12 +125,24 @@ angular.module('portainer.app').factory('LocalStorage', [
|
|||||||
getJobImage: function () {
|
getJobImage: function () {
|
||||||
return localStorageService.get('job_image');
|
return localStorageService.get('job_image');
|
||||||
},
|
},
|
||||||
|
storeToolbarToggle(value) {
|
||||||
|
localStorageService.set('toolbar_toggle', value);
|
||||||
|
},
|
||||||
|
getToolbarToggle() {
|
||||||
|
return localStorageService.get('toolbar_toggle');
|
||||||
|
},
|
||||||
storeLogoutReason: (reason) => localStorageService.set('logout_reason', reason),
|
storeLogoutReason: (reason) => localStorageService.set('logout_reason', reason),
|
||||||
getLogoutReason: () => localStorageService.get('logout_reason'),
|
getLogoutReason: () => localStorageService.get('logout_reason'),
|
||||||
cleanLogoutReason: () => localStorageService.remove('logout_reason'),
|
cleanLogoutReason: () => localStorageService.remove('logout_reason'),
|
||||||
clean: function () {
|
clean: function () {
|
||||||
localStorageService.clearAll();
|
localStorageService.clearAll();
|
||||||
},
|
},
|
||||||
|
cleanAuthData() {
|
||||||
|
localStorageService.remove('JWT', 'EXTENSION_STATE', 'APPLICATION_STATE', 'LOGIN_STATE_UUID');
|
||||||
|
},
|
||||||
|
cleanEndpointData() {
|
||||||
|
localStorageService.remove('ENDPOINT_ID', 'ENDPOINT_PUBLIC_URL', 'ENDPOINT_OFFLINE_MODE', 'ENDPOINTS_DATA', 'ENDPOINT_STATE');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -76,6 +76,36 @@ angular.module('portainer.app').factory('StateManager', [
|
|||||||
LocalStorage.storeApplicationState(state.application);
|
LocalStorage.storeApplicationState(state.application);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
manager.updateAllowStackManagementForRegularUsers = function updateAllowStackManagementForRegularUsers(allowStackManagementForRegularUsers) {
|
||||||
|
state.application.allowStackManagementForRegularUsers = allowStackManagementForRegularUsers;
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.updateAllowBindMountsForRegularUsers = function updateAllowBindMountsForRegularUsers(allowBindMountsForRegularUsers) {
|
||||||
|
state.application.allowBindMountsForRegularUsers = allowBindMountsForRegularUsers;
|
||||||
|
LocalStorage.storeApplicationState(state.application);
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.updateAllowPrivilegedModeForRegularUsers = function (AllowPrivilegedModeForRegularUsers) {
|
||||||
|
state.application.allowPrivilegedModeForRegularUsers = AllowPrivilegedModeForRegularUsers;
|
||||||
|
LocalStorage.storeApplicationState(state.application);
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.updateAllowContainerCapabilitiesForRegularUsers = function updateAllowContainerCapabilitiesForRegularUsers(allowContainerCapabilitiesForRegularUsers) {
|
||||||
|
state.application.allowContainerCapabilitiesForRegularUsers = allowContainerCapabilitiesForRegularUsers;
|
||||||
|
LocalStorage.storeApplicationState(state.application);
|
||||||
|
};
|
||||||
|
|
||||||
function assignStateFromStatusAndSettings(status, settings) {
|
function assignStateFromStatusAndSettings(status, settings) {
|
||||||
state.application.authentication = status.Authentication;
|
state.application.authentication = status.Authentication;
|
||||||
state.application.analytics = status.Analytics;
|
state.application.analytics = status.Analytics;
|
||||||
@@ -87,6 +117,12 @@ angular.module('portainer.app').factory('StateManager', [
|
|||||||
state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures;
|
state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures;
|
||||||
state.application.enableVolumeBrowserForNonAdminUsers = settings.AllowVolumeBrowserForRegularUsers;
|
state.application.enableVolumeBrowserForNonAdminUsers = settings.AllowVolumeBrowserForRegularUsers;
|
||||||
state.application.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
state.application.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||||
|
state.application.allowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers;
|
||||||
|
state.application.allowBindMountsForRegularUsers = settings.AllowBindMountsForRegularUsers;
|
||||||
|
state.application.allowPrivilegedModeForRegularUsers = settings.AllowPrivilegedModeForRegularUsers;
|
||||||
|
state.application.allowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers;
|
||||||
|
state.application.allowHostNamespaceForRegularUsers = settings.AllowHostNamespaceForRegularUsers;
|
||||||
|
state.application.allowContainerCapabilitiesForRegularUsers = settings.AllowContainerCapabilitiesForRegularUsers;
|
||||||
state.application.validity = moment().unix();
|
state.application.validity = moment().unix();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,8 +102,10 @@
|
|||||||
button-spinner="state.actionInProgress"
|
button-spinner="state.actionInProgress"
|
||||||
style="margin-left: 0px;"
|
style="margin-left: 0px;"
|
||||||
>
|
>
|
||||||
<span ng-hide="state.actionInProgress">Enable extension</span>
|
<span ng-hide="state.actionInProgress" ng-if="!state.updateLicense">Enable extension</span>
|
||||||
<span ng-show="state.actionInProgress">Enabling extension...</span>
|
<span ng-show="state.actionInProgress" ng-if="!state.updateLicense">Enabling extension...</span>
|
||||||
|
<span ng-hide="state.actionInProgress" ng-if="state.updateLicense">Update license</span>
|
||||||
|
<span ng-show="state.actionInProgress" ng-if="state.updateLicense">Updating license...</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
angular.module('portainer.app').controller('ExtensionsController', [
|
angular.module('portainer.app').controller('ExtensionsController', [
|
||||||
'$scope',
|
'$scope',
|
||||||
@@ -9,6 +10,7 @@ angular.module('portainer.app').controller('ExtensionsController', [
|
|||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
currentDate: moment().format('YYYY-MM-dd'),
|
currentDate: moment().format('YYYY-MM-dd'),
|
||||||
|
updateLicense: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
@@ -59,6 +61,15 @@ angular.module('portainer.app').controller('ExtensionsController', [
|
|||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const licensePrefix = $scope.formValues.License[0];
|
||||||
|
|
||||||
|
$scope.state.updateLicense = false;
|
||||||
|
_.forEach($scope.extensions, (extension) => {
|
||||||
|
if (licensePrefix === '' + extension.Id && extension.Enabled) {
|
||||||
|
$scope.state.updateLicense = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
form.extension_license.$setValidity('invalidLicense', valid);
|
form.extension_license.$setValidity('invalidLicense', valid);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
angular.module('portainer.app').controller('MainController', [
|
angular.module('portainer.app').controller('MainController', [
|
||||||
'$scope',
|
'$scope',
|
||||||
'$cookieStore',
|
'LocalStorage',
|
||||||
'StateManager',
|
'StateManager',
|
||||||
'EndpointProvider',
|
'EndpointProvider',
|
||||||
function ($scope, $cookieStore, StateManager, EndpointProvider) {
|
function ($scope, LocalStorage, StateManager, EndpointProvider) {
|
||||||
/**
|
/**
|
||||||
* Sidebar Toggle & Cookie Control
|
* Sidebar Toggle & Cookie Control
|
||||||
*/
|
*/
|
||||||
@@ -17,11 +17,8 @@ angular.module('portainer.app').controller('MainController', [
|
|||||||
|
|
||||||
$scope.$watch($scope.getWidth, function (newValue) {
|
$scope.$watch($scope.getWidth, function (newValue) {
|
||||||
if (newValue >= mobileView) {
|
if (newValue >= mobileView) {
|
||||||
if (angular.isDefined($cookieStore.get('toggle'))) {
|
const toggleValue = LocalStorage.getToolbarToggle();
|
||||||
$scope.toggle = !$cookieStore.get('toggle') ? false : true;
|
$scope.toggle = typeof toggleValue === 'boolean' ? toggleValue : true;
|
||||||
} else {
|
|
||||||
$scope.toggle = true;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$scope.toggle = false;
|
$scope.toggle = false;
|
||||||
}
|
}
|
||||||
@@ -29,7 +26,7 @@ angular.module('portainer.app').controller('MainController', [
|
|||||||
|
|
||||||
$scope.toggleSidebar = function () {
|
$scope.toggleSidebar = function () {
|
||||||
$scope.toggle = !$scope.toggle;
|
$scope.toggle = !$scope.toggle;
|
||||||
$cookieStore.put('toggle', $scope.toggle);
|
LocalStorage.storeToolbarToggle($scope.toggle);
|
||||||
};
|
};
|
||||||
|
|
||||||
window.onresize = function () {
|
window.onresize = function () {
|
||||||
|
|||||||
@@ -120,6 +120,55 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="toggle_disableStackManagementForRegularUsers" class="control-label text-left">
|
||||||
|
Disable the use of Stacks for non-administrators
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;">
|
||||||
|
<input type="checkbox" name="toggle_disableStackManagementForRegularUsers" ng-model="formValues.disableStackManagementForRegularUsers" /><i></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="toggle_allowHostNamespaceForRegularUsers" class="control-label text-left">
|
||||||
|
Disable the use of host PID 1 for non-administrators
|
||||||
|
<portainer-tooltip position="bottom" message="Prevent users from accessing the host filesystem through the host PID namespace."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;">
|
||||||
|
<input type="checkbox" name="toggle_allowHostNamespaceForRegularUsers" ng-model="formValues.restrictHostNamespaceForRegularUsers" /><i></i>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="toggle_disableContainerCapabilitiesForRegularUsers" class="control-label text-left">
|
||||||
|
Disable container capabilities for non-administrators
|
||||||
|
</label>
|
||||||
|
<label class="switch" style="margin-left: 20px;">
|
||||||
|
<input type="checkbox" name="toggle_disableContainerCapabilitiesForRegularUsers" ng-model="formValues.disableContainerCapabilitiesForRegularUsers" /><i></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="isContainerEditDisabled()">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
Note: The recreate/duplicate/edit feature is currently disabled (for non-admin users) by one or more security settings.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<!-- !security -->
|
<!-- !security -->
|
||||||
<!-- edge -->
|
<!-- edge -->
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
|||||||
@@ -33,6 +33,23 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||||||
enableHostManagementFeatures: false,
|
enableHostManagementFeatures: false,
|
||||||
enableVolumeBrowser: false,
|
enableVolumeBrowser: false,
|
||||||
enableEdgeComputeFeatures: false,
|
enableEdgeComputeFeatures: false,
|
||||||
|
allowStackManagementForRegularUsers: false,
|
||||||
|
restrictHostNamespaceForRegularUsers: false,
|
||||||
|
allowDeviceMappingForRegularUsers: false,
|
||||||
|
disableContainerCapabilitiesForRegularUsers: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.isContainerEditDisabled = function isContainerEditDisabled() {
|
||||||
|
const {
|
||||||
|
restrictBindMounts,
|
||||||
|
restrictHostNamespaceForRegularUsers,
|
||||||
|
restrictPrivilegedMode,
|
||||||
|
disableDeviceMappingForRegularUsers,
|
||||||
|
disableContainerCapabilitiesForRegularUsers,
|
||||||
|
} = this.formValues;
|
||||||
|
return (
|
||||||
|
restrictBindMounts || restrictHostNamespaceForRegularUsers || restrictPrivilegedMode || disableDeviceMappingForRegularUsers || disableContainerCapabilitiesForRegularUsers
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.removeFilteredContainerLabel = function (index) {
|
$scope.removeFilteredContainerLabel = function (index) {
|
||||||
@@ -69,6 +86,10 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||||||
settings.AllowVolumeBrowserForRegularUsers = $scope.formValues.enableVolumeBrowser;
|
settings.AllowVolumeBrowserForRegularUsers = $scope.formValues.enableVolumeBrowser;
|
||||||
settings.EnableHostManagementFeatures = $scope.formValues.enableHostManagementFeatures;
|
settings.EnableHostManagementFeatures = $scope.formValues.enableHostManagementFeatures;
|
||||||
settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures;
|
settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures;
|
||||||
|
settings.AllowStackManagementForRegularUsers = !$scope.formValues.disableStackManagementForRegularUsers;
|
||||||
|
settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers;
|
||||||
|
settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers;
|
||||||
|
settings.AllowContainerCapabilitiesForRegularUsers = !$scope.formValues.disableContainerCapabilitiesForRegularUsers;
|
||||||
|
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
updateSettings(settings);
|
updateSettings(settings);
|
||||||
@@ -83,6 +104,12 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||||||
StateManager.updateEnableHostManagementFeatures(settings.EnableHostManagementFeatures);
|
StateManager.updateEnableHostManagementFeatures(settings.EnableHostManagementFeatures);
|
||||||
StateManager.updateEnableVolumeBrowserForNonAdminUsers(settings.AllowVolumeBrowserForRegularUsers);
|
StateManager.updateEnableVolumeBrowserForNonAdminUsers(settings.AllowVolumeBrowserForRegularUsers);
|
||||||
StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures);
|
StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures);
|
||||||
|
StateManager.updateAllowStackManagementForRegularUsers(settings.AllowStackManagementForRegularUsers);
|
||||||
|
StateManager.updateAllowHostNamespaceForRegularUsers(settings.AllowHostNamespaceForRegularUsers);
|
||||||
|
StateManager.updateAllowDeviceMappingForRegularUsers(settings.AllowDeviceMappingForRegularUsers);
|
||||||
|
StateManager.updateAllowPrivilegedModeForRegularUsers(settings.AllowPrivilegedModeForRegularUsers);
|
||||||
|
StateManager.updateAllowBindMountsForRegularUsers(settings.AllowBindMountsForRegularUsers);
|
||||||
|
StateManager.updateAllowContainerCapabilitiesForRegularUsers(settings.AllowContainerCapabilitiesForRegularUsers);
|
||||||
$state.reload();
|
$state.reload();
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
@@ -109,6 +136,10 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||||||
$scope.formValues.enableVolumeBrowser = settings.AllowVolumeBrowserForRegularUsers;
|
$scope.formValues.enableVolumeBrowser = settings.AllowVolumeBrowserForRegularUsers;
|
||||||
$scope.formValues.enableHostManagementFeatures = settings.EnableHostManagementFeatures;
|
$scope.formValues.enableHostManagementFeatures = settings.EnableHostManagementFeatures;
|
||||||
$scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
$scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||||
|
$scope.formValues.disableStackManagementForRegularUsers = !settings.AllowStackManagementForRegularUsers;
|
||||||
|
$scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers;
|
||||||
|
$scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers;
|
||||||
|
$scope.formValues.disableContainerCapabilitiesForRegularUsers = !settings.AllowContainerCapabilitiesForRegularUsers;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"
|
standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"
|
||||||
admin-access="!applicationState.application.authentication || isAdmin"
|
admin-access="!applicationState.application.authentication || isAdmin"
|
||||||
offline-mode="endpointState.OfflineMode"
|
offline-mode="endpointState.OfflineMode"
|
||||||
|
show-stacks="showStacks"
|
||||||
></docker-sidebar-content>
|
></docker-sidebar-content>
|
||||||
<li class="sidebar-title" authorization="IntegrationStoridgeAdmin" ng-if="applicationState.endpoint.mode && applicationState.endpoint.extensions.length > 0">
|
<li class="sidebar-title" authorization="IntegrationStoridgeAdmin" ng-if="applicationState.endpoint.mode && applicationState.endpoint.extensions.length > 0">
|
||||||
<span>Integrations</span>
|
<span>Integrations</span>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
angular.module('portainer.app').controller('SidebarController', [
|
angular.module('portainer.app').controller('SidebarController', [
|
||||||
'$q',
|
'$q',
|
||||||
'$scope',
|
'$scope',
|
||||||
|
'$transitions',
|
||||||
'StateManager',
|
'StateManager',
|
||||||
'Notifications',
|
'Notifications',
|
||||||
'Authentication',
|
'Authentication',
|
||||||
'UserService',
|
'UserService',
|
||||||
function ($q, $scope, StateManager, Notifications, Authentication, UserService) {
|
'ExtensionService',
|
||||||
|
function ($q, $scope, $transitions, StateManager, Notifications, Authentication, UserService, ExtensionService) {
|
||||||
function checkPermissions(memberships) {
|
function checkPermissions(memberships) {
|
||||||
var isLeader = false;
|
var isLeader = false;
|
||||||
angular.forEach(memberships, function (membership) {
|
angular.forEach(memberships, function (membership) {
|
||||||
@@ -16,9 +18,10 @@ angular.module('portainer.app').controller('SidebarController', [
|
|||||||
$scope.isTeamLeader = isLeader;
|
$scope.isTeamLeader = isLeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initView() {
|
async function initView() {
|
||||||
$scope.uiVersion = StateManager.getState().application.version;
|
$scope.uiVersion = StateManager.getState().application.version;
|
||||||
$scope.logo = StateManager.getState().application.logo;
|
$scope.logo = StateManager.getState().application.logo;
|
||||||
|
$scope.showStacks = await shouldShowStacks();
|
||||||
|
|
||||||
var authenticationEnabled = $scope.applicationState.application.authentication;
|
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||||
if (authenticationEnabled) {
|
if (authenticationEnabled) {
|
||||||
@@ -37,5 +40,24 @@ angular.module('portainer.app').controller('SidebarController', [
|
|||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
initView();
|
||||||
|
|
||||||
|
async function shouldShowStacks() {
|
||||||
|
const isAdmin = !$scope.applicationState.application.authentication || Authentication.isAdmin();
|
||||||
|
const { allowStackManagementForRegularUsers } = $scope.applicationState.application;
|
||||||
|
|
||||||
|
if (isAdmin || allowStackManagementForRegularUsers) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
||||||
|
if (rbacEnabled) {
|
||||||
|
return Authentication.hasAuthorizations(['EndpointResourcesAccess']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$transitions.onEnter({}, async () => {
|
||||||
|
$scope.showStacks = await shouldShowStacks();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
show-ownership-column="applicationState.application.authentication"
|
show-ownership-column="applicationState.application.authentication"
|
||||||
offline-mode="offlineMode"
|
offline-mode="offlineMode"
|
||||||
refresh-callback="getStacks"
|
refresh-callback="getStacks"
|
||||||
|
create-enabled="createEnabled"
|
||||||
></stacks-datatable>
|
></stacks-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,65 +1,85 @@
|
|||||||
angular.module('portainer.app').controller('StacksController', [
|
angular.module('portainer.app').controller('StacksController', StacksController);
|
||||||
'$scope',
|
|
||||||
'$state',
|
|
||||||
'Notifications',
|
|
||||||
'StackService',
|
|
||||||
'ModalService',
|
|
||||||
'EndpointProvider',
|
|
||||||
function ($scope, $state, Notifications, StackService, ModalService, EndpointProvider) {
|
|
||||||
$scope.removeAction = function (selectedItems) {
|
|
||||||
ModalService.confirmDeletion('Do you want to remove the selected stack(s)? Associated services will be removed as well.', function onConfirm(confirmed) {
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
deleteSelectedStacks(selectedItems);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function deleteSelectedStacks(stacks) {
|
/* @ngInject */
|
||||||
var endpointId = EndpointProvider.endpointID();
|
function StacksController($scope, $state, Notifications, StackService, ModalService, EndpointProvider, Authentication, StateManager, ExtensionService) {
|
||||||
var actionCount = stacks.length;
|
$scope.removeAction = function (selectedItems) {
|
||||||
angular.forEach(stacks, function (stack) {
|
ModalService.confirmDeletion('Do you want to remove the selected stack(s)? Associated services will be removed as well.', function onConfirm(confirmed) {
|
||||||
StackService.remove(stack, stack.External, endpointId)
|
if (!confirmed) {
|
||||||
.then(function success() {
|
return;
|
||||||
Notifications.success('Stack successfully removed', stack.Name);
|
}
|
||||||
var index = $scope.stacks.indexOf(stack);
|
deleteSelectedStacks(selectedItems);
|
||||||
$scope.stacks.splice(index, 1);
|
});
|
||||||
})
|
};
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to remove stack ' + stack.Name);
|
|
||||||
})
|
|
||||||
.finally(function final() {
|
|
||||||
--actionCount;
|
|
||||||
if (actionCount === 0) {
|
|
||||||
$state.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.offlineMode = false;
|
function deleteSelectedStacks(stacks) {
|
||||||
|
var endpointId = EndpointProvider.endpointID();
|
||||||
$scope.getStacks = getStacks;
|
var actionCount = stacks.length;
|
||||||
function getStacks() {
|
angular.forEach(stacks, function (stack) {
|
||||||
var endpointMode = $scope.applicationState.endpoint.mode;
|
StackService.remove(stack, stack.External, endpointId)
|
||||||
var endpointId = EndpointProvider.endpointID();
|
.then(function success() {
|
||||||
|
Notifications.success('Stack successfully removed', stack.Name);
|
||||||
StackService.stacks(true, endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER', endpointId)
|
var index = $scope.stacks.indexOf(stack);
|
||||||
.then(function success(data) {
|
$scope.stacks.splice(index, 1);
|
||||||
var stacks = data;
|
|
||||||
$scope.stacks = stacks;
|
|
||||||
$scope.offlineMode = EndpointProvider.offlineMode();
|
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
$scope.stacks = [];
|
Notifications.error('Failure', err, 'Unable to remove stack ' + stack.Name);
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve stacks');
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
--actionCount;
|
||||||
|
if (actionCount === 0) {
|
||||||
|
$state.reload();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.offlineMode = false;
|
||||||
|
$scope.createEnabled = false;
|
||||||
|
|
||||||
|
$scope.getStacks = getStacks;
|
||||||
|
|
||||||
|
function getStacks() {
|
||||||
|
var endpointMode = $scope.applicationState.endpoint.mode;
|
||||||
|
var endpointId = EndpointProvider.endpointID();
|
||||||
|
|
||||||
|
StackService.stacks(true, endpointMode.provider === 'DOCKER_SWARM_MODE' && endpointMode.role === 'MANAGER', endpointId)
|
||||||
|
.then(function success(data) {
|
||||||
|
var stacks = data;
|
||||||
|
$scope.stacks = stacks;
|
||||||
|
$scope.offlineMode = EndpointProvider.offlineMode();
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
$scope.stacks = [];
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve stacks');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCreateEnabled() {
|
||||||
|
const appState = StateManager.getState().application;
|
||||||
|
if (appState.allowStackManagementForRegularUsers) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initView() {
|
let isAdmin = true;
|
||||||
getStacks();
|
if (appState.authentication) {
|
||||||
|
isAdmin = Authentication.isAdmin();
|
||||||
|
}
|
||||||
|
if (isAdmin) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
const RBACExtensionEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
|
||||||
},
|
if (!RBACExtensionEnabled) {
|
||||||
]);
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Authentication.hasAuthorizations(['EndpointResourcesAccess']);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initView() {
|
||||||
|
getStacks();
|
||||||
|
$scope.createEnabled = await loadCreateEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
param (
|
|
||||||
[string]$platform,
|
|
||||||
[string]$arch
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop";
|
|
||||||
|
|
||||||
$binary = "portainer.exe"
|
|
||||||
$go_path = "$($(Get-ITEM -Path env:AGENT_TEMPDIRECTORY).Value)\go"
|
|
||||||
|
|
||||||
Set-Item env:GOPATH "$go_path"
|
|
||||||
|
|
||||||
New-Item -Name dist -Path "." -ItemType Directory -Force | Out-Null
|
|
||||||
New-Item -Name portainer -Path "$go_path\src\github.com\portainer" -ItemType Directory -Force | Out-Null
|
|
||||||
|
|
||||||
Copy-Item -Path "api" -Destination "$go_path\src\github.com\portainer\portainer\api" -Recurse -Force
|
|
||||||
|
|
||||||
Set-Location -Path "api\cmd\portainer"
|
|
||||||
|
|
||||||
go get -t -d -v ./...
|
|
||||||
## go build -v
|
|
||||||
& cmd /c 'go build -v 2>&1'
|
|
||||||
|
|
||||||
Copy-Item -Path "portainer.exe" -Destination "$($env:BUILD_SOURCESDIRECTORY)\dist\portainer.exe" -Force
|
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
PLATFORM=$1
|
||||||
|
ARCH=$2
|
||||||
|
|
||||||
export GOPATH="/tmp/go"
|
export GOPATH="/tmp/go"
|
||||||
|
|
||||||
binary="portainer"
|
binary="portainer"
|
||||||
@@ -10,6 +15,10 @@ cp -R api ${GOPATH}/src/github.com/portainer/portainer/api
|
|||||||
cd 'api/cmd/portainer'
|
cd 'api/cmd/portainer'
|
||||||
|
|
||||||
go get -t -d -v ./...
|
go get -t -d -v ./...
|
||||||
GOOS=$1 GOARCH=$2 CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s'
|
GOOS=${PLATFORM} GOARCH=${ARCH} CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s'
|
||||||
|
|
||||||
mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/$binary" "$BUILD_SOURCESDIRECTORY/dist/portainer"
|
if [ "${PLATFORM}" == 'windows' ]; then
|
||||||
|
mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/${binary}.exe" "$BUILD_SOURCESDIRECTORY/dist/portainer.exe"
|
||||||
|
else
|
||||||
|
mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/$binary" "$BUILD_SOURCESDIRECTORY/dist/portainer"
|
||||||
|
fi
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
param (
|
|
||||||
[string]$docker_version
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop";
|
|
||||||
|
|
||||||
New-Item -Path "docker-binary" -ItemType Directory | Out-Null
|
|
||||||
|
|
||||||
$download_folder = "docker-binary"
|
|
||||||
|
|
||||||
Invoke-WebRequest -O "$($download_folder)/docker-binaries.zip" "https://download.docker.com/win/static/stable/x86_64/docker-$($docker_version).zip"
|
|
||||||
Expand-Archive -Path "$($download_folder)/docker-binaries.zip" -DestinationPath "$($download_folder)"
|
|
||||||
Move-Item -Path "$($download_folder)/docker/docker.exe" -Destination "dist"
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
ARG OSVERSION
|
||||||
|
FROM --platform=linux/amd64 gcr.io/k8s-staging-e2e-test-images/windows-servercore-cache:1.0-linux-amd64-${OSVERSION} as core
|
||||||
|
FROM mcr.microsoft.com/windows/nanoserver:${OSVERSION}
|
||||||
|
|
||||||
|
COPY --from=core /Windows/System32/netapi32.dll /Windows/System32/netapi32.dll
|
||||||
|
|
||||||
|
USER ContainerAdministrator
|
||||||
|
|
||||||
|
COPY dist /
|
||||||
|
|
||||||
|
EXPOSE 9000
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
ENTRYPOINT ["/portainer.exe"]
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
FROM microsoft/nanoserver:sac2016
|
|
||||||
|
|
||||||
COPY dist /
|
|
||||||
|
|
||||||
WORKDIR /
|
|
||||||
|
|
||||||
EXPOSE 9000
|
|
||||||
|
|
||||||
ENTRYPOINT ["/portainer.exe"]
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: portainer
|
Name: portainer
|
||||||
Version: 1.24.0
|
Version: 1.24.1
|
||||||
Release: 0
|
Release: 0
|
||||||
License: Zlib
|
License: Zlib
|
||||||
Summary: A lightweight docker management UI
|
Summary: A lightweight docker management UI
|
||||||
|
|||||||
+9
-16
@@ -142,11 +142,7 @@ function shell_build_binary(p, a) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shell_build_binary_azuredevops(p, a) {
|
function shell_build_binary_azuredevops(p, a) {
|
||||||
if (p === 'linux') {
|
return 'build/build_binary_azuredevops.sh ' + p + ' ' + a + ';';
|
||||||
return 'build/build_binary_azuredevops.sh ' + p + ' ' + a + ';';
|
|
||||||
} else {
|
|
||||||
return 'powershell -Command ".\\build\\build_binary_azuredevops.ps1 -platform ' + p + ' -arch ' + a + '"';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function shell_run_container() {
|
function shell_run_container() {
|
||||||
@@ -164,15 +160,12 @@ function shell_download_docker_binary(p, a) {
|
|||||||
var ip = ps[p] === undefined ? p : ps[p];
|
var ip = ps[p] === undefined ? p : ps[p];
|
||||||
var ia = as[a] === undefined ? a : as[a];
|
var ia = as[a] === undefined ? a : as[a];
|
||||||
var binaryVersion = p === 'windows' ? '<%= shippedDockerVersionWindows %>' : '<%= shippedDockerVersion %>';
|
var binaryVersion = p === 'windows' ? '<%= shippedDockerVersionWindows %>' : '<%= shippedDockerVersion %>';
|
||||||
if (p === 'linux' || p === 'mac') {
|
|
||||||
return ['if [ -f dist/docker ]; then', 'echo "Docker binary exists";', 'else', 'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';', 'fi'].join(' ');
|
return [
|
||||||
} else {
|
'if [ -f dist/docker ] || [ -f dist/docker.exe ]; then',
|
||||||
return [
|
'echo "docker binary exists";',
|
||||||
'powershell -Command "& {if (Get-Item -Path dist/docker.exe -ErrorAction:SilentlyContinue) {',
|
'else',
|
||||||
'Write-Host "Docker binary exists"',
|
'build/download_docker_binary.sh ' + ip + ' ' + ia + ' ' + binaryVersion + ';',
|
||||||
'} else {',
|
'fi',
|
||||||
'& ".\\build\\download_docker_binary.ps1" -docker_version ' + binaryVersion + '',
|
].join(' ');
|
||||||
'}}"',
|
|
||||||
].join(' ');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"author": "Portainer.io",
|
"author": "Portainer.io",
|
||||||
"name": "portainer",
|
"name": "portainer",
|
||||||
"homepage": "http://portainer.io",
|
"homepage": "http://portainer.io",
|
||||||
"version": "1.24.0",
|
"version": "1.24.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@github.com:portainer/portainer.git"
|
"url": "git@github.com:portainer/portainer.git"
|
||||||
|
|||||||
Reference in New Issue
Block a user