mirror of https://github.com/portainer/portainer
feat(demo): disable features on demo env [EE-1874] (#6040)
parent
3791b7a16f
commit
12cddbd896
|
@ -35,6 +35,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
|||
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
|
||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
DemoEnvironment: kingpin.Flag("demo", "Demo environment").Bool(),
|
||||
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
|
||||
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
|
||||
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/portainer/portainer/api/database/boltdb"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/exec"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
|
@ -572,6 +573,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
openAMTService := openamt.NewService()
|
||||
|
||||
cryptoService := initCryptoService()
|
||||
|
||||
digitalSignatureService := initDigitalSignatureService()
|
||||
|
||||
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
|
||||
|
@ -634,6 +636,14 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
|
||||
applicationStatus := initStatus(instanceID)
|
||||
|
||||
demoService := demo.NewService()
|
||||
if *flags.DemoEnvironment {
|
||||
err := demoService.Init(dataStore, cryptoService)
|
||||
if err != nil {
|
||||
log.Fatalf("failed initializing demo environment: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = initEndpoint(flags, dataStore, snapshotService)
|
||||
if err != nil {
|
||||
logrus.Fatalf("Failed initializing environment: %v", err)
|
||||
|
@ -722,6 +732,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
|||
ShutdownCtx: shutdownCtx,
|
||||
ShutdownTrigger: shutdownTrigger,
|
||||
StackDeployer: stackDeployer,
|
||||
DemoService: demoService,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
type EnvironmentDetails struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Users []portainer.UserID `json:"users"`
|
||||
Environments []portainer.EndpointID `json:"environments"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
details EnvironmentDetails
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (service *Service) Details() EnvironmentDetails {
|
||||
return service.details
|
||||
}
|
||||
|
||||
func (service *Service) Init(store dataservices.DataStore, cryptoService portainer.CryptoService) error {
|
||||
log.Print("[INFO] [main] Starting demo environment")
|
||||
|
||||
isClean, err := isCleanStore(store)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed checking if store is clean")
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
return errors.New(" Demo environment can only be initialized on a clean database")
|
||||
}
|
||||
|
||||
id, err := initDemoUser(store, cryptoService)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed creating demo user")
|
||||
}
|
||||
|
||||
endpointIds, err := initDemoEndpoints(store)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed creating demo endpoint")
|
||||
}
|
||||
|
||||
err = initDemoSettings(store)
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed updating demo settings")
|
||||
}
|
||||
|
||||
service.details = EnvironmentDetails{
|
||||
Enabled: true,
|
||||
Users: []portainer.UserID{id},
|
||||
// endpoints 2,3 are created after deployment of portainer
|
||||
Environments: endpointIds,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isCleanStore(store dataservices.DataStore) (bool, error) {
|
||||
endpoints, err := store.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(endpoints) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
users, err := store.User().Users()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(users) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (service *Service) IsDemo() bool {
|
||||
return service.details.Enabled
|
||||
}
|
||||
|
||||
func (service *Service) IsDemoEnvironment(environmentID portainer.EndpointID) bool {
|
||||
if !service.IsDemo() {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, demoEndpointID := range service.details.Environments {
|
||||
if environmentID == demoEndpointID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (service *Service) IsDemoUser(userID portainer.UserID) bool {
|
||||
if !service.IsDemo() {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, demoUserID := range service.details.Users {
|
||||
if userID == demoUserID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package demo
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
func initDemoUser(
|
||||
store dataservices.DataStore,
|
||||
cryptoService portainer.CryptoService,
|
||||
) (portainer.UserID, error) {
|
||||
|
||||
password, err := cryptoService.Hash("tryportainer")
|
||||
if err != nil {
|
||||
return 0, errors.WithMessage(err, "failed creating password hash")
|
||||
}
|
||||
|
||||
admin := &portainer.User{
|
||||
Username: "admin",
|
||||
Password: password,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
|
||||
err = store.User().Create(admin)
|
||||
return admin.ID, errors.WithMessage(err, "failed creating user")
|
||||
}
|
||||
|
||||
func initDemoEndpoints(store dataservices.DataStore) ([]portainer.EndpointID, error) {
|
||||
localEndpointId, err := initDemoLocalEndpoint(store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// second and third endpoints are going to be created with docker-compose as a part of the demo environment set up.
|
||||
// ref: https://github.com/portainer/portainer-demo/blob/master/docker-compose.yml
|
||||
return []portainer.EndpointID{localEndpointId, localEndpointId + 1, localEndpointId + 2}, nil
|
||||
}
|
||||
|
||||
func initDemoLocalEndpoint(store dataservices.DataStore) (portainer.EndpointID, error) {
|
||||
id := portainer.EndpointID(store.Endpoint().GetNextIdentifier())
|
||||
localEndpoint := &portainer.Endpoint{
|
||||
ID: id,
|
||||
Name: "local",
|
||||
URL: "unix:///var/run/docker.sock",
|
||||
PublicURL: "demo.portainer.io",
|
||||
Type: portainer.DockerEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(1),
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||
TagIDs: []portainer.TagID{},
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
}
|
||||
|
||||
err := store.Endpoint().Create(localEndpoint)
|
||||
return id, errors.WithMessage(err, "failed creating local endpoint")
|
||||
}
|
||||
|
||||
func initDemoSettings(
|
||||
store dataservices.DataStore,
|
||||
) error {
|
||||
settings, err := store.Settings().Settings()
|
||||
if err != nil {
|
||||
return errors.WithMessage(err, "failed fetching settings")
|
||||
}
|
||||
|
||||
settings.EnableTelemetry = false
|
||||
settings.LogoURL = ""
|
||||
|
||||
err = store.Settings().UpdateSettings(settings)
|
||||
return errors.WithMessage(err, "failed updating settings")
|
||||
}
|
|
@ -9,4 +9,6 @@ var (
|
|||
ErrUnauthorized = errors.New("Unauthorized")
|
||||
// ErrResourceAccessDenied Access denied to resource error
|
||||
ErrResourceAccessDenied = errors.New("Access denied to resource")
|
||||
// ErrNotAvailableInDemo feature is not allowed in demo
|
||||
ErrNotAvailableInDemo = errors.New("This feature is not available in the demo version of Portainer")
|
||||
)
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/docker/docker/pkg/ioutils"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
i "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -49,7 +50,7 @@ func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T)
|
|||
gate := offlinegate.NewOfflineGate()
|
||||
adminMonitor := adminmonitor.New(time.Hour, nil, context.Background())
|
||||
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{}).backup(w, r)
|
||||
assert.Nil(t, handlerErr, "Handler should not fail")
|
||||
|
||||
response := w.Result()
|
||||
|
@ -86,7 +87,7 @@ func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *test
|
|||
gate := offlinegate.NewOfflineGate()
|
||||
adminMonitor := adminmonitor.New(time.Hour, nil, nil)
|
||||
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor).backup(w, r)
|
||||
handlerErr := NewHandler(nil, i.NewDatastore(), gate, "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{}).backup(w, r)
|
||||
assert.Nil(t, handlerErr, "Handler should not fail")
|
||||
|
||||
response := w.Result()
|
||||
|
|
|
@ -9,6 +9,8 @@ import (
|
|||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/middlewares"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
@ -25,7 +27,17 @@ type Handler struct {
|
|||
}
|
||||
|
||||
// NewHandler creates an new instance of backup handler
|
||||
func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataStore, gate *offlinegate.OfflineGate, filestorePath string, shutdownTrigger context.CancelFunc, adminMonitor *adminmonitor.Monitor) *Handler {
|
||||
func NewHandler(
|
||||
bouncer *security.RequestBouncer,
|
||||
dataStore dataservices.DataStore,
|
||||
gate *offlinegate.OfflineGate,
|
||||
filestorePath string,
|
||||
shutdownTrigger context.CancelFunc,
|
||||
adminMonitor *adminmonitor.Monitor,
|
||||
demoService *demo.Service,
|
||||
|
||||
) *Handler {
|
||||
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
bouncer: bouncer,
|
||||
|
@ -36,8 +48,11 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
|
|||
adminMonitor: adminMonitor,
|
||||
}
|
||||
|
||||
h.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
|
||||
h.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
|
||||
demoRestrictedRouter := h.NewRoute().Subrouter()
|
||||
demoRestrictedRouter.Use(middlewares.RestrictDemoEnv(demoService.IsDemo))
|
||||
|
||||
demoRestrictedRouter.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
|
||||
demoRestrictedRouter.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
@ -50,7 +65,7 @@ func adminAccess(next http.Handler) http.Handler {
|
|||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perfom the action", nil)
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "User is not authorized to perform the action", nil)
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/adminmonitor"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/offlinegate"
|
||||
i "github.com/portainer/portainer/api/internal/testhelpers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
@ -51,7 +52,7 @@ func Test_restoreArchive_usingCombinationOfPasswords(t *testing.T) {
|
|||
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}), i.WithEdgeJobs([]portainer.EdgeJob{}))
|
||||
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
|
||||
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{})
|
||||
|
||||
//backup
|
||||
archive := backup(t, h, test.backupPassword)
|
||||
|
@ -74,7 +75,7 @@ func Test_restoreArchive_shouldFailIfSystemWasAlreadyInitialized(t *testing.T) {
|
|||
datastore := i.NewDatastore(i.WithUsers([]portainer.User{admin}), i.WithEdgeJobs([]portainer.EdgeJob{}))
|
||||
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
|
||||
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor)
|
||||
h := NewHandler(nil, datastore, offlinegate.NewOfflineGate(), "./test_assets/handler_test", func() {}, adminMonitor, &demo.Service{})
|
||||
|
||||
//backup
|
||||
archive := backup(t, h, "password")
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
func TestEmptyGlobalKey(t *testing.T) {
|
||||
handler := NewHandler(
|
||||
helper.NewTestRequestBouncer(),
|
||||
nil,
|
||||
)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, "https://portainer.io:9443/endpoints/global-key", nil)
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
)
|
||||
|
||||
// @id EndpointDelete
|
||||
|
@ -29,6 +30,10 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
|
||||
}
|
||||
|
||||
if handler.demoService.IsDemoEnvironment(portainer.EndpointID(endpointID)) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, httperrors.ErrNotAvailableInDemo.Error(), httperrors.ErrNotAvailableInDemo}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
|
||||
|
|
|
@ -52,7 +52,7 @@ func Test_endpointList(t *testing.T) {
|
|||
is.NoError(err, "error creating a user")
|
||||
|
||||
bouncer := helper.NewTestRequestBouncer()
|
||||
h := NewHandler(bouncer)
|
||||
h := NewHandler(bouncer, nil)
|
||||
h.DataStore = store
|
||||
h.ComposeStackManager = testhelpers.NewComposeStackManager()
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
|
@ -35,6 +36,7 @@ type requestBouncer interface {
|
|||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer requestBouncer
|
||||
demoService *demo.Service
|
||||
DataStore dataservices.DataStore
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
|
@ -48,10 +50,11 @@ type Handler struct {
|
|||
}
|
||||
|
||||
// NewHandler creates a handler to manage environment(endpoint) operations.
|
||||
func NewHandler(bouncer requestBouncer) *Handler {
|
||||
func NewHandler(bouncer requestBouncer, demoService *demo.Service) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
requestBouncer: bouncer,
|
||||
demoService: demoService,
|
||||
}
|
||||
|
||||
h.Handle("/endpoints",
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
|
@ -24,12 +25,14 @@ type Handler struct {
|
|||
JWTService dataservices.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SnapshotService portainer.SnapshotService
|
||||
demoService *demo.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage settings operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, demoService *demo.Service) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Router: mux.NewRouter(),
|
||||
demoService: demoService,
|
||||
}
|
||||
h.Handle("/settings",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet)
|
||||
|
|
|
@ -113,6 +113,11 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
|
||||
}
|
||||
|
||||
if handler.demoService.IsDemo() {
|
||||
payload.EnableTelemetry = nil
|
||||
payload.LogoURL = nil
|
||||
}
|
||||
|
||||
if payload.AuthenticationMethod != nil {
|
||||
settings.AuthenticationMethod = portainer.AuthenticationMethod(*payload.AuthenticationMethod)
|
||||
}
|
||||
|
|
|
@ -5,21 +5,24 @@ import (
|
|||
|
||||
"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/demo"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle status operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
Status *portainer.Status
|
||||
Status *portainer.Status
|
||||
demoService *demo.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage status operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status, demoService *demo.Service) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
Status: status,
|
||||
Router: mux.NewRouter(),
|
||||
Status: status,
|
||||
demoService: demoService,
|
||||
}
|
||||
h.Handle("/status",
|
||||
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
|
||||
|
|
|
@ -5,16 +5,26 @@ import (
|
|||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
)
|
||||
|
||||
type status struct {
|
||||
*portainer.Status
|
||||
DemoEnvironment demo.EnvironmentDetails
|
||||
}
|
||||
|
||||
// @id StatusInspect
|
||||
// @summary Check Portainer status
|
||||
// @description Retrieve Portainer status
|
||||
// @description **Access policy**: public
|
||||
// @tags status
|
||||
// @produce json
|
||||
// @success 200 {object} portainer.Status "Success"
|
||||
// @success 200 {object} status "Success"
|
||||
// @router /status [get]
|
||||
func (handler *Handler) statusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
return response.JSON(w, handler.Status)
|
||||
return response.JSON(w, &status{
|
||||
Status: handler.Status,
|
||||
DemoEnvironment: handler.demoService.Details(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"net/http"
|
||||
|
@ -32,16 +33,18 @@ type Handler struct {
|
|||
*mux.Router
|
||||
bouncer *security.RequestBouncer
|
||||
apiKeyService apikey.APIKeyService
|
||||
demoService *demo.Service
|
||||
DataStore dataservices.DataStore
|
||||
CryptoService portainer.CryptoService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage user operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, apiKeyService apikey.APIKeyService) *Handler {
|
||||
func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimiter, apiKeyService apikey.APIKeyService, demoService *demo.Service) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
bouncer: bouncer,
|
||||
apiKeyService: apiKeyService,
|
||||
demoService: demoService,
|
||||
}
|
||||
h.Handle("/users",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost)
|
||||
|
|
|
@ -40,7 +40,7 @@ func Test_userCreateAccessToken(t *testing.T) {
|
|||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||
h.DataStore = store
|
||||
|
||||
// generate standard and admin user tokens
|
||||
|
|
|
@ -32,7 +32,7 @@ func Test_deleteUserRemovesAccessTokens(t *testing.T) {
|
|||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||
h.DataStore = store
|
||||
|
||||
t.Run("standard user deletion removes all associated access tokens", func(t *testing.T) {
|
||||
|
|
|
@ -39,7 +39,7 @@ func Test_userGetAccessTokens(t *testing.T) {
|
|||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||
h.DataStore = store
|
||||
|
||||
// generate standard and admin user tokens
|
||||
|
|
|
@ -37,7 +37,7 @@ func Test_userRemoveAccessToken(t *testing.T) {
|
|||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||
h.DataStore = store
|
||||
|
||||
// generate standard and admin user tokens
|
||||
|
|
|
@ -57,6 +57,10 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err}
|
||||
}
|
||||
|
||||
if handler.demoService.IsDemoUser(portainer.UserID(userID)) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, httperrors.ErrNotAvailableInDemo.Error(), httperrors.ErrNotAvailableInDemo}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
|
|
|
@ -55,6 +55,10 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques
|
|||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid user identifier route variable", err}
|
||||
}
|
||||
|
||||
if handler.demoService.IsDemoUser(portainer.UserID(userID)) {
|
||||
return &httperror.HandlerError{http.StatusForbidden, httperrors.ErrNotAvailableInDemo.Error(), httperrors.ErrNotAvailableInDemo}
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
||||
|
|
|
@ -32,7 +32,7 @@ func Test_updateUserRemovesAccessTokens(t *testing.T) {
|
|||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||
h.DataStore = store
|
||||
|
||||
t.Run("standard user deletion removes all associated access tokens", func(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
)
|
||||
|
||||
// restrict functionality on demo environments
|
||||
func RestrictDemoEnv(isDemo func() bool) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !isDemo() {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
httperror.WriteError(w, http.StatusBadRequest, errors.ErrNotAvailableInDemo.Error(), errors.ErrNotAvailableInDemo)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_demoEnvironment_shouldFail(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
RestrictDemoEnv(func() bool { return true }).Middleware(h).ServeHTTP(w, r)
|
||||
|
||||
response := w.Result()
|
||||
defer response.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, response.StatusCode)
|
||||
|
||||
body, _ := io.ReadAll(response.Body)
|
||||
assert.Contains(t, string(body), "This feature is not available in the demo version of Portainer")
|
||||
}
|
||||
|
||||
func Test_notDemoEnvironment_shouldSucceed(t *testing.T) {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
RestrictDemoEnv(func() bool { return false }).Middleware(h).ServeHTTP(w, r)
|
||||
|
||||
response := w.Result()
|
||||
assert.Equal(t, http.StatusOK, response.StatusCode)
|
||||
|
||||
}
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/portainer/portainer/api/apikey"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/demo"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/handler"
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
|
@ -98,6 +99,7 @@ type Server struct {
|
|||
ShutdownCtx context.Context
|
||||
ShutdownTrigger context.CancelFunc
|
||||
StackDeployer stackdeployer.StackDeployer
|
||||
DemoService *demo.Service
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
|
@ -121,7 +123,15 @@ func (server *Server) Start() error {
|
|||
adminMonitor := adminmonitor.New(5*time.Minute, server.DataStore, server.ShutdownCtx)
|
||||
adminMonitor.Start()
|
||||
|
||||
var backupHandler = backup.NewHandler(requestBouncer, server.DataStore, offlineGate, server.FileService.GetDatastorePath(), server.ShutdownTrigger, adminMonitor)
|
||||
var backupHandler = backup.NewHandler(
|
||||
requestBouncer,
|
||||
server.DataStore,
|
||||
offlineGate,
|
||||
server.FileService.GetDatastorePath(),
|
||||
server.ShutdownTrigger,
|
||||
adminMonitor,
|
||||
server.DemoService,
|
||||
)
|
||||
|
||||
var roleHandler = roles.NewHandler(requestBouncer)
|
||||
roleHandler.DataStore = server.DataStore
|
||||
|
@ -147,7 +157,7 @@ func (server *Server) Start() error {
|
|||
var edgeTemplatesHandler = edgetemplates.NewHandler(requestBouncer)
|
||||
edgeTemplatesHandler.DataStore = server.DataStore
|
||||
|
||||
var endpointHandler = endpoints.NewHandler(requestBouncer)
|
||||
var endpointHandler = endpoints.NewHandler(requestBouncer, server.DemoService)
|
||||
endpointHandler.DataStore = server.DataStore
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.ProxyManager = server.ProxyManager
|
||||
|
@ -194,7 +204,7 @@ func (server *Server) Start() error {
|
|||
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
|
||||
resourceControlHandler.DataStore = server.DataStore
|
||||
|
||||
var settingsHandler = settings.NewHandler(requestBouncer)
|
||||
var settingsHandler = settings.NewHandler(requestBouncer, server.DemoService)
|
||||
settingsHandler.DataStore = server.DataStore
|
||||
settingsHandler.FileService = server.FileService
|
||||
settingsHandler.JWTService = server.JWTService
|
||||
|
@ -234,7 +244,7 @@ func (server *Server) Start() error {
|
|||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||
teamMembershipHandler.DataStore = server.DataStore
|
||||
|
||||
var statusHandler = status.NewHandler(requestBouncer, server.Status)
|
||||
var statusHandler = status.NewHandler(requestBouncer, server.Status, server.DemoService)
|
||||
|
||||
var templatesHandler = templates.NewHandler(requestBouncer)
|
||||
templatesHandler.DataStore = server.DataStore
|
||||
|
@ -244,7 +254,7 @@ func (server *Server) Start() error {
|
|||
var uploadHandler = upload.NewHandler(requestBouncer)
|
||||
uploadHandler.FileService = server.FileService
|
||||
|
||||
var userHandler = users.NewHandler(requestBouncer, rateLimiter, server.APIKeyService)
|
||||
var userHandler = users.NewHandler(requestBouncer, rateLimiter, server.APIKeyService, server.DemoService)
|
||||
userHandler.DataStore = server.DataStore
|
||||
userHandler.CryptoService = server.CryptoService
|
||||
|
||||
|
|
|
@ -102,6 +102,7 @@ type (
|
|||
Assets *string
|
||||
Data *string
|
||||
FeatureFlags *[]Pair
|
||||
DemoEnvironment *bool
|
||||
EnableEdgeComputeFeatures *bool
|
||||
EndpointURL *string
|
||||
Labels *[]Pair
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
class DemoFeatureIndicatorController {
|
||||
/* @ngInject */
|
||||
constructor(StateManager) {
|
||||
Object.assign(this, { StateManager });
|
||||
|
||||
this.isDemo = false;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
const state = this.StateManager.getState();
|
||||
|
||||
this.isDemo = state.application.demoEnvironment.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
export default DemoFeatureIndicatorController;
|
|
@ -0,0 +1,10 @@
|
|||
<div class="row" ng-if="$ctrl.isDemo">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-exclamation-triangle" title-text="Feature not available"> </rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<span class="small text-muted">{{ $ctrl.content }}</span>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
import angular from 'angular';
|
||||
import controller from './demo-feature-indicator.controller.js';
|
||||
|
||||
export const demoFeatureIndicator = {
|
||||
templateUrl: './demo-feature-indicator.html',
|
||||
controller,
|
||||
bindings: {
|
||||
content: '<',
|
||||
},
|
||||
};
|
||||
|
||||
angular.module('portainer.app').component('demoFeatureIndicator', demoFeatureIndicator);
|
|
@ -20,8 +20,12 @@ export default class ThemeSettingsController {
|
|||
} else {
|
||||
this.ThemeManager.setTheme(theme);
|
||||
}
|
||||
|
||||
this.state.userTheme = theme;
|
||||
await this.UserService.updateUserTheme(this.state.userId, this.state.userTheme);
|
||||
if (!this.state.isDemo) {
|
||||
await this.UserService.updateUserTheme(this.state.userId, this.state.userTheme);
|
||||
}
|
||||
|
||||
this.Notifications.success('Success', 'User theme successfully updated');
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to update user theme');
|
||||
|
@ -30,10 +34,13 @@ export default class ThemeSettingsController {
|
|||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
const state = this.StateManager.getState();
|
||||
|
||||
this.state = {
|
||||
userId: null,
|
||||
userTheme: '',
|
||||
defaultTheme: 'auto',
|
||||
isDemo: state.application.demoEnvironment.enabled,
|
||||
};
|
||||
|
||||
this.state.availableThemes = [
|
||||
|
|
|
@ -4,6 +4,7 @@ export function StatusViewModel(data) {
|
|||
this.Version = data.Version;
|
||||
this.Edition = data.Edition;
|
||||
this.InstanceID = data.InstanceID;
|
||||
this.DemoEnvironment = data.DemoEnvironment;
|
||||
}
|
||||
|
||||
export function StatusVersionViewModel(data) {
|
||||
|
|
|
@ -9,12 +9,20 @@ angular.module('portainer.app').factory('Backup', [
|
|||
{
|
||||
download: {
|
||||
method: 'POST',
|
||||
responseType: 'blob',
|
||||
responseType: 'arraybuffer',
|
||||
ignoreLoadingBar: true,
|
||||
transformResponse: (data, headersGetter) => ({
|
||||
file: data,
|
||||
name: headersGetter('Content-Disposition').replace('attachment; filename=', ''),
|
||||
}),
|
||||
transformResponse: (data, headersGetter, status) => {
|
||||
if (status !== 200) {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const str = decoder.decode(data);
|
||||
return JSON.parse(str);
|
||||
}
|
||||
|
||||
return {
|
||||
file: data,
|
||||
name: headersGetter('Content-Disposition').replace('attachment; filename=', ''),
|
||||
};
|
||||
},
|
||||
},
|
||||
getS3Settings: { method: 'GET', params: { subResource: 's3', action: 'settings' } },
|
||||
saveS3Settings: { method: 'POST', params: { subResource: 's3', action: 'settings' } },
|
||||
|
|
|
@ -101,7 +101,8 @@ angular.module('portainer.app').factory('Authentication', [
|
|||
|
||||
async function setUserTheme() {
|
||||
const data = await UserService.user(user.ID);
|
||||
// Initialize user theme base on Usertheme from database
|
||||
|
||||
// Initialize user theme base on UserTheme from database
|
||||
const userTheme = data.UserTheme;
|
||||
if (userTheme === 'auto' || !userTheme) {
|
||||
ThemeManager.autoTheme();
|
||||
|
|
|
@ -87,6 +87,7 @@ function StateManagerFactory(
|
|||
state.application.version = status.Version;
|
||||
state.application.edition = status.Edition;
|
||||
state.application.instanceId = status.InstanceID;
|
||||
state.application.demoEnvironment = status.DemoEnvironment;
|
||||
|
||||
state.application.enableTelemetry = settings.EnableTelemetry;
|
||||
state.application.logo = settings.LogoURL;
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
<rd-header-content>User settings</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<demo-feature-indicator ng-if="isDemoUser" content="'You cannot change the password of this account in the demo version of Portainer.'"> </demo-feature-indicator>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
|
@ -56,15 +58,16 @@
|
|||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="(AuthenticationMethod !== 1 && userID !== 1) || !formValues.currentPassword || !passwordStrength || formValues.newPassword !== formValues.confirmPassword"
|
||||
ng-disabled="isDemoUser || (AuthenticationMethod !== 1 && !initialUser) || !formValues.currentPassword || !passwordStrength || formValues.newPassword !== formValues.confirmPassword"
|
||||
ng-click="updatePassword()"
|
||||
>Update password</button
|
||||
>
|
||||
<span class="text-muted small" style="margin-left: 5px" ng-if="AuthenticationMethod === 2 && userID !== 1">
|
||||
Update password
|
||||
</button>
|
||||
<span class="text-muted small" style="margin-left: 5px" ng-if="AuthenticationMethod === 2 && !initialUser">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
You cannot change your password when using LDAP authentication.
|
||||
</span>
|
||||
<span class="text-muted small" style="margin-left: 5px" ng-if="AuthenticationMethod === 3 && userID !== 1">
|
||||
<span class="text-muted small" style="margin-left: 5px" ng-if="AuthenticationMethod === 3 && !initialUser">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
You cannot change your password when using OAuth authentication.
|
||||
</span>
|
||||
|
|
|
@ -97,12 +97,19 @@ angular.module('portainer.app').controller('AccountController', [
|
|||
};
|
||||
|
||||
async function initView() {
|
||||
$scope.userID = Authentication.getUserDetails().ID;
|
||||
$scope.forceChangePassword = Authentication.getUserDetails().forceChangePassword;
|
||||
const state = StateManager.getState();
|
||||
const userDetails = Authentication.getUserDetails();
|
||||
$scope.userID = userDetails.ID;
|
||||
$scope.forceChangePassword = userDetails.forceChangePassword;
|
||||
|
||||
if (state.application.demoEnvironment.enabled) {
|
||||
$scope.isDemoUser = state.application.demoEnvironment.users.includes($scope.userID);
|
||||
}
|
||||
|
||||
const data = await UserService.user($scope.userID);
|
||||
|
||||
$scope.formValues.userTheme = data.Usertheme;
|
||||
$scope.formValues.userTheme = data.UserTheme;
|
||||
|
||||
SettingsService.publicSettings()
|
||||
.then(function success(data) {
|
||||
$scope.AuthenticationMethod = data.AuthenticationMethod;
|
||||
|
|
|
@ -20,13 +20,19 @@
|
|||
<!-- logo -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<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>
|
||||
<por-switch-field
|
||||
label="'Use custom logo'"
|
||||
value="formValues.customLogo"
|
||||
name="'toggle_logo'"
|
||||
disabled="state.isDemo"
|
||||
on-change="(onToggleCustomLogo)"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
<div class="col-sm-12" ng-if="state.isDemo" style="margin-top: 10px">
|
||||
<span class="small text-muted">You cannot use this feature in the demo version of Portainer.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="formValues.customLogo">
|
||||
<div class="form-group">
|
||||
<span class="col-sm-12 text-muted small"> You can specify the URL to your logo here. For an optimal display, logo dimensions should be 155px by 55px. </span>
|
||||
|
@ -39,21 +45,25 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !logo -->
|
||||
<!-- analytics -->
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<label for="toggle_enableTelemetry" class="control-label text-left"> Allow the collection of anonymous statistics </label>
|
||||
<label class="switch" style="margin-left: 20px">
|
||||
<input type="checkbox" name="toggle_enableTelemetry" ng-model="formValues.enableTelemetry" />
|
||||
<i></i>
|
||||
</label>
|
||||
<por-switch-field
|
||||
label="'Allow the collection of anonymous statistics'"
|
||||
value="formValues.enableTelemetry"
|
||||
name="'toggle_enableTelemetry'"
|
||||
on-change="(onToggleEnableTelemetry)"
|
||||
disabled="state.isDemo"
|
||||
></por-switch-field>
|
||||
</div>
|
||||
<div class="col-sm-12" ng-if="state.isDemo" style="margin-top: 10px">
|
||||
<span class="small text-muted">You cannot use this feature in the demo version of Portainer.</span>
|
||||
</div>
|
||||
<div class="col-sm-12 text-muted small" style="margin-top: 10px">
|
||||
You can find more information about this in our
|
||||
<a href="https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/" target="_blank">privacy policy</a>.
|
||||
</div>
|
||||
</div>
|
||||
<!-- !analytics -->
|
||||
<!-- templates -->
|
||||
<div class="col-sm-12 form-section-title"> App Templates </div>
|
||||
<div>
|
||||
|
|
|
@ -20,6 +20,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
];
|
||||
|
||||
$scope.state = {
|
||||
isDemo: false,
|
||||
actionInProgress: false,
|
||||
availableKubeconfigExpiryOptions: [
|
||||
{
|
||||
|
@ -60,6 +61,18 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
backupFormType: $scope.BACKUP_FORM_TYPES.FILE,
|
||||
};
|
||||
|
||||
$scope.onToggleEnableTelemetry = function onToggleEnableTelemetry(checked) {
|
||||
$scope.$evalAsync(() => {
|
||||
$scope.formValues.enableTelemetry = checked;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onToggleCustomLogo = function onToggleCustomLogo(checked) {
|
||||
$scope.$evalAsync(() => {
|
||||
$scope.formValues.customLogo = checked;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onToggleAutoBackups = function onToggleAutoBackups(checked) {
|
||||
$scope.$evalAsync(() => {
|
||||
$scope.formValues.scheduleAutomaticBackups = checked;
|
||||
|
@ -142,6 +155,9 @@ angular.module('portainer.app').controller('SettingsController', [
|
|||
}
|
||||
|
||||
function initView() {
|
||||
const state = StateManager.getState();
|
||||
$scope.state.isDemo = state.application.demoEnvironment.enabled;
|
||||
|
||||
SettingsService.settings()
|
||||
.then(function success(data) {
|
||||
var settings = data;
|
||||
|
|
Loading…
Reference in New Issue