Compare commits

...

69 Commits
2.14.2 ... demo

Author SHA1 Message Date
Anthony Lapenna
df3886fd25 Merge tag '1.24.1' into demo
Release 1.24.1
2020-07-23 11:43:41 +12: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
Anthony Lapenna
f2682d82b6 merge branch master into demo 2020-06-05 11:55:22 +12:00
Anthony Lapenna
d3576fe8e6 merge branch master into demo 2020-06-05 11:55:14 +12:00
Anthony Lapenna
f34d27df0f Merge branch 'master' into demo 2020-03-20 11:43:39 +13:00
Anthony Lapenna
f228b28639 Merge branch 'master' into demo 2020-02-18 13:49:34 +13:00
Anthony Lapenna
d7b4e4aba1 Merge branch 'master' into demo 2019-12-04 17:30:43 +13:00
Anthony Lapenna
7234c443e8 Merge branch 'develop' into demo 2019-10-11 12:17:32 +13:00
Anthony Lapenna
5b844fd40a fix(http): allow endpoint update operation on demo environment 2019-07-26 15:25:13 +12:00
Anthony Lapenna
7f2ef8fb06 Merge branch 'develop' into demo
# Conflicts:
#	api/cmd/portainer/main.go
2019-07-26 14:43:10 +12:00
Anthony Lapenna
9f817749c1 Merge branch 'develop' into demo 2019-06-04 16:12:31 +12:00
Anthony Lapenna
36198b57a5 Merge branch 'develop' into demo 2019-03-05 18:48:33 +13:00
Anthony Lapenna
70f9e37eab fix(api): fix missing endpoint identifier 2019-02-14 11:37:54 +13:00
Anthony Lapenna
24f69f0185 Merge branch 'develop' into demo 2019-01-31 15:23:48 +13:00
Anthony Lapenna
4a2e9a892d fix(demo): fix invalid demo data and trigger snapshot at startup 2018-12-12 20:17:53 +13:00
Anthony Lapenna
1e4bedde4b Merge branch 'develop' into demo 2018-12-12 17:59:46 +13:00
Anthony Lapenna
9db07e7f4e Merge branch 'develop' into demo 2018-12-12 14:52:50 +13:00
Anthony Lapenna
d4b8d9947d Merge branch 'master' into demo 2018-09-15 19:30:58 +08:00
Anthony Lapenna
9194ddcd03 Merge branch 'develop' into demo 2018-07-28 20:35:14 +02:00
Anthony Lapenna
e1edf37770 Merge branch 'develop' into demo
Conflicts:
	app/portainer/views/settings/settings.html
2018-07-27 18:03:52 +02:00
Anthony Lapenna
82b73c06b4 Merge branch 'develop' into demo 2018-06-25 16:44:43 +03:00
Anthony Lapenna
ae286998ab Merge branch 'develop' into demo
Conflicts:
	api/cmd/portainer/main.go
	api/http/handler/endpoint.go
	api/http/handler/user.go
2018-06-21 16:47:13 +03:00
Anthony Lapenna
ffc3ed67e2 Merge branch 'develop' into demo
Conflicts:
	api/cmd/portainer/main.go
2018-05-21 13:45:45 +02:00
Anthony Lapenna
6252be4a08 Merge branch 'develop' into demo
Conflicts:
	api/cmd/portainer/main.go
	app/portainer/views/settings/settings.html
2018-05-10 18:14:48 +02:00
Anthony Lapenna
75481d928e Merge branch 'develop' into demo 2018-04-02 08:57:02 +10:00
Anthony Lapenna
d65d65803f Merge branch 'develop' into demo 2018-03-11 20:30:52 +10:00
Anthony Lapenna
6a8fc253bd fix(demo): fix demo init data 2018-03-03 10:31:09 +10:00
Anthony Lapenna
1fa8921c11 Merge branch 'develop' into demo 2018-03-03 10:16:10 +10:00
Anthony Lapenna
ba3892aebf Merge branch 'develop' into demo 2018-02-08 10:22:47 +01:00
Anthony Lapenna
27fc700d6f Merge branch 'develop' into demo 2018-01-21 18:29:26 +01:00
Anthony Lapenna
3187cb0ada feat(api): add demo.portainer.io as the Public URL of the endpoint 2018-01-16 15:53:49 +01:00
Anthony Lapenna
b462a15921 Merge branch 'develop' into demo 2017-12-11 17:29:33 +01:00
Anthony Lapenna
11a646a076 Merge branch 'develop' into demo
Conflicts:
	app/components/endpoints/endpoints.html
	app/components/users/users.html
2017-12-10 11:32:26 +01:00
Anthony Lapenna
4c1edaf251 Merge branch 'develop' into demo 2017-11-26 11:18:14 +01:00
Anthony Lapenna
b45c4f8bea Merge branch 'develop' into demo
Conflicts:
	app/components/settings/settings.html
2017-11-13 12:07:24 +01:00
Anthony Lapenna
968f070b0b Merge branch 'develop' into demo 2017-11-08 09:33:45 +01:00
Anthony Lapenna
e679483ffd Merge branch 'develop' into demo 2017-10-15 20:50:36 +02:00
Anthony Lapenna
494ef6c392 Merge branch 'develop' into 'demo' 2017-10-15 20:27:59 +02:00
Anthony Lapenna
7951c1d150 Merge branch 'develop' into demo 2017-09-28 19:03:22 +02:00
Anthony Lapenna
0083bdb6a2 Merge branch 'master' into demo 2017-09-21 17:49:21 +02:00
Anthony Lapenna
e65881223d Merge branch 'develop' into 'demo' 2017-09-20 16:15:23 +02:00
Anthony Lapenna
f0c9058568 Merge branch 'develop' into 'demo' 2017-08-13 21:03:39 +02:00
Anthony Lapenna
afd654e4a8 Merge branch 'develop' into demo 2017-07-17 16:01:00 +02:00
Anthony Lapenna
0ef116d6b5 Merge branch 'develop' into demo 2017-07-13 18:19:14 +02:00
Anthony Lapenna
ee64a53782 Merge branch 'develop' into demo 2017-06-29 16:41:40 +02:00
Anthony Lapenna
4a0c899df5 Merge branch 'develop' into 'demo' 2017-06-20 13:24:29 +02:00
Anthony Lapenna
840603bb8c feat(settings): prevent the display of the URL field for custom templates 2017-06-05 09:56:04 +02:00
Anthony Lapenna
5fe79621a6 feat(settings): set a default value for custom templates 2017-06-05 09:51:01 +02:00
Anthony Lapenna
d3b9822105 Merge branch 'develop' into 'demo' 2017-06-05 09:45:06 +02:00
Anthony Lapenna
9163bcae25 Merge branch 'develop' into demo 2017-05-25 12:40:11 +02:00
Anthony Lapenna
17dad6e36f fix(api): prevent the deletion of the admin user 2017-05-23 21:51:30 +02:00
Anthony Lapenna
75b836453a merge branch 'develop' into demo 2017-05-23 21:06:50 +02:00
Anthony Lapenna
b8866d487f Merge branch 'develop' into demo 2017-04-06 11:02:27 +02:00
Anthony Lapenna
dfb3b3aaa1 Merge branch 'develop' into demo 2017-03-28 15:38:24 +02:00
Anthony Lapenna
09fdc9781c feat(demo): add demo restrictions 2017-03-14 08:47:57 +01:00
80 changed files with 914 additions and 341 deletions

View File

