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(),
|
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(),
|
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(),
|
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(),
|
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
|
||||||
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
|
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
|
||||||
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
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/database/boltdb"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
|
"github.com/portainer/portainer/api/demo"
|
||||||
"github.com/portainer/portainer/api/docker"
|
"github.com/portainer/portainer/api/docker"
|
||||||
"github.com/portainer/portainer/api/exec"
|
"github.com/portainer/portainer/api/exec"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
|
@ -572,6 +573,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||||
openAMTService := openamt.NewService()
|
openAMTService := openamt.NewService()
|
||||||
|
|
||||||
cryptoService := initCryptoService()
|
cryptoService := initCryptoService()
|
||||||
|
|
||||||
digitalSignatureService := initDigitalSignatureService()
|
digitalSignatureService := initDigitalSignatureService()
|
||||||
|
|
||||||
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
|
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)
|
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)
|
err = initEndpoint(flags, dataStore, snapshotService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalf("Failed initializing environment: %v", err)
|
logrus.Fatalf("Failed initializing environment: %v", err)
|
||||||
|
@ -722,6 +732,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
|
||||||
ShutdownCtx: shutdownCtx,
|
ShutdownCtx: shutdownCtx,
|
||||||
ShutdownTrigger: shutdownTrigger,
|
ShutdownTrigger: shutdownTrigger,
|
||||||
StackDeployer: stackDeployer,
|
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")
|
ErrUnauthorized = errors.New("Unauthorized")
|
||||||
// ErrResourceAccessDenied Access denied to resource error
|
// ErrResourceAccessDenied Access denied to resource error
|
||||||
ErrResourceAccessDenied = errors.New("Access denied to resource")
|
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/docker/docker/pkg/ioutils"
|
||||||
"github.com/portainer/portainer/api/adminmonitor"
|
"github.com/portainer/portainer/api/adminmonitor"
|
||||||
"github.com/portainer/portainer/api/crypto"
|
"github.com/portainer/portainer/api/crypto"
|
||||||
|
"github.com/portainer/portainer/api/demo"
|
||||||
"github.com/portainer/portainer/api/http/offlinegate"
|
"github.com/portainer/portainer/api/http/offlinegate"
|
||||||
i "github.com/portainer/portainer/api/internal/testhelpers"
|
i "github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -49,7 +50,7 @@ func Test_backupHandlerWithoutPassword_shouldCreateATarballArchive(t *testing.T)
|
||||||
gate := offlinegate.NewOfflineGate()
|
gate := offlinegate.NewOfflineGate()
|
||||||
adminMonitor := adminmonitor.New(time.Hour, nil, context.Background())
|
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")
|
assert.Nil(t, handlerErr, "Handler should not fail")
|
||||||
|
|
||||||
response := w.Result()
|
response := w.Result()
|
||||||
|
@ -86,7 +87,7 @@ func Test_backupHandlerWithPassword_shouldCreateEncryptedATarballArchive(t *test
|
||||||
gate := offlinegate.NewOfflineGate()
|
gate := offlinegate.NewOfflineGate()
|
||||||
adminMonitor := adminmonitor.New(time.Hour, nil, nil)
|
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")
|
assert.Nil(t, handlerErr, "Handler should not fail")
|
||||||
|
|
||||||
response := w.Result()
|
response := w.Result()
|
||||||
|
|
|
@ -9,6 +9,8 @@ import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/adminmonitor"
|
"github.com/portainer/portainer/api/adminmonitor"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"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/offlinegate"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
@ -25,7 +27,17 @@ type Handler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates an new instance of backup handler
|
// 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{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
bouncer: bouncer,
|
bouncer: bouncer,
|
||||||
|
@ -36,8 +48,11 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
|
||||||
adminMonitor: adminMonitor,
|
adminMonitor: adminMonitor,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Handle("/backup", bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup)))).Methods(http.MethodPost)
|
demoRestrictedRouter := h.NewRoute().Subrouter()
|
||||||
h.Handle("/restore", bouncer.PublicAccess(httperror.LoggerHandler(h.restore))).Methods(http.MethodPost)
|
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
|
return h
|
||||||
}
|
}
|
||||||
|
@ -50,7 +65,7 @@ func adminAccess(next http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
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)
|
next.ServeHTTP(w, r)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/adminmonitor"
|
"github.com/portainer/portainer/api/adminmonitor"
|
||||||
|
"github.com/portainer/portainer/api/demo"
|
||||||
"github.com/portainer/portainer/api/http/offlinegate"
|
"github.com/portainer/portainer/api/http/offlinegate"
|
||||||
i "github.com/portainer/portainer/api/internal/testhelpers"
|
i "github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
"github.com/stretchr/testify/assert"
|
"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{}))
|
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}), i.WithEdgeJobs([]portainer.EdgeJob{}))
|
||||||
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
|
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
|
//backup
|
||||||
archive := backup(t, h, test.backupPassword)
|
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{}))
|
datastore := i.NewDatastore(i.WithUsers([]portainer.User{admin}), i.WithEdgeJobs([]portainer.EdgeJob{}))
|
||||||
adminMonitor := adminmonitor.New(time.Hour, datastore, context.Background())
|
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
|
//backup
|
||||||
archive := backup(t, h, "password")
|
archive := backup(t, h, "password")
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
func TestEmptyGlobalKey(t *testing.T) {
|
func TestEmptyGlobalKey(t *testing.T) {
|
||||||
handler := NewHandler(
|
handler := NewHandler(
|
||||||
helper.NewTestRequestBouncer(),
|
helper.NewTestRequestBouncer(),
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, "https://portainer.io:9443/endpoints/global-key", 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/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @id EndpointDelete
|
// @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}
|
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))
|
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", 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")
|
is.NoError(err, "error creating a user")
|
||||||
|
|
||||||
bouncer := helper.NewTestRequestBouncer()
|
bouncer := helper.NewTestRequestBouncer()
|
||||||
h := NewHandler(bouncer)
|
h := NewHandler(bouncer, nil)
|
||||||
h.DataStore = store
|
h.DataStore = store
|
||||||
h.ComposeStackManager = testhelpers.NewComposeStackManager()
|
h.ComposeStackManager = testhelpers.NewComposeStackManager()
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/demo"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
"github.com/portainer/portainer/api/internal/authorization"
|
"github.com/portainer/portainer/api/internal/authorization"
|
||||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
|
@ -35,6 +36,7 @@ type requestBouncer interface {
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
requestBouncer requestBouncer
|
requestBouncer requestBouncer
|
||||||
|
demoService *demo.Service
|
||||||
DataStore dataservices.DataStore
|
DataStore dataservices.DataStore
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
|
@ -48,10 +50,11 @@ type Handler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage environment(endpoint) operations.
|
// NewHandler creates a handler to manage environment(endpoint) operations.
|
||||||
func NewHandler(bouncer requestBouncer) *Handler {
|
func NewHandler(bouncer requestBouncer, demoService *demo.Service) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
requestBouncer: bouncer,
|
requestBouncer: bouncer,
|
||||||
|
demoService: demoService,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Handle("/endpoints",
|
h.Handle("/endpoints",
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/demo"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,12 +25,14 @@ type Handler struct {
|
||||||
JWTService dataservices.JWTService
|
JWTService dataservices.JWTService
|
||||||
LDAPService portainer.LDAPService
|
LDAPService portainer.LDAPService
|
||||||
SnapshotService portainer.SnapshotService
|
SnapshotService portainer.SnapshotService
|
||||||
|
demoService *demo.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage settings operations.
|
// 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{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
|
demoService: demoService,
|
||||||
}
|
}
|
||||||
h.Handle("/settings",
|
h.Handle("/settings",
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet)
|
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}
|
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 {
|
if payload.AuthenticationMethod != nil {
|
||||||
settings.AuthenticationMethod = portainer.AuthenticationMethod(*payload.AuthenticationMethod)
|
settings.AuthenticationMethod = portainer.AuthenticationMethod(*payload.AuthenticationMethod)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,8 @@ import (
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/demo"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -13,13 +14,15 @@ import (
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
Status *portainer.Status
|
Status *portainer.Status
|
||||||
|
demoService *demo.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage status operations.
|
// 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{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
Status: status,
|
Status: status,
|
||||||
|
demoService: demoService,
|
||||||
}
|
}
|
||||||
h.Handle("/status",
|
h.Handle("/status",
|
||||||
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
|
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
|
||||||
|
|
|
@ -5,16 +5,26 @@ import (
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/response"
|
"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
|
// @id StatusInspect
|
||||||
// @summary Check Portainer status
|
// @summary Check Portainer status
|
||||||
// @description Retrieve Portainer status
|
// @description Retrieve Portainer status
|
||||||
// @description **Access policy**: public
|
// @description **Access policy**: public
|
||||||
// @tags status
|
// @tags status
|
||||||
// @produce json
|
// @produce json
|
||||||
// @success 200 {object} portainer.Status "Success"
|
// @success 200 {object} status "Success"
|
||||||
// @router /status [get]
|
// @router /status [get]
|
||||||
func (handler *Handler) statusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
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/apikey"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/demo"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -32,16 +33,18 @@ type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
bouncer *security.RequestBouncer
|
bouncer *security.RequestBouncer
|
||||||
apiKeyService apikey.APIKeyService
|
apiKeyService apikey.APIKeyService
|
||||||
|
demoService *demo.Service
|
||||||
DataStore dataservices.DataStore
|
DataStore dataservices.DataStore
|
||||||
CryptoService portainer.CryptoService
|
CryptoService portainer.CryptoService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage user operations.
|
// 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{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
bouncer: bouncer,
|
bouncer: bouncer,
|
||||||
apiKeyService: apiKeyService,
|
apiKeyService: apiKeyService,
|
||||||
|
demoService: demoService,
|
||||||
}
|
}
|
||||||
h.Handle("/users",
|
h.Handle("/users",
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost)
|
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)
|
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||||
|
|
||||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||||
h.DataStore = store
|
h.DataStore = store
|
||||||
|
|
||||||
// generate standard and admin user tokens
|
// generate standard and admin user tokens
|
||||||
|
|
|
@ -32,7 +32,7 @@ func Test_deleteUserRemovesAccessTokens(t *testing.T) {
|
||||||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||||
|
|
||||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||||
h.DataStore = store
|
h.DataStore = store
|
||||||
|
|
||||||
t.Run("standard user deletion removes all associated access tokens", func(t *testing.T) {
|
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)
|
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||||
|
|
||||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||||
h.DataStore = store
|
h.DataStore = store
|
||||||
|
|
||||||
// generate standard and admin user tokens
|
// generate standard and admin user tokens
|
||||||
|
|
|
@ -37,7 +37,7 @@ func Test_userRemoveAccessToken(t *testing.T) {
|
||||||
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||||
|
|
||||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||||
h.DataStore = store
|
h.DataStore = store
|
||||||
|
|
||||||
// generate standard and admin user tokens
|
// 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}
|
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)
|
tokenData, err := security.RetrieveTokenData(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
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}
|
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)
|
tokenData, err := security.RetrieveTokenData(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
|
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)
|
requestBouncer := security.NewRequestBouncer(store, jwtService, apiKeyService)
|
||||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||||
|
|
||||||
h := NewHandler(requestBouncer, rateLimiter, apiKeyService)
|
h := NewHandler(requestBouncer, rateLimiter, apiKeyService, nil)
|
||||||
h.DataStore = store
|
h.DataStore = store
|
||||||
|
|
||||||
t.Run("standard user deletion removes all associated access tokens", func(t *testing.T) {
|
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/apikey"
|
||||||
"github.com/portainer/portainer/api/crypto"
|
"github.com/portainer/portainer/api/crypto"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/demo"
|
||||||
"github.com/portainer/portainer/api/docker"
|
"github.com/portainer/portainer/api/docker"
|
||||||
"github.com/portainer/portainer/api/http/handler"
|
"github.com/portainer/portainer/api/http/handler"
|
||||||
"github.com/portainer/portainer/api/http/handler/auth"
|
"github.com/portainer/portainer/api/http/handler/auth"
|
||||||
|
@ -98,6 +99,7 @@ type Server struct {
|
||||||
ShutdownCtx context.Context
|
ShutdownCtx context.Context
|
||||||
ShutdownTrigger context.CancelFunc
|
ShutdownTrigger context.CancelFunc
|
||||||
StackDeployer stackdeployer.StackDeployer
|
StackDeployer stackdeployer.StackDeployer
|
||||||
|
DemoService *demo.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start starts the HTTP server
|
// 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 := adminmonitor.New(5*time.Minute, server.DataStore, server.ShutdownCtx)
|
||||||
adminMonitor.Start()
|
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)
|
var roleHandler = roles.NewHandler(requestBouncer)
|
||||||
roleHandler.DataStore = server.DataStore
|
roleHandler.DataStore = server.DataStore
|
||||||
|
@ -147,7 +157,7 @@ func (server *Server) Start() error {
|
||||||
var edgeTemplatesHandler = edgetemplates.NewHandler(requestBouncer)
|
var edgeTemplatesHandler = edgetemplates.NewHandler(requestBouncer)
|
||||||
edgeTemplatesHandler.DataStore = server.DataStore
|
edgeTemplatesHandler.DataStore = server.DataStore
|
||||||
|
|
||||||
var endpointHandler = endpoints.NewHandler(requestBouncer)
|
var endpointHandler = endpoints.NewHandler(requestBouncer, server.DemoService)
|
||||||
endpointHandler.DataStore = server.DataStore
|
endpointHandler.DataStore = server.DataStore
|
||||||
endpointHandler.FileService = server.FileService
|
endpointHandler.FileService = server.FileService
|
||||||
endpointHandler.ProxyManager = server.ProxyManager
|
endpointHandler.ProxyManager = server.ProxyManager
|
||||||
|
@ -194,7 +204,7 @@ func (server *Server) Start() error {
|
||||||
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
|
var resourceControlHandler = resourcecontrols.NewHandler(requestBouncer)
|
||||||
resourceControlHandler.DataStore = server.DataStore
|
resourceControlHandler.DataStore = server.DataStore
|
||||||
|
|
||||||
var settingsHandler = settings.NewHandler(requestBouncer)
|
var settingsHandler = settings.NewHandler(requestBouncer, server.DemoService)
|
||||||
settingsHandler.DataStore = server.DataStore
|
settingsHandler.DataStore = server.DataStore
|
||||||
settingsHandler.FileService = server.FileService
|
settingsHandler.FileService = server.FileService
|
||||||
settingsHandler.JWTService = server.JWTService
|
settingsHandler.JWTService = server.JWTService
|
||||||
|
@ -234,7 +244,7 @@ func (server *Server) Start() error {
|
||||||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||||
teamMembershipHandler.DataStore = server.DataStore
|
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)
|
var templatesHandler = templates.NewHandler(requestBouncer)
|
||||||
templatesHandler.DataStore = server.DataStore
|
templatesHandler.DataStore = server.DataStore
|
||||||
|
@ -244,7 +254,7 @@ func (server *Server) Start() error {
|
||||||
var uploadHandler = upload.NewHandler(requestBouncer)
|
var uploadHandler = upload.NewHandler(requestBouncer)
|
||||||
uploadHandler.FileService = server.FileService
|
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.DataStore = server.DataStore
|
||||||
userHandler.CryptoService = server.CryptoService
|
userHandler.CryptoService = server.CryptoService
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,7 @@ type (
|
||||||
Assets *string
|
Assets *string
|
||||||
Data *string
|
Data *string
|
||||||
FeatureFlags *[]Pair
|
FeatureFlags *[]Pair
|
||||||
|
DemoEnvironment *bool
|
||||||
EnableEdgeComputeFeatures *bool
|
EnableEdgeComputeFeatures *bool
|
||||||
EndpointURL *string
|
EndpointURL *string
|
||||||
Labels *[]Pair
|
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 {
|
} else {
|
||||||
this.ThemeManager.setTheme(theme);
|
this.ThemeManager.setTheme(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state.userTheme = theme;
|
this.state.userTheme = theme;
|
||||||
|
if (!this.state.isDemo) {
|
||||||
await this.UserService.updateUserTheme(this.state.userId, this.state.userTheme);
|
await this.UserService.updateUserTheme(this.state.userId, this.state.userTheme);
|
||||||
|
}
|
||||||
|
|
||||||
this.Notifications.success('Success', 'User theme successfully updated');
|
this.Notifications.success('Success', 'User theme successfully updated');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.Notifications.error('Failure', err, 'Unable to update user theme');
|
this.Notifications.error('Failure', err, 'Unable to update user theme');
|
||||||
|
@ -30,10 +34,13 @@ export default class ThemeSettingsController {
|
||||||
|
|
||||||
$onInit() {
|
$onInit() {
|
||||||
return this.$async(async () => {
|
return this.$async(async () => {
|
||||||
|
const state = this.StateManager.getState();
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
userId: null,
|
userId: null,
|
||||||
userTheme: '',
|
userTheme: '',
|
||||||
defaultTheme: 'auto',
|
defaultTheme: 'auto',
|
||||||
|
isDemo: state.application.demoEnvironment.enabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state.availableThemes = [
|
this.state.availableThemes = [
|
||||||
|
|
|
@ -4,6 +4,7 @@ export function StatusViewModel(data) {
|
||||||
this.Version = data.Version;
|
this.Version = data.Version;
|
||||||
this.Edition = data.Edition;
|
this.Edition = data.Edition;
|
||||||
this.InstanceID = data.InstanceID;
|
this.InstanceID = data.InstanceID;
|
||||||
|
this.DemoEnvironment = data.DemoEnvironment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusVersionViewModel(data) {
|
export function StatusVersionViewModel(data) {
|
||||||
|
|
|
@ -9,12 +9,20 @@ angular.module('portainer.app').factory('Backup', [
|
||||||
{
|
{
|
||||||
download: {
|
download: {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
responseType: 'blob',
|
responseType: 'arraybuffer',
|
||||||
ignoreLoadingBar: true,
|
ignoreLoadingBar: true,
|
||||||
transformResponse: (data, headersGetter) => ({
|
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,
|
file: data,
|
||||||
name: headersGetter('Content-Disposition').replace('attachment; filename=', ''),
|
name: headersGetter('Content-Disposition').replace('attachment; filename=', ''),
|
||||||
}),
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getS3Settings: { method: 'GET', params: { subResource: 's3', action: 'settings' } },
|
getS3Settings: { method: 'GET', params: { subResource: 's3', action: 'settings' } },
|
||||||
saveS3Settings: { method: 'POST', 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() {
|
async function setUserTheme() {
|
||||||
const data = await UserService.user(user.ID);
|
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;
|
const userTheme = data.UserTheme;
|
||||||
if (userTheme === 'auto' || !userTheme) {
|
if (userTheme === 'auto' || !userTheme) {
|
||||||
ThemeManager.autoTheme();
|
ThemeManager.autoTheme();
|
||||||
|
|
|
@ -87,6 +87,7 @@ function StateManagerFactory(
|
||||||
state.application.version = status.Version;
|
state.application.version = status.Version;
|
||||||
state.application.edition = status.Edition;
|
state.application.edition = status.Edition;
|
||||||
state.application.instanceId = status.InstanceID;
|
state.application.instanceId = status.InstanceID;
|
||||||
|
state.application.demoEnvironment = status.DemoEnvironment;
|
||||||
|
|
||||||
state.application.enableTelemetry = settings.EnableTelemetry;
|
state.application.enableTelemetry = settings.EnableTelemetry;
|
||||||
state.application.logo = settings.LogoURL;
|
state.application.logo = settings.LogoURL;
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
<rd-header-content>User settings</rd-header-content>
|
<rd-header-content>User settings</rd-header-content>
|
||||||
</rd-header>
|
</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="row">
|
||||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
|
@ -56,15 +58,16 @@
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-primary btn-sm"
|
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()"
|
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>
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
You cannot change your password when using LDAP authentication.
|
You cannot change your password when using LDAP authentication.
|
||||||
</span>
|
</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>
|
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||||
You cannot change your password when using OAuth authentication.
|
You cannot change your password when using OAuth authentication.
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -97,12 +97,19 @@ angular.module('portainer.app').controller('AccountController', [
|
||||||
};
|
};
|
||||||
|
|
||||||
async function initView() {
|
async function initView() {
|
||||||
$scope.userID = Authentication.getUserDetails().ID;
|
const state = StateManager.getState();
|
||||||
$scope.forceChangePassword = Authentication.getUserDetails().forceChangePassword;
|
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);
|
const data = await UserService.user($scope.userID);
|
||||||
|
|
||||||
$scope.formValues.userTheme = data.Usertheme;
|
$scope.formValues.userTheme = data.UserTheme;
|
||||||
|
|
||||||
SettingsService.publicSettings()
|
SettingsService.publicSettings()
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.AuthenticationMethod = data.AuthenticationMethod;
|
$scope.AuthenticationMethod = data.AuthenticationMethod;
|
||||||
|
|
|
@ -20,13 +20,19 @@
|
||||||
<!-- logo -->
|
<!-- logo -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label for="toggle_logo" class="control-label text-left"> Use custom logo </label>
|
<por-switch-field
|
||||||
<label class="switch" style="margin-left: 20px">
|
label="'Use custom logo'"
|
||||||
<input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo" />
|
value="formValues.customLogo"
|
||||||
<i></i>
|
name="'toggle_logo'"
|
||||||
</label>
|
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>
|
</div>
|
||||||
|
|
||||||
<div ng-if="formValues.customLogo">
|
<div ng-if="formValues.customLogo">
|
||||||
<div class="form-group">
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<!-- !logo -->
|
<!-- !logo -->
|
||||||
<!-- analytics -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<label for="toggle_enableTelemetry" class="control-label text-left"> Allow the collection of anonymous statistics </label>
|
<por-switch-field
|
||||||
<label class="switch" style="margin-left: 20px">
|
label="'Allow the collection of anonymous statistics'"
|
||||||
<input type="checkbox" name="toggle_enableTelemetry" ng-model="formValues.enableTelemetry" />
|
value="formValues.enableTelemetry"
|
||||||
<i></i>
|
name="'toggle_enableTelemetry'"
|
||||||
</label>
|
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>
|
||||||
<div class="col-sm-12 text-muted small" style="margin-top: 10px">
|
<div class="col-sm-12 text-muted small" style="margin-top: 10px">
|
||||||
You can find more information about this in our
|
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>.
|
<a href="https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/" target="_blank">privacy policy</a>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !analytics -->
|
|
||||||
<!-- templates -->
|
<!-- templates -->
|
||||||
<div class="col-sm-12 form-section-title"> App Templates </div>
|
<div class="col-sm-12 form-section-title"> App Templates </div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -20,6 +20,7 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||||
];
|
];
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
|
isDemo: false,
|
||||||
actionInProgress: false,
|
actionInProgress: false,
|
||||||
availableKubeconfigExpiryOptions: [
|
availableKubeconfigExpiryOptions: [
|
||||||
{
|
{
|
||||||
|
@ -60,6 +61,18 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||||
backupFormType: $scope.BACKUP_FORM_TYPES.FILE,
|
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.onToggleAutoBackups = function onToggleAutoBackups(checked) {
|
||||||
$scope.$evalAsync(() => {
|
$scope.$evalAsync(() => {
|
||||||
$scope.formValues.scheduleAutomaticBackups = checked;
|
$scope.formValues.scheduleAutomaticBackups = checked;
|
||||||
|
@ -142,6 +155,9 @@ angular.module('portainer.app').controller('SettingsController', [
|
||||||
}
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
|
const state = StateManager.getState();
|
||||||
|
$scope.state.isDemo = state.application.demoEnvironment.enabled;
|
||||||
|
|
||||||
SettingsService.settings()
|
SettingsService.settings()
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var settings = data;
|
var settings = data;
|
||||||
|
|
Loading…
Reference in New Issue