Compare commits

...

21 Commits

Author SHA1 Message Date
Dmitry Salakhov 39e9dca7b8 bump version to 1.24.2 (#4927) 2021-03-18 13:02:39 +13:00
Chaim Lev-Ari cfdd38c55e fix(endpoints): create roles array in edge endpoint (#4742) 2021-03-14 20:55:32 +01:00
Chaim Lev-Ari a12a0b61dc feat(containers): enforce disable bind mounts (#4110) (#4467)
* feat(containers): enforce disable bind mounts (#4110)

* feat(containers): enforce disable bind mounts

* refactor(docker): move check for endpoint admin to a function

* feat(docker): check if service has bind mounts

* feat(services): allow bind mounts for endpoint admin

* feat(container): enable bind mounts for endpoint admin

* fix(services): fix typo

* fix(docker): tag user as admin when auth is disabled
2021-03-04 11:50:35 +01:00
Steven Kang d2cdbf789e feat(build): introduced buildx for 1.24 branch (#4850) 2021-02-16 09:49:37 +13:00
Chaim Lev-Ari 06db4e0ad4 fix(auth): skip security checks with --no-auth flag (#4513)
* fix(stacks): skip security checks if no-auth

* fix(containers): skip security check when auth is disabled

* fix(volumes): show browse if auth is disabled
2021-01-18 09:31:23 +13:00
Chaim Lev-Ari 9f92e0aee3 feat(settings): introduce setting to disable container caps for non-admins (#4109) (#4510)
* feat(settings): introduce settings to allow/disable

* feat(settings): update the setting

* feat(docker): prevent user from using caps if disabled

* refactor(stacks): revert file

* style(api): remove portainer ns
2020-12-09 17:15:19 +13:00
Anthony Lapenna f347d97daf chore(version): bump version number 2020-07-23 10:28:34 +12:00
Anthony Lapenna d5cee5b8b1 feat(core/extensions): add the ability to update a license (#4081)
* feat(core/extensions): add the ability to update a license

* feat(core/extensions): trigger data upgrade if extension is not enabled yet

* feat(core/extensions): trigger data upgrade if extension is not enabled yet

* feat(core/extensions): trigger data upgrade if extension is not enabled yet

* feat(core/extensions): trigger data upgrade if extension is not enabled yet
2020-07-22 21:13:51 +12:00
Anthony Lapenna 4da6824bc7 feat(database): review database migration (#4054) 2020-07-17 17:04:32 +12:00
Chaim Lev-Ari 80b6b6e300 fix(registries): filter gitlab repos without tags (#4048) 2020-07-16 20:57:52 +12:00
Anthony Lapenna 484dab5932 feat(database): trigger missing database migration for AllowHostNamespaceForRegularUsers setting (#4035) 2020-07-13 22:27:22 +12:00
Chaim Lev-Ari f8bd075ce4 feat(containers): disable edit container on security setting restricting regular users (#4033)
* feat(settings): add info about container edit disable

* feat(settings): set security settings

* feat(containers): hide recreate button when setting is enabled

* feat(settings): rephrase security notice

* fix(settings): save allowHostNamespaceForRegularUsers to state
2020-07-13 22:26:23 +12:00
Chaim Lev-Ari cd58c16b4e feat(settings): hide stacks for non admin when settings is set (#4025)
* refactor(settings): replace disableDeviceMapping with allow

* feat(dashboard): hide stacks if settings disabled and non admin

* refactor(sidebar): check if user is endpoint admin

* feat(settings): set the default value for stack management

* feat(settings): rename field label

* fix(sidebar): refresh show stacks state
2020-07-13 18:36:47 +12:00
Chaim Lev-Ari 5ebb03cb4e feat(settings): add setting to disable device mapping for regular users (#4017)
* feat(settings): introduce device mapping service

* feat(containers): hide devices field when setting is on

* feat(containers): prevent passing of devices when not allowed

* feat(stacks): prevent non admin from device mapping

* feat(stacks): disallow swarm stack creation for user

* refactor(settings): replace disableDeviceMapping with allow

* fix(stacks): remove check for disable device mappings from swarm

* feat(settings): rename field to disable

* feat(settings): supply default value for disableDeviceMapping

* feat(container): check for endpoint admin
2020-07-13 16:32:56 +12:00
Chaim Lev-Ari dffcd3fdfd feat(settings): replace cookies with local storage (#3979)
* feat(cookies): use secured cookies in frontend

* fix(datatables): persist state changes

* fix(datatables): persist order

* feat(sidebar): use local storage to store toggle state

* feat(config): use local storage instead of cookies
2020-07-10 11:51:31 +12:00
Chaim Lev-Ari 3f7687e78a feat(server): support minimum tls v1.2 (#4019)
* feat(crypto): use tls 1.2

* feat(crypto): use secure cipher suites

* feat(server): accept tls1.2 connections

* refactor(crypto): create base tls config

* refactor(server): use basic tls config

* fix(server): remove unused import

* refactor(crypto): rename tls conf factory
2020-07-10 11:48:01 +12:00
Maxime Bajeux 0f58ece899 feat(containers): prevent non-admin users from running containers using the host namespace pid (#3970)
* feat(containers): Prevent non-admin users from running containers using the host namespace pid

* feat(containers): add rbac check for swarm stack too

* feat(containers): remove forgotten conflict

* feat(containers): init EnableHostNamespaceUse to true and return 403 on forbidden action

* feat(containers): change enableHostNamespaceUse to restrictHostNamespaceUse in html

* feat(settings): rename EnableHostNamespaceUse to AllowHostNamespaceForRegularUsers
2020-07-08 09:48:34 +12:00
Chaim Lev-Ari b0ad212858 fix(registries): hide zero tags repositories (#3985) 2020-07-07 10:59:33 +12:00
Chaim Lev-Ari 7eb2fd3424 feat(stacks): add a setting to disable the creation of stacks for non-admin users (#3932)
* feat(settings): introduce a setting to prevent non-admin from stack creation

* feat(settings): update stack creation setting

* feat(settings): fail stack creation if user is non admin

* fix(settings): save preventStackCreation setting to state

* feat(stacks): disable add button when settings is enabled

* format(stacks): remove line

* feat(stacks): setting to hide stacks from users

* feat(settings): rename disable stacks setting

* refactor(settings): rename setting to disableStackManagementForRegularUsers
2020-07-01 09:34:43 +12:00
Maxime Bajeux 4c0d8ce732 feat(containers): Ensure users cannot create privileged containers via the API (#3969)
* feat(containers): Ensure users cannot create privileged containers via the API

* feat(containers): add rbac check in stack creation
2020-06-30 17:13:37 +12:00
Anthony Lapenna e1cc4bc9a1 chore(version): bump version number 2020-06-16 17:22:51 +12:00
66 changed files with 997 additions and 319 deletions
+3 -1
View File
@@ -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()
+14
View File
@@ -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)
}
+12
View File
@@ -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)
}
+17 -1
View File
@@ -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)
} }
+12 -8
View File
@@ -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 != "" {
+18
View File
@@ -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}
+26 -18
View File
@@ -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 != "" {
+34 -14
View File
@@ -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
+30 -12
View File
@@ -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
}
} }
} }
+26 -12
View File
@@ -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
}
} }
} }
+58 -3
View File
@@ -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
}
+49 -8
View File
@@ -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 {
+1
View File
@@ -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
}
+55
View File
@@ -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 -2
View File
@@ -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
}
+1
View File
@@ -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{}
+1
View File
@@ -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{}
+4 -1
View File
@@ -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 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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) {
+1 -1
View File
@@ -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);
@@ -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: '<',
}, },
}); });
+8
View File
@@ -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) {
+1 -1
View File
@@ -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 = {};
}; };
+32 -21
View File
@@ -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');
},
}; };
}, },
]); ]);
+36
View File
@@ -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);
}; };
+5 -8
View File
@@ -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');
+1
View File
@@ -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();
});
}, },
]); ]);
+1
View File
@@ -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>
+76 -56
View File
@@ -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();
}
-24
View File
@@ -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
+11 -2
View File
@@ -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
-13
View File
@@ -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"
+14
View File
@@ -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"]
-9
View File
@@ -1,9 +0,0 @@
FROM microsoft/nanoserver:sac2016
COPY dist /
WORKDIR /
EXPOSE 9000
ENTRYPOINT ["/portainer.exe"]
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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"