@@ -6,10 +6,10 @@ Some basic conventions for contributing to this project.
Please make sure that there aren't existing pull requests attempting to address the issue mentioned. Likewise, please check for issues related to update, as someone else may be working on the issue in a branch or fork.
* Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring
* Develop in a topic branch, not master/develop
- Please open a discussion in a new issue / existing issue to talk about the changes you'd like to bring
- Develop in a topic branch, not master/develop
When creating a new branch, prefix it with the *type* of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator).
When creating a new branch, prefix it with the _type_ of the change (see section **Commit Message Format** below), the associated opened issue number, a dash and some text describing the issue (using dash as a separator).
For example, if you work on a bugfix for the issue #361, you could name the branch `fix361-template-selection`.
@@ -37,14 +37,14 @@ Lines should not exceed 100 characters. This allows the message to be easier to
Must be one of the following:
* **feat**: A new feature
* **fix**: A bug fix
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation only changes
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
semi-colons, etc)
* **refactor**: A code change that neither fixes a bug or adds a feature
* **test**: Adding missing tests
* **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
- **refactor**: A code change that neither fixes a bug or adds a feature
- **test**: Adding missing tests
- **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
generation
### Scope
@@ -57,9 +57,9 @@ You can use the **area** label tag associated on the issue here (for `area/conta
The subject contains succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* no dot (.) at the end
- use the imperative, present tense: "change" not "changed" nor "changes"
- don't capitalize first letter
- no dot (.) at the end
## Contribution process

View File

@@ -1,6 +1,8 @@
package migrator
import "github.com/portainer/portainer/api"
import (
"github.com/portainer/portainer/api"
)
func (m *Migrator) updateTagsToDBVersion23() error {
tags, err := m.tagService.Tags()

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

View File

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

View File

@@ -9,7 +9,7 @@ import (
"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/cli"
"github.com/portainer/portainer/api/cron"
@@ -70,6 +70,45 @@ func initStore(dataStorePath string, fileService portainer.FileService) *bolt.St
return store
}
func initDemoData(store *bolt.Store, cryptoService portainer.CryptoService) error {
password, err := cryptoService.Hash("tryportainer")
if err != nil {
return err
}
admin := &portainer.User{
Username: "admin",
Password: password,
Role: portainer.AdministratorRole,
}
err = store.UserService.CreateUser(admin)
if err != nil {
return err
}
localEndpoint := &portainer.Endpoint{
ID: portainer.EndpointID(1),
Name: "local",
URL: "unix:///var/run/docker.sock",
PublicURL: "demo.portainer.io",
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: []string{},
}
err = store.EndpointService.CreateEndpoint(localEndpoint)
if err != nil {
return err
}
return nil
}
func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager {
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
}
@@ -269,13 +308,16 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
SnapshotInterval: *flags.SnapshotInterval,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowVolumeBrowserForRegularUsers: false,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
EnableHostManagementFeatures: false,
AllowHostNamespaceForRegularUsers: true,
SnapshotInterval: *flags.SnapshotInterval,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
}
if *flags.Templates != "" {
@@ -529,6 +571,8 @@ func main() {
cryptoService := initCryptoService()
initDemoData(store, cryptoService)
digitalSignatureService := initDigitalSignatureService()
err := initKeyPair(fileService, digitalSignatureService)

View File

@@ -6,6 +6,24 @@ import (
"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
// loaded from memory.
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {

View File

@@ -89,6 +89,11 @@ const (
ErrUndefinedTLSFileType = Error("Undefined TLS file type")
)
// Demo errors.
const (
ErrNotAvailableInDemo = Error("This feature is not available in the demo version of Portainer")
)
// Extension errors.
const (
ErrExtensionAlreadyEnabled = Error("This extension is already enabled")

View File

@@ -21,6 +21,10 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
}
if endpointID == 1 {
return &httperror.HandlerError{http.StatusForbidden, "This feature is not available in the demo version of Portainer", portainer.ErrNotAvailableInDemo}
}
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}

View File

@@ -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}
}
for _, existingExtension := range extensions {
if existingExtension.ID == extensionID && existingExtension.Enabled {
return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", portainer.ErrExtensionAlreadyEnabled}
}
}
extension := &portainer.Extension{
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()
if err != nil {
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}
}
extension.Enabled = true
if extension.ID == portainer.RBACExtension {
if extension.ID == portainer.RBACExtension && !extension.Enabled {
err = handler.upgradeRBACData()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
}
}
extension.Enabled = true
err = handler.ExtensionService.Persist(extension)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}

View File

@@ -46,10 +46,21 @@ func (handler *Handler) extensionUpload(w http.ResponseWriter, r *http.Request)
}
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{
ID: extensionID,
}
for _, existingExtension := range extensions {
if existingExtension.ID == extensionID && (existingExtension.Enabled || !existingExtension.License.Valid) {
extension.Enabled = true
}
}
_ = handler.ExtensionManager.DisableExtension(extension)
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}
}
extension.Enabled = true
if extension.ID == portainer.RBACExtension {
if extension.ID == portainer.RBACExtension && !extension.Enabled {
err = handler.upgradeRBACData()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
}
}
extension.Enabled = true
err = handler.ExtensionService.Persist(extension)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}

View File

@@ -6,19 +6,22 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
)
type publicSettingsResponse struct {
LogoURL string `json:"LogoURL"`
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
ExternalTemplates bool `json:"ExternalTemplates"`
OAuthLoginURI string `json:"OAuthLoginURI"`
LogoURL string `json:"LogoURL"`
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
ExternalTemplates bool `json:"ExternalTemplates"`
OAuthLoginURI string `json:"OAuthLoginURI"`
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
}
// GET request on /api/settings/public
@@ -29,19 +32,22 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
}
publicSettings := &publicSettingsResponse{
LogoURL: settings.LogoURL,
AuthenticationMethod: settings.AuthenticationMethod,
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
ExternalTemplates: false,
LogoURL: settings.LogoURL,
AuthenticationMethod: settings.AuthenticationMethod,
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers,
ExternalTemplates: false,
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
settings.OAuthSettings.AuthorizationURI,
settings.OAuthSettings.ClientID,
settings.OAuthSettings.RedirectURI,
settings.OAuthSettings.Scopes),
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
}
if settings.TemplatesURL != "" {

View File

@@ -7,24 +7,27 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
)
type settingsUpdatePayload struct {
LogoURL *string
BlackListedLabels []portainer.Pair
AuthenticationMethod *int
LDAPSettings *portainer.LDAPSettings
OAuthSettings *portainer.OAuthSettings
AllowBindMountsForRegularUsers *bool
AllowPrivilegedModeForRegularUsers *bool
AllowVolumeBrowserForRegularUsers *bool
EnableHostManagementFeatures *bool
SnapshotInterval *string
TemplatesURL *string
EdgeAgentCheckinInterval *int
EnableEdgeComputeFeatures *bool
LogoURL *string
BlackListedLabels []portainer.Pair
AuthenticationMethod *int
LDAPSettings *portainer.LDAPSettings
OAuthSettings *portainer.OAuthSettings
AllowBindMountsForRegularUsers *bool
AllowPrivilegedModeForRegularUsers *bool
AllowVolumeBrowserForRegularUsers *bool
EnableHostManagementFeatures *bool
SnapshotInterval *string
TemplatesURL *string
EdgeAgentCheckinInterval *int
EnableEdgeComputeFeatures *bool
AllowStackManagementForRegularUsers *bool
AllowHostNamespaceForRegularUsers *bool
AllowDeviceMappingForRegularUsers *bool
}
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
@@ -114,6 +117,14 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
}
if payload.AllowStackManagementForRegularUsers != nil {
settings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers
}
if payload.AllowHostNamespaceForRegularUsers != nil {
settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers
}
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
if err != nil {
@@ -125,6 +136,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.EdgeAgentCheckinInterval = *payload.EdgeAgentCheckinInterval
}
if payload.AllowDeviceMappingForRegularUsers != nil {
settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
}
tlsError := handler.updateTLS(settings)
if tlsError != nil {
return tlsError

View File

@@ -1,7 +1,6 @@
package stacks
import (
"errors"
"net/http"
"path"
"regexp"
@@ -11,7 +10,7 @@ import (
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"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/http/security"
)
@@ -283,6 +282,7 @@ type composeStackDeploymentConfig struct {
dockerhub *portainer.DockerHub
registries []portainer.Registry
isAdmin bool
user *portainer.User
}
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
@@ -302,12 +302,18 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
}
filteredRegistries := security.FilterRegistries(registries, securityContext)
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{
stack: stack,
endpoint: endpoint,
dockerhub: dockerhub,
registries: filteredRegistries,
isAdmin: securityContext.IsAdmin,
user: user,
}
return config, nil
@@ -324,7 +330,16 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
return err
}
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return err
}
if (!settings.AllowBindMountsForRegularUsers ||
!settings.AllowPrivilegedModeForRegularUsers ||
!settings.AllowHostNamespaceForRegularUsers ||
!settings.AllowDeviceMappingForRegularUsers) && !isAdminOrEndpointAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
@@ -332,13 +347,10 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
return err
}
valid, err := handler.isValidStackFile(stackContent)
err = handler.isValidStackFile(stackContent, settings)
if err != nil {
return err
}
if !valid {
return errors.New("bind-mount disabled for non administrator users")
}
}
handler.stackCreationMutex.Lock()

View File

@@ -1,7 +1,6 @@
package stacks
import (
"errors"
"net/http"
"path"
"strconv"
@@ -10,7 +9,7 @@ import (
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"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/http/security"
)
@@ -292,6 +291,7 @@ type swarmStackDeploymentConfig struct {
registries []portainer.Registry
prune bool
isAdmin bool
user *portainer.User
}
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
@@ -311,6 +311,11 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
}
filteredRegistries := security.FilterRegistries(registries, securityContext)
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{
stack: stack,
endpoint: endpoint,
@@ -318,6 +323,7 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
registries: filteredRegistries,
prune: prune,
isAdmin: securityContext.IsAdmin,
user: user,
}
return config, nil
@@ -329,7 +335,13 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
return err
}
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
if err != nil {
return err
}
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
@@ -337,13 +349,10 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
return err
}
valid, err := handler.isValidStackFile(stackContent)
err = handler.isValidStackFile(stackContent, settings)
if err != nil {
return err
}
if !valid {
return errors.New("bind-mount disabled for non administrator users")
}
}
handler.stackCreationMutex.Lock()

View File

@@ -1,12 +1,13 @@
package stacks
import (
"errors"
"net/http"
"sync"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
@@ -87,3 +88,54 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR
}
return false, nil
}
func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID) (bool, error) {
if securityContext.IsAdmin {
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
}

View File

@@ -10,7 +10,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/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}
}
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))
if err == portainer.ErrObjectNotFound {
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)}
}
func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error) {
func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *portainer.Settings) error {
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
if err != nil {
return false, err
return err
}
composeConfigFile := types.ConfigFile{
@@ -117,19 +140,33 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error)
options.SkipInterpolation = true
})
if err != nil {
return false, err
return err
}
for key := range composeConfig.Services {
service := composeConfig.Services[key]
for _, volume := range service.Volumes {
if volume.Type == "bind" {
return false, nil
if !settings.AllowBindMountsForRegularUsers {
for _, volume := range service.Volumes {
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")
}
}
return true, nil
return nil
}
func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {

View File

@@ -22,6 +22,10 @@ func (handler *Handler) userDelete(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
}
if userID == 1 {
return &httperror.HandlerError{http.StatusForbidden, "This feature is not available in the demo version of Portainer", portainer.ErrNotAvailableInDemo}
}
if tokenData.ID == portainer.UserID(userID) {
return &httperror.HandlerError{http.StatusForbidden, "Cannot remove your own user account. Contact another administrator", portainer.ErrAdminCannotRemoveSelf}
}

View File

@@ -29,6 +29,10 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err}
}
if userID == 1 {
return &httperror.HandlerError{http.StatusForbidden, "This feature is not available in the demo version of Portainer", portainer.ErrNotAvailableInDemo}
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}

View File

@@ -33,6 +33,10 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques
return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err}
}
if userID == 1 {
return &httperror.HandlerError{http.StatusForbidden, "This feature is not available in the demo version of Portainer", portainer.ErrNotAvailableInDemo}
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}

View File

@@ -1,12 +1,17 @@
package docker
import (
"bytes"
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"github.com/docker/docker/client"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
"github.com/portainer/portainer/api/http/security"
)
const (
@@ -147,3 +152,88 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB
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"`
} `json:"HostConfig"`
}
forbiddenResponse := &http.Response{
StatusCode: http.StatusForbidden,
}
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return nil, err
}
user, err := transport.userService.User(tokenData.ID)
if err != nil {
return nil, err
}
rbacExtension, err := transport.extensionService.Extension(portainer.RBACExtension)
if err != nil && err != portainer.ErrObjectNotFound {
return nil, err
}
endpointResourceAccess := false
_, ok := user.EndpointAuthorizations[portainer.EndpointID(transport.endpoint.ID)][portainer.EndpointResourcesAccess]
if ok {
endpointResourceAccess = true
}
isAdmin := (rbacExtension != nil && endpointResourceAccess) || tokenData.Role == portainer.AdministratorRole
if !isAdmin {
settings, err := transport.settingsService.Settings()
if err != nil {
return nil, err
}
if !settings.AllowPrivilegedModeForRegularUsers ||
!settings.AllowHostNamespaceForRegularUsers ||
!settings.AllowDeviceMappingForRegularUsers {
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 partialContainer.HostConfig.Privileged {
return forbiddenResponse, errors.New("forbidden to use privileged mode")
}
if partialContainer.HostConfig.PidMode == "host" {
return forbiddenResponse, errors.New("forbidden to use pid host namespace")
}
if len(partialContainer.HostConfig.Devices) > 0 {
return nil, errors.New("forbidden to use device mapping")
}
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
}

View File

@@ -209,7 +209,7 @@ func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Res
func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/containers/create":
return transport.decorateGenericResourceCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
return transport.decorateContainerCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
case "/containers/prune":
return transport.administratorOperation(request)
@@ -656,6 +656,7 @@ func (transport *Transport) createRegistryAccessContext(request *http.Request) (
return nil, err
}
accessContext := &registryAccessContext{
isAdmin: true,
userID: tokenData.ID,

View File

@@ -3,6 +3,7 @@ package http
import (
"time"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/handler/edgegroups"
"github.com/portainer/portainer/api/http/handler/edgestacks"
"github.com/portainer/portainer/api/http/handler/edgetemplates"
@@ -338,8 +339,14 @@ func (server *Server) Start() error {
SchedulesHanlder: schedulesHandler,
}
if server.SSL {
return http.ListenAndServeTLS(server.BindAddress, server.SSLCert, server.SSLKey, server.Handler)
httpServer := &http.Server{
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()
}

View File

@@ -420,19 +420,22 @@ type (
// Settings represents the application settings
Settings struct {
LogoURL string `json:"LogoURL"`
BlackListedLabels []Pair `json:"BlackListedLabels"`
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
LDAPSettings LDAPSettings `json:"LDAPSettings"`
OAuthSettings OAuthSettings `json:"OAuthSettings"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
SnapshotInterval string `json:"SnapshotInterval"`
TemplatesURL string `json:"TemplatesURL"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
LogoURL string `json:"LogoURL"`
BlackListedLabels []Pair `json:"BlackListedLabels"`
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
LDAPSettings LDAPSettings `json:"LDAPSettings"`
OAuthSettings OAuthSettings `json:"OAuthSettings"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
SnapshotInterval string `json:"SnapshotInterval"`
TemplatesURL string `json:"TemplatesURL"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
AllowStackManagementForRegularUsers bool `json:"AllowStackManagementForRegularUsers"`
AllowHostNamespaceForRegularUsers bool `json:"AllowHostNamespaceForRegularUsers"`
AllowDeviceMappingForRegularUsers bool `json:"AllowDeviceMappingForRegularUsers"`
// Deprecated fields
DisplayDonationHeader bool
@@ -1007,9 +1010,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "1.24.0"
APIVersion = "1.24.1"
// DBVersion is the version number of the Portainer database
DBVersion = 23
DBVersion = 24
// AssetsServerURL represents the URL of the Portainer asset server
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved

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).
version: "1.24.0"
version: "1.24.1"
title: "Portainer API"
contact:
email: "info@portainer.io"
@@ -3174,7 +3174,7 @@ definitions:
description: "Is analytics enabled"
Version:
type: "string"
example: "1.24.0"
example: "1.24.1"
description: "Portainer API version"
PublicSettingsInspectResponse:
type: "object"

View File

@@ -1,5 +1,5 @@
{
"packageName": "portainer",
"packageVersion": "1.24.0",
"packageVersion": "1.24.1",
"projectName": "portainer"
}

View File

@@ -1,81 +1,82 @@
import _ from 'lodash-es';
angular.module('portainer.docker')
.controller('ContainerNetworksDatatableController', ['$scope', '$controller', 'DatatableService',
function ($scope, $controller, DatatableService) {
angular.module('portainer.docker').controller('ContainerNetworksDatatableController', [
'$scope',
'$controller',
'DatatableService',
function ($scope, $controller, DatatableService) {
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.state = Object.assign(this.state, {
expandedItems: [],
expandAll: true,
});
angular.extend(this, $controller('GenericDatatableController', { $scope: $scope }));
this.state = Object.assign(this.state, {
expandedItems: [],
expandAll: true
});
this.expandItem = function (item, expanded) {
if (!this.itemCanExpand(item)) {
return;
}
item.Expanded = expanded;
if (!expanded) {
item.Highlighted = false;
}
if (!item.Expanded) {
this.state.expandAll = false;
}
};
this.itemCanExpand = function (item) {
return item.GlobalIPv6Address !== '';
this.expandItem = function (item, expanded) {
if (!this.itemCanExpand(item)) {
return;
}
this.hasExpandableItems = function () {
return _.filter(this.dataset, (item) => this.itemCanExpand(item)).length;
};
item.Expanded = expanded;
if (!expanded) {
item.Highlighted = false;
}
if (!item.Expanded) {
this.state.expandAll = false;
}
};
this.expandAll = function () {
this.state.expandAll = !this.state.expandAll;
_.forEach(this.dataset, (item) => {
if (this.itemCanExpand(item)) {
this.expandItem(item, this.state.expandAll);
}
});
};
this.itemCanExpand = function (item) {
return item.GlobalIPv6Address !== '';
};
this.$onInit = function () {
this.setDefaults();
this.prepareTableFromDataset();
this.hasExpandableItems = function () {
return _.filter(this.dataset, (item) => this.itemCanExpand(item)).length;
};
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
this.expandAll = function () {
this.state.expandAll = !this.state.expandAll;
_.forEach(this.dataset, (item) => {
if (this.itemCanExpand(item)) {
this.expandItem(item, this.state.expandAll);
}
});
};
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
this.$onInit = function () {
this.setDefaults();
this.prepareTableFromDataset();
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
this.state.orderBy = this.orderBy;
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
if (storedOrder !== null) {
this.state.reverseOrder = storedOrder.reverse;
this.state.orderBy = storedOrder.orderBy;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
if (textFilter !== null) {
this.state.textFilter = textFilter;
this.onTextFilterChange();
}
_.forEach(this.dataset, (item) => {
item.Expanded = true;
item.Highlighted = true;
});
};
}
]);
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
if (storedFilters !== null) {
this.filters = storedFilters;
}
if (this.filters && this.filters.state) {
this.filters.state.open = false;
}
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
if (storedSettings !== null) {
this.settings = storedSettings;
this.settings.open = false;
}
_.forEach(this.dataset, (item) => {
item.Expanded = true;
item.Highlighted = true;
});
};
},
]);

View File

@@ -120,11 +120,11 @@
</div>
<div class="menuContent">
<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>
</div>
<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>
</div>
</div>

View File

@@ -93,11 +93,11 @@
</div>
<div class="menuContent">
<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>
</div>
<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>
</div>
</div>

View File

@@ -6,5 +6,6 @@ angular.module('portainer.docker').component('dockerSidebarContent', {
standaloneManagement: '<',
adminAccess: '<',
offlineMode: '<',
showStacks: '<',
},
});

View File

@@ -4,7 +4,7 @@
<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>
</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>
</li>
<li class="sidebar-list" ng-if="$ctrl.swarmManagement">

View File

@@ -38,7 +38,14 @@
<div class="form-group col-md-12">
<label for="nfs_mountpoint" class="col-sm-2 col-md-1 control-label text-left">Mount point</label>
<div class="col-sm-10 col-md-11">
<input type="text" class="form-control" ng-model="$ctrl.data.mountPoint" name="nfs_mountpoint" placeholder="e.g. /export/share, :/export/share, /share or :/share" required />
<input
type="text"
class="form-control"
ng-model="$ctrl.data.mountPoint"
name="nfs_mountpoint"
placeholder="e.g. /export/share, :/export/share, /share or :/share"
required
/>
</div>
</div>
<div class="form-group col-md-12" ng-show="nfsInformationForm.nfs_mountpoint.$invalid">

View File

@@ -1,4 +1,4 @@
import { ContainerDetailsViewModel, ContainerViewModel, ContainerStatsViewModel } from '../models/container';
import { ContainerDetailsViewModel, ContainerStatsViewModel, ContainerViewModel } from '../models/container';
angular.module('portainer.docker').factory('ContainerService', [
'$q',

View File

@@ -30,6 +30,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
'SettingsService',
'PluginService',
'HttpRequestHelper',
'ExtensionService',
function (
$q,
$scope,
@@ -55,7 +56,8 @@ angular.module('portainer.docker').controller('CreateContainerController', [
SystemService,
SettingsService,
PluginService,
HttpRequestHelper
HttpRequestHelper,
ExtensionService
) {
$scope.create = create;
@@ -603,7 +605,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
});
}
function initView() {
async function initView() {
var nodeName = $transition$.params().nodeName;
$scope.formValues.NodeName = nodeName;
HttpRequestHelper.setPortainerAgentTargetHeader(nodeName);
@@ -682,6 +684,7 @@ angular.module('portainer.docker').controller('CreateContainerController', [
});
$scope.isAdmin = Authentication.isAdmin();
$scope.showDeviceMapping = await shouldShowDevices();
}
function validateForm(accessControlData, isAdmin) {
@@ -894,6 +897,19 @@ angular.module('portainer.docker').controller('CreateContainerController', [
}
}
async function shouldShowDevices() {
const isAdmin = !$scope.applicationState.application.authentication || Authentication.isAdmin();
const { allowDeviceMappingForRegularUsers } = $scope.applicationState.application;
if (isAdmin || allowDeviceMappingForRegularUsers) {
return true;
}
const rbacEnabled = await ExtensionService.extensionEnabled(ExtensionService.EXTENSIONS.RBAC);
if (rbacEnabled) {
return Authentication.hasAuthorizations(['EndpointResourcesAccess']);
}
}
initView();
},
]);

View File

@@ -629,7 +629,7 @@
</form>
<form class="form-horizontal" style="margin-top: 15px;">
<!-- devices -->
<div class="form-group">
<div ng-if="showDeviceMapping" class="form-group">
<div class="col-sm-12" style="margin-top: 5px;">
<label class="control-label text-left">Devices</label>
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="addDevice()">

View File

@@ -21,6 +21,7 @@ angular.module('portainer.docker').controller('ContainerController', [
'ImageService',
'HttpRequestHelper',
'Authentication',
'StateManager',
function (
$q,
$scope,
@@ -40,7 +41,8 @@ angular.module('portainer.docker').controller('ContainerController', [
RegistryService,
ImageService,
HttpRequestHelper,
Authentication
Authentication,
StateManager
) {
$scope.activityTime = 0;
$scope.portBindings = [];
@@ -94,9 +96,13 @@ angular.module('portainer.docker').controller('ContainerController', [
const inSwarm = $scope.container.Config.Labels['com.docker.swarm.service.id'];
const autoRemove = $scope.container.HostConfig.AutoRemove;
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) => {
$scope.displayRecreateButton = !inSwarm && !autoRemove && (rbacEnabled ? admin : true);
$scope.displayRecreateButton = !inSwarm && !autoRemove && (settingRestrictsRegularUsers || rbacEnabled ? admin : true);
});
})
.catch(function error(err) {

View File

@@ -81,7 +81,7 @@
</div>
<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">
<rd-widget>
<rd-widget-body>

View File

@@ -1,6 +1,7 @@
angular.module('portainer.docker').controller('DashboardController', [
'$scope',
'$q',
'Authentication',
'ContainerService',
'ImageService',
'NetworkService',
@@ -11,10 +12,12 @@ angular.module('portainer.docker').controller('DashboardController', [
'EndpointService',
'Notifications',
'EndpointProvider',
'ExtensionService',
'StateManager',
function (
$scope,
$q,
Authentication,
ContainerService,
ImageService,
NetworkService,
@@ -25,6 +28,7 @@ angular.module('portainer.docker').controller('DashboardController', [
EndpointService,
Notifications,
EndpointProvider,
ExtensionService,
StateManager
) {
$scope.dismissInformationPanel = function (id) {
@@ -32,11 +36,14 @@ angular.module('portainer.docker').controller('DashboardController', [
};
$scope.offlineMode = false;
$scope.showStacks = false;
function initView() {
async function initView() {
var endpointMode = $scope.applicationState.endpoint.mode;
var endpointId = EndpointProvider.endpointID();
$scope.showStacks = await shouldShowStacks();
$q.all({
containers: ContainerService.containers(1),
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();
},
]);

View File

@@ -99,7 +99,6 @@
<td colspan="2">
<p class="small text-muted" authorization="DockerServiceUpdate">
Note: you can only rollback one level of changes. Clicking the rollback button without making a new change will undo your previous rollback </p
><p>
<a
authorization="DockerServiceLogs"

View File

@@ -72,9 +72,7 @@
<input type="checkbox" name="useNFS" ng-model="formValues.NFSData.useNFS" ng-click="formValues.CIFSData.useCIFS = false" />
<i></i>
</label>
<div ng-if="formValues.NFSData.useNFS" class="small text-muted" style="margin-top: 10px;">
Ensure <code>nfs-utils</code> are installed on your hosts.
</div>
<div ng-if="formValues.NFSData.useNFS" class="small text-muted" style="margin-top: 10px;"> Ensure <code>nfs-utils</code> are installed on your hosts. </div>
</div>
<volumes-nfs-form data="formValues.NFSData" ng-show="formValues.Driver === 'local'"></volumes-nfs-form>
<!-- !nfs-management -->
@@ -87,9 +85,7 @@
<input type="checkbox" name="useCIFS" ng-model="formValues.CIFSData.useCIFS" ng-click="formValues.NFSData.useNFS = false" />
<i></i>
</label>
<div ng-if="formValues.CIFSData.useCIFS" class="small text-muted" style="margin-top: 10px;">
Ensure <code>cifs-utils</code> are installed on your hosts.
</div>
<div ng-if="formValues.CIFSData.useCIFS" class="small text-muted" style="margin-top: 10px;"> Ensure <code>cifs-utils</code> are installed on your hosts. </div>
</div>
<volumes-cifs-form data="formValues.CIFSData" ng-show="formValues.Driver === 'local'"></volumes-cifs-form>
<!-- !cifs-management -->

View File

@@ -1,16 +1,10 @@
<ui-select
multiple
ng-model="$ctrl.model"
close-on-select="false"
>
<ui-select multiple ng-model="$ctrl.model" close-on-select="false">
<ui-select-match placeholder="Select one or multiple group(s)">
<span>
{{ $item.Name }}
</span>
</ui-select-match>
<ui-select-choices
repeat="item.Id as item in $ctrl.items | filter: { Name: $select.search }"
>
<ui-select-choices repeat="item.Id as item in $ctrl.items | filter: { Name: $select.search }">
<span>
{{ item.Name }}
</span>

View File

@@ -2,6 +2,6 @@ angular.module('portainer.edge').component('edgeGroupsSelector', {
templateUrl: './edge-groups-selector.html',
bindings: {
model: '=',
items: '<'
}
items: '<',
},
});

View File

@@ -1,8 +1,4 @@
import {
RoleViewModel,
// EndpointRoleCreateRequest,
// EndpointRoleUpdateRequest
} from '../models/role';
import { RoleViewModel } from '../models/role';
angular.module('portainer.extensions.rbac').factory('RoleService', [
'$q',

View File

@@ -1,4 +1,5 @@
import _ from 'lodash-es';
export function RegistryRepositoryViewModel(item) {
if (item.name && item.tags) {
this.Name = item.name;

View File

@@ -42,7 +42,8 @@ angular.module('portainer.extensions.registrymanagement').factory('RegistryGitla
async function _getRepositoriesPage(params, repositories) {
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) {
params.page = response.next;
repositories = await _getRepositoriesPage(params, repositories);

View File

@@ -1,6 +1,5 @@
import _ from 'lodash-es';
import { RepositoryShortTag } from '../models/repositoryTag';
import { RepositoryAddTagPayload } from '../models/repositoryTag';
import { RepositoryAddTagPayload, RepositoryShortTag } from '../models/repositoryTag';
import { RegistryRepositoryViewModel } from '../models/registryRepository';
import genericAsyncGenerator from './genericAsyncGenerator';

View File

@@ -1,5 +1,5 @@
import _ from 'lodash-es';
import { RepositoryTagViewModel, RepositoryShortTag } from '../../../models/repositoryTag';
import { RepositoryShortTag, RepositoryTagViewModel } from '../../../models/repositoryTag';
angular.module('portainer.app').controller('RegistryRepositoryController', [
'$q',

View File

@@ -54,8 +54,8 @@ angular.module('portainer.extensions.registrymanagement').controller('RegistryRe
.then(function success() {
return RegistryServiceSelector.repositories($scope.registry);
})
.then(function success(data) {
$scope.repositories = data;
.then(function success(repositories) {
$scope.repositories = repositories;
})
.catch(function error() {
$scope.state.displayInvalidConfigurationMessage = true;

View File

@@ -1,4 +1,4 @@
import { StoridgeNodeModel, StoridgeNodeDetailedModel } from '../models/node';
import { StoridgeNodeDetailedModel, StoridgeNodeModel } from '../models/node';
angular.module('portainer.integrations.storidge').factory('StoridgeNodeService', [
'$q',

View File

@@ -1,6 +1,6 @@
import _ from 'lodash-es';
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
import { ResourceControlTypeString as RCTS, ResourceControlTypeInt as RCTI } from 'Portainer/models/resourceControl/resourceControlTypes';
import { ResourceControlTypeInt as RCTI, ResourceControlTypeString as RCTS } from 'Portainer/models/resourceControl/resourceControlTypes';
import { AccessControlPanelData } from './porAccessControlPanelModel';
angular.module('portainer.app').controller('porAccessControlPanelController', [

View File

@@ -42,12 +42,11 @@ angular.module('portainer.app').controller('GenericDatatableController', [
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
};
this.changeOrderBy = changeOrderBy.bind(this);
function changeOrderBy(orderField) {
this.changeOrderBy = function changeOrderBy(orderField) {
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
this.state.orderBy = orderField;
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
}
};
this.selectItem = function (item, event) {
// Handle range select using shift

View File

@@ -2,7 +2,7 @@
<rd-widget>
<rd-widget-body classes="no-padding">
<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">
<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>
@@ -52,7 +52,7 @@
>
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</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
</button>
</div>
@@ -144,7 +144,10 @@
</table>
</div>
<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">
<form class="form-inline">
<span class="limitSelector">

View File

@@ -12,5 +12,6 @@ angular.module('portainer.app').component('stacksDatatable', {
removeAction: '<',
offlineMode: '<',
refreshCallback: '<',
createEnabled: '<',
},
});

View File

@@ -5,11 +5,7 @@
<span ng-if="$item.TagIds.length"> - <i class="fa fa-tags"></i> {{ $ctrl.tagIdsToTagNames($item.TagIds) | arraytostr }}</span>
</span>
</ui-select-match>
<ui-select-choices
group-by="$ctrl.groupEndpoints"
group-filter="$ctrl.sortGroups"
repeat="endpoint.Id as endpoint in $ctrl.endpoints | filter: { Name: $select.search }"
>
<ui-select-choices group-by="$ctrl.groupEndpoints" group-filter="$ctrl.sortGroups" repeat="endpoint.Id as endpoint in $ctrl.endpoints | filter: { Name: $select.search }">
<span>
{{ endpoint.Name }}
<span ng-if="endpoint.TagIds.length"> - <i class="fa fa-tags"></i> {{ $ctrl.tagIdsToTagNames(endpoint.TagIds) | arraytostr }}</span>

View File

@@ -1,5 +1,5 @@
import _ from 'lodash-es';
import { createStatus } from '../../docker/models/container';
import {createStatus} from '../../docker/models/container';
export function ScheduleDefaultModel() {
this.Name = '';

View File

@@ -13,6 +13,9 @@ export function SettingsViewModel(data) {
this.EnableHostManagementFeatures = data.EnableHostManagementFeatures;
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures;
this.AllowStackManagementForRegularUsers = data.AllowStackManagementForRegularUsers;
this.AllowHostNamespaceForRegularUsers = data.AllowHostNamespaceForRegularUsers;
this.AllowDeviceMappingForRegularUsers = data.AllowDeviceMappingForRegularUsers;
}
export function PublicSettingsViewModel(settings) {
@@ -25,6 +28,9 @@ export function PublicSettingsViewModel(settings) {
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
this.LogoURL = settings.LogoURL;
this.OAuthLoginURI = settings.OAuthLoginURI;
this.AllowStackManagementForRegularUsers = settings.AllowStackManagementForRegularUsers;
this.AllowDeviceMappingForRegularUsers = settings.AllowDeviceMappingForRegularUsers;
this.AllowHostNamespaceForRegularUsers = settings.AllowHostNamespaceForRegularUsers;
}
export function LDAPSettingsViewModel(data) {

View File

@@ -1,6 +1,5 @@
import _ from 'lodash-es';
import { UserAccessViewModel } from '../../models/access';
import { TeamAccessViewModel } from '../../models/access';
import { TeamAccessViewModel, UserAccessViewModel } from '../../models/access';
angular.module('portainer.app').factory('AccessService', [
'$q',

View File

@@ -1,7 +1,7 @@
import _ from 'lodash-es';
import { PorImageRegistryModel } from 'Docker/models/porImageRegistry';
import { RegistryTypes } from 'Extensions/registry-management/models/registryTypes';
import { RegistryViewModel, RegistryCreateRequest } from '../../models/registry';
import { RegistryCreateRequest, RegistryViewModel } from '../../models/registry';
angular.module('portainer.app').factory('RegistryService', [
'$q',

View File

@@ -1,4 +1,4 @@
import { ScheduleModel, ScheduleCreateRequest, ScheduleUpdateRequest, ScriptExecutionTaskModel } from '../../models/schedule';
import { ScheduleCreateRequest, ScheduleModel, ScheduleUpdateRequest, ScriptExecutionTaskModel } from '../../models/schedule';
angular.module('portainer.app').factory('ScheduleService', [
'$q',

View File

@@ -1,4 +1,4 @@
import { SettingsViewModel, PublicSettingsViewModel } from '../../models/settings';
import { PublicSettingsViewModel, SettingsViewModel } from '../../models/settings';
angular.module('portainer.app').factory('SettingsService', [
'$q',

View File

@@ -1,5 +1,5 @@
import _ from 'lodash-es';
import { StackViewModel, ExternalStackViewModel } from '../../models/stack';
import { ExternalStackViewModel, StackViewModel } from '../../models/stack';
angular.module('portainer.app').factory('StackService', [
'$q',

View File

@@ -1,4 +1,4 @@
import { TemplateViewModel, TemplateCreateRequest, TemplateUpdateRequest } from '../../models/template';
import { TemplateCreateRequest, TemplateUpdateRequest, TemplateViewModel } from '../../models/template';
angular.module('portainer.app').factory('TemplateService', [
'$q',

View File

@@ -37,7 +37,7 @@ angular.module('portainer.app').factory('Authentication', [
function logout() {
StateManager.clean();
EndpointProvider.clean();
LocalStorage.clean();
LocalStorage.cleanAuthData();
LocalStorage.storeLoginStateUUID('');
}

View File

@@ -24,6 +24,7 @@ angular.module('portainer.app').factory('EndpointProvider', [
};
service.clean = function () {
LocalStorage.cleanEndpointData();
endpoint = {};
};

View File

@@ -91,12 +91,11 @@ angular.module('portainer.app').factory('FileUploadService', [
data: {
file: file,
Name: stackName,
EdgeGroups: Upload.json(edgeGroups)
EdgeGroups: Upload.json(edgeGroups),
},
ignoreLoadingBar: true
ignoreLoadingBar: true,
});
};
service.configureRegistry = function (registryId, registryManagementConfigurationModel) {
return Upload.upload({

View File

@@ -1,7 +1,6 @@
angular.module('portainer.app').factory('LocalStorage', [
'localStorageService',
function LocalStorageFactory(localStorageService) {
'use strict';
return {
storeEndpointID: function (id) {
localStorageService.set('ENDPOINT_ID', id);
@@ -16,10 +15,10 @@ angular.module('portainer.app').factory('LocalStorage', [
return localStorageService.get('ENDPOINT_PUBLIC_URL');
},
storeLoginStateUUID: function (uuid) {
localStorageService.cookie.set('LOGIN_STATE_UUID', uuid);
localStorageService.set('LOGIN_STATE_UUID', uuid);
},
getLoginStateUUID: function () {
return localStorageService.cookie.get('LOGIN_STATE_UUID');
return localStorageService.get('LOGIN_STATE_UUID');
},
storeOfflineMode: function (isOffline) {
localStorageService.set('ENDPOINT_OFFLINE_MODE', isOffline);
@@ -46,10 +45,10 @@ angular.module('portainer.app').factory('LocalStorage', [
return localStorageService.get('APPLICATION_STATE');
},
storeUIState: function (state) {
localStorageService.cookie.set('UI_STATE', state);
localStorageService.set('UI_STATE', state);
},
getUIState: function () {
return localStorageService.cookie.get('UI_STATE');
return localStorageService.get('UI_STATE');
},
storeExtensionState: function (state) {
localStorageService.set('EXTENSION_STATE', state);
@@ -67,40 +66,40 @@ angular.module('portainer.app').factory('LocalStorage', [
localStorageService.remove('JWT');
},
storePaginationLimit: function (key, count) {
localStorageService.cookie.set('datatable_pagination_' + key, count);
localStorageService.set('datatable_pagination_' + key, count);
},
getPaginationLimit: function (key) {
return localStorageService.cookie.get('datatable_pagination_' + key);
return localStorageService.get('datatable_pagination_' + key);
},
getDataTableOrder: function (key) {
return localStorageService.cookie.get('datatable_order_' + key);
return localStorageService.get('datatable_order_' + key);
},
storeDataTableOrder: function (key, data) {
localStorageService.cookie.set('datatable_order_' + key, data);
localStorageService.set('datatable_order_' + key, data);
},
getDataTableTextFilters: function (key) {
return localStorageService.cookie.get('datatable_text_filter_' + key);
return localStorageService.get('datatable_text_filter_' + key);
},
storeDataTableTextFilters: function (key, data) {
localStorageService.cookie.set('datatable_text_filter_' + key, data);
localStorageService.set('datatable_text_filter_' + key, data);
},
getDataTableFilters: function (key) {
return localStorageService.cookie.get('datatable_filters_' + key);
return localStorageService.get('datatable_filters_' + key);
},
storeDataTableFilters: function (key, data) {
localStorageService.cookie.set('datatable_filters_' + key, data);
localStorageService.set('datatable_filters_' + key, data);
},
getDataTableSettings: function (key) {
return localStorageService.cookie.get('datatable_settings_' + key);
return localStorageService.get('datatable_settings_' + key);
},
storeDataTableSettings: function (key, data) {
localStorageService.cookie.set('datatable_settings_' + key, data);
localStorageService.set('datatable_settings_' + key, data);
},
getDataTableExpandedItems: function (key) {
return localStorageService.cookie.get('datatable_expandeditems_' + key);
return localStorageService.get('datatable_expandeditems_' + key);
},
storeDataTableExpandedItems: function (key, data) {
localStorageService.cookie.set('datatable_expandeditems_' + key, data);
localStorageService.set('datatable_expandeditems_' + key, data);
},
getDataTableSelectedItems: function (key) {
return localStorageService.get('datatable_selecteditems_' + key);
@@ -109,16 +108,16 @@ angular.module('portainer.app').factory('LocalStorage', [
localStorageService.set('datatable_selecteditems_' + key, data);
},
storeSwarmVisualizerSettings: function (key, data) {
localStorageService.cookie.set('swarmvisualizer_' + key, data);
localStorageService.set('swarmvisualizer_' + key, data);
},
getSwarmVisualizerSettings: function (key) {
return localStorageService.cookie.get('swarmvisualizer_' + key);
return localStorageService.get('swarmvisualizer_' + key);
},
storeColumnVisibilitySettings: function (key, data) {
localStorageService.cookie.set('col_visibility_' + key, data);
localStorageService.set('col_visibility_' + key, data);
},
getColumnVisibilitySettings: function (key) {
return localStorageService.cookie.get('col_visibility_' + key);
return localStorageService.get('col_visibility_' + key);
},
storeJobImage: function (data) {
localStorageService.set('job_image', data);
@@ -126,12 +125,24 @@ angular.module('portainer.app').factory('LocalStorage', [
getJobImage: function () {
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),
getLogoutReason: () => localStorageService.get('logout_reason'),
cleanLogoutReason: () => localStorageService.remove('logout_reason'),
clean: function () {
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');
},
};
},
]);

View File

@@ -76,6 +76,31 @@ angular.module('portainer.app').factory('StateManager', [
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);
};
function assignStateFromStatusAndSettings(status, settings) {
state.application.authentication = status.Authentication;
state.application.analytics = status.Analytics;
@@ -87,6 +112,11 @@ angular.module('portainer.app').factory('StateManager', [
state.application.enableHostManagementFeatures = settings.EnableHostManagementFeatures;
state.application.enableVolumeBrowserForNonAdminUsers = settings.AllowVolumeBrowserForRegularUsers;
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.validity = moment().unix();
}

View File

@@ -3,6 +3,17 @@
<rd-header-content>User settings</rd-header-content>
</rd-header>
<div class="row" ng-if="UserId === 1">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-exclamation-triangle" title="Feature not available"> </rd-widget-header>
<rd-widget-body>
<span class="small text-muted">You cannot change the password of this account in the demo version of Portainer.</span>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>

View File

@@ -36,10 +36,10 @@
<div style="margin-top: 10px;">
<uib-tabset active="state.deploymentTab">
<uib-tab index="0" heading="Standalone">
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{dockerCommands.standalone}}</code>
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.standalone }}</code>
</uib-tab>
<uib-tab index="1" heading="Swarm">
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{dockerCommands.swarm}}</code>
<code style="display: block; white-space: pre-wrap; padding: 16px 90px;">{{ dockerCommands.swarm }}</code>
</uib-tab>
</uib-tabset>
<div style="margin-top: 10px;">

View File

@@ -102,8 +102,10 @@
button-spinner="state.actionInProgress"
style="margin-left: 0px;"
>
<span ng-hide="state.actionInProgress">Enable extension</span>
<span ng-show="state.actionInProgress">Enabling extension...</span>
<span ng-hide="state.actionInProgress" ng-if="!state.updateLicense">Enable 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>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import moment from 'moment';
import _ from 'lodash-es';
angular.module('portainer.app').controller('ExtensionsController', [
'$scope',
@@ -9,6 +10,7 @@ angular.module('portainer.app').controller('ExtensionsController', [
$scope.state = {
actionInProgress: false,
currentDate: moment().format('YYYY-MM-dd'),
updateLicense: false,
};
$scope.formValues = {
@@ -59,6 +61,15 @@ angular.module('portainer.app').controller('ExtensionsController', [
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);
};

View File

@@ -1,9 +1,9 @@
angular.module('portainer.app').controller('MainController', [
'$scope',
'$cookieStore',
'LocalStorage',
'StateManager',
'EndpointProvider',
function ($scope, $cookieStore, StateManager, EndpointProvider) {
function ($scope, LocalStorage, StateManager, EndpointProvider) {
/**
* Sidebar Toggle & Cookie Control
*/
@@ -17,11 +17,8 @@ angular.module('portainer.app').controller('MainController', [
$scope.$watch($scope.getWidth, function (newValue) {
if (newValue >= mobileView) {
if (angular.isDefined($cookieStore.get('toggle'))) {
$scope.toggle = !$cookieStore.get('toggle') ? false : true;
} else {
$scope.toggle = true;
}
const toggleValue = LocalStorage.getToolbarToggle();
$scope.toggle = typeof toggleValue === 'boolean' ? toggleValue : true;
} else {
$scope.toggle = false;
}
@@ -29,7 +26,7 @@ angular.module('portainer.app').controller('MainController', [
$scope.toggleSidebar = function () {
$scope.toggle = !$scope.toggle;
$cookieStore.put('toggle', $scope.toggle);
LocalStorage.storeToolbarToggle($scope.toggle);
};
window.onresize = function () {

View File

@@ -3,6 +3,17 @@
<rd-header-content>Settings</rd-header-content>
</rd-header>
<div class="row" ng-if="UserId === 1">
<div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget>
<rd-widget-header icon="fa-exclamation-triangle" title="Feature not available"> </rd-widget-header>
<rd-widget-body>
<span class="small text-muted">You cannot change the password of this account in the demo version of Portainer.</span>
</rd-widget-body>
</rd-widget>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<rd-widget>
@@ -23,7 +34,10 @@
<label for="toggle_logo" class="control-label text-left">
Use custom logo
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo" /><i></i> </label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled name="toggle_logo" ng-model="formValues.customLogo" /><i></i> </label>
</div>
<div class="col-sm-12">
<span class="small text-muted">You cannot use this feature in the demo version of Portainer.</span>
</div>
</div>
<div ng-if="formValues.customLogo">
@@ -120,6 +134,44 @@
</label>
</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" 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 -->
<!-- edge -->
<div class="col-sm-12 form-section-title">

View File

@@ -33,6 +33,14 @@ angular.module('portainer.app').controller('SettingsController', [
enableHostManagementFeatures: false,
enableVolumeBrowser: false,
enableEdgeComputeFeatures: false,
allowStackManagementForRegularUsers: false,
restrictHostNamespaceForRegularUsers: false,
allowDeviceMappingForRegularUsers: false,
};
$scope.isContainerEditDisabled = function isContainerEditDisabled() {
const { restrictBindMounts, restrictHostNamespaceForRegularUsers, restrictPrivilegedMode, disableDeviceMappingForRegularUsers } = this.formValues;
return restrictBindMounts || restrictHostNamespaceForRegularUsers || restrictPrivilegedMode || disableDeviceMappingForRegularUsers;
};
$scope.removeFilteredContainerLabel = function (index) {
@@ -69,6 +77,9 @@ angular.module('portainer.app').controller('SettingsController', [
settings.AllowVolumeBrowserForRegularUsers = $scope.formValues.enableVolumeBrowser;
settings.EnableHostManagementFeatures = $scope.formValues.enableHostManagementFeatures;
settings.EnableEdgeComputeFeatures = $scope.formValues.enableEdgeComputeFeatures;
settings.AllowStackManagementForRegularUsers = !$scope.formValues.disableStackManagementForRegularUsers;
settings.AllowHostNamespaceForRegularUsers = !$scope.formValues.restrictHostNamespaceForRegularUsers;
settings.AllowDeviceMappingForRegularUsers = !$scope.formValues.disableDeviceMappingForRegularUsers;
$scope.state.actionInProgress = true;
updateSettings(settings);
@@ -83,6 +94,11 @@ angular.module('portainer.app').controller('SettingsController', [
StateManager.updateEnableHostManagementFeatures(settings.EnableHostManagementFeatures);
StateManager.updateEnableVolumeBrowserForNonAdminUsers(settings.AllowVolumeBrowserForRegularUsers);
StateManager.updateEnableEdgeComputeFeatures(settings.EnableEdgeComputeFeatures);
StateManager.updateAllowStackManagementForRegularUsers(settings.AllowStackManagementForRegularUsers);
StateManager.updateAllowHostNamespaceForRegularUsers(settings.AllowHostNamespaceForRegularUsers);
StateManager.updateAllowDeviceMappingForRegularUsers(settings.AllowDeviceMappingForRegularUsers);
StateManager.updateAllowPrivilegedModeForRegularUsers(settings.AllowPrivilegedModeForRegularUsers);
StateManager.updateAllowBindMountsForRegularUsers(settings.AllowBindMountsForRegularUsers);
$state.reload();
})
.catch(function error(err) {
@@ -109,6 +125,9 @@ angular.module('portainer.app').controller('SettingsController', [
$scope.formValues.enableVolumeBrowser = settings.AllowVolumeBrowserForRegularUsers;
$scope.formValues.enableHostManagementFeatures = settings.EnableHostManagementFeatures;
$scope.formValues.enableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
$scope.formValues.disableStackManagementForRegularUsers = !settings.AllowStackManagementForRegularUsers;
$scope.formValues.restrictHostNamespaceForRegularUsers = !settings.AllowHostNamespaceForRegularUsers;
$scope.formValues.disableDeviceMappingForRegularUsers = !settings.AllowDeviceMappingForRegularUsers;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings');

View File

@@ -21,6 +21,7 @@
standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"
admin-access="!applicationState.application.authentication || isAdmin"
offline-mode="endpointState.OfflineMode"
show-stacks="showStacks"
></docker-sidebar-content>
<li class="sidebar-title" authorization="IntegrationStoridgeAdmin" ng-if="applicationState.endpoint.mode && applicationState.endpoint.extensions.length > 0">
<span>Integrations</span>

View File

@@ -1,11 +1,13 @@
angular.module('portainer.app').controller('SidebarController', [
'$q',
'$scope',
'$transitions',
'StateManager',
'Notifications',
'Authentication',
'UserService',
function ($q, $scope, StateManager, Notifications, Authentication, UserService) {
'ExtensionService',
function ($q, $scope, $transitions, StateManager, Notifications, Authentication, UserService, ExtensionService) {
function checkPermissions(memberships) {
var isLeader = false;
angular.forEach(memberships, function (membership) {
@@ -16,9 +18,10 @@ angular.module('portainer.app').controller('SidebarController', [
$scope.isTeamLeader = isLeader;
}
function initView() {
async function initView() {
$scope.uiVersion = StateManager.getState().application.version;
$scope.logo = StateManager.getState().application.logo;
$scope.showStacks = await shouldShowStacks();
var authenticationEnabled = $scope.applicationState.application.authentication;
if (authenticationEnabled) {
@@ -37,5 +40,24 @@ angular.module('portainer.app').controller('SidebarController', [
}
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();
});
},
]);

View File

@@ -19,6 +19,7 @@
show-ownership-column="applicationState.application.authentication"
offline-mode="offlineMode"
refresh-callback="getStacks"
create-enabled="createEnabled"
></stacks-datatable>
</div>
</div>

View File

@@ -1,65 +1,85 @@
angular.module('portainer.app').controller('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);
});
};
angular.module('portainer.app').controller('StacksController', StacksController);
function deleteSelectedStacks(stacks) {
var endpointId = EndpointProvider.endpointID();
var actionCount = stacks.length;
angular.forEach(stacks, function (stack) {
StackService.remove(stack, stack.External, endpointId)
.then(function success() {
Notifications.success('Stack successfully removed', stack.Name);
var index = $scope.stacks.indexOf(stack);
$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();
}
});
});
}
/* @ngInject */
function StacksController($scope, $state, Notifications, StackService, ModalService, EndpointProvider, Authentication, StateManager, ExtensionService) {
$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);
});
};
$scope.offlineMode = 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();
function deleteSelectedStacks(stacks) {
var endpointId = EndpointProvider.endpointID();
var actionCount = stacks.length;
angular.forEach(stacks, function (stack) {
StackService.remove(stack, stack.External, endpointId)
.then(function success() {
Notifications.success('Stack successfully removed', stack.Name);
var index = $scope.stacks.indexOf(stack);
$scope.stacks.splice(index, 1);
})
.catch(function error(err) {
$scope.stacks = [];
Notifications.error('Failure', err, 'Unable to retrieve stacks');
Notifications.error('Failure', err, 'Unable to remove stack ' + stack.Name);
})
.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() {
getStacks();
let isAdmin = true;
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();
}

View File

@@ -15,7 +15,6 @@ import 'angular-moment-picker/dist/angular-moment-picker.min.css';
import 'angular-multiselect/isteven-multi-select.css';
import angular from 'angular';
window.angular = angular;
import 'moment';
import '@uirouter/angularjs';
import 'ui-select';
@@ -39,3 +38,5 @@ import 'js-yaml/dist/js-yaml.js';
import 'angular-ui-bootstrap';
import 'angular-moment-picker';
import 'angular-multiselect/isteven-multi-select.js';
window.angular = angular;

View File

@@ -1,5 +1,5 @@
Name: portainer
Version: 1.24.0
Version: 1.24.1
Release: 0
License: Zlib
Summary: A lightweight docker management UI

View File

@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "1.24.0",
"version": "1.24.1",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"