Compare commits

...

6 Commits

Author SHA1 Message Date
yi-portainer
2fec8f1c49 Merge branch 'master' into demo-v2 2021-08-10 15:51:05 +12:00
Chaim Lev-Ari
223494141a feat(backup): disable backup for demo environment (#5034)
* feat(server): add demo cli flag

* feat(backup): block backup operations on demo version

* fix(http/backup): fix tests

* feat(backup): test demo block

* refactor(backup): move demo flag

* Trigger Build

Co-authored-by: yi-portainer <yi.chen@portainer.io>
2021-06-28 12:14:32 +12:00
yi-portainer
55d06f8bab Merge branch 'master' into demo-v2 2021-06-28 11:29:37 +12:00
yi-portainer
c920b9fb39 Merge branch 'master' into demo-v2 2021-05-26 09:19:13 +12:00
yi-portainer
e4890c5ccf * remove unused codes 2021-02-12 16:56:07 +13:00
yi-portainer
6c956f2f0d * backport demo for v2 2021-02-12 15:16:29 +13:00
18 changed files with 184 additions and 24 deletions

View File

@@ -34,6 +34,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", "Endpoint URL").Short('H').String(),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),

View File

@@ -76,6 +76,55 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
return store
}
func initDemoData(
store portainer.DataStore,
cryptoService portainer.CryptoService,
) error {
password, err := cryptoService.Hash("tryportainer")
if err != nil {
return err
}
admin := &portainer.User{
Username: "admin",
Password: password,
Role: portainer.AdministratorRole,
}
err = store.User().CreateUser(admin)
if err != nil {
return err
}
localEndpoint := &portainer.Endpoint{
ID: portainer.EndpointID(1),
Name: "local",
URL: "unix:///var/run/docker.sock",
PublicURL: "demo.portainer.io",
Type: portainer.DockerEnvironment,
GroupID: portainer.EndpointGroupID(1),
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
Kubernetes: portainer.KubernetesDefault(),
}
err = store.Endpoint().CreateEndpoint(localEndpoint)
if err != nil {
return err
}
return err
}
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper := exec.NewComposeWrapper(assetsPath, dataStorePath, proxyManager)
if composeWrapper != nil {
@@ -165,7 +214,7 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI
settings.LogoURL = *flags.Logo
settings.SnapshotInterval = *flags.SnapshotInterval
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
settings.EnableTelemetry = true
settings.EnableTelemetry = false
settings.OAuthSettings.SSO = true
if *flags.Templates != "" {
@@ -367,6 +416,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
cryptoService := initCryptoService()
err = initDemoData(dataStore, cryptoService)
if err != nil {
log.Printf("error init demo: %v", err)
}
digitalSignatureService := initDigitalSignatureService()
err = initKeyPair(fileService, digitalSignatureService)
@@ -471,6 +525,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
BindAddress: *flags.Addr,
AssetsPath: *flags.Assets,
DataStore: dataStore,
DemoEnvironment: *flags.DemoEnvironment,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
KubernetesDeployer: kubernetesDeployer,

View File

@@ -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")
)

View File

@@ -49,7 +49,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, false).backup(w, r)
assert.Nil(t, handlerErr, "Handler should not fail")
response := w.Result()
@@ -86,7 +86,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, false).backup(w, r)
assert.Nil(t, handlerErr, "Handler should not fail")
response := w.Result()

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"github.com/gorilla/mux"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/adminmonitor"
@@ -24,7 +25,7 @@ type Handler struct {
}
// NewHandler creates an new instance of backup handler
func NewHandler(bouncer *security.RequestBouncer, dataStore portainer.DataStore, gate *offlinegate.OfflineGate, filestorePath string, shutdownTrigger context.CancelFunc, adminMonitor *adminmonitor.Monitor) *Handler {
func NewHandler(bouncer *security.RequestBouncer, dataStore portainer.DataStore, gate *offlinegate.OfflineGate, filestorePath string, shutdownTrigger context.CancelFunc, adminMonitor *adminmonitor.Monitor, isDemo bool) *Handler {
h := &Handler{
Router: mux.NewRouter(),
bouncer: bouncer,
@@ -35,8 +36,8 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore portainer.DataStore,
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)
h.Handle("/backup", restrictDemoEnv(isDemo, bouncer.RestrictedAccess(adminAccess(httperror.LoggerHandler(h.backup))))).Methods(http.MethodPost)
h.Handle("/restore", restrictDemoEnv(isDemo, bouncer.PublicAccess(httperror.LoggerHandler(h.restore)))).Methods(http.MethodPost)
return h
}
@@ -46,20 +47,26 @@ func adminAccess(next http.Handler) http.Handler {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
httperror.WriteError(w, http.StatusInternalServerError, "Unable to retrieve user info from request context", err)
return
}
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)
return
}
next.ServeHTTP(w, r)
})
}
func systemWasInitialized(dataStore portainer.DataStore) (bool, error) {
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
return false, err
}
return len(users) > 0, nil
// restrict backup functionality on demo environments
func restrictDemoEnv(isDemo bool, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isDemo {
httperror.WriteError(w, http.StatusBadRequest, "This feature is not available in the demo version of Portainer", errors.New("this feature is not available in the demo version of Portainer"))
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,35 @@
package backup
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()
restrictDemoEnv(true, http.DefaultServeMux).ServeHTTP(w, r)
response := w.Result()
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()
restrictDemoEnv(false, http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {})).ServeHTTP(w, r)
response := w.Result()
assert.Equal(t, http.StatusOK, response.StatusCode)
}

View File

@@ -51,7 +51,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, false)
//backup
archive := backup(t, h, test.backupPassword)
@@ -74,7 +74,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, false)
//backup
archive := backup(t, h, "password")

View File

@@ -9,6 +9,7 @@ import (
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
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 endpoint identifier route variable", err}
}
if endpointID == 1 || endpointID == 2 {
return &httperror.HandlerError{http.StatusForbidden, "This feature is not available in the demo version of Portainer", httperrors.ErrNotAvailableInDemo}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == errors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}

View File

@@ -81,6 +81,9 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
}
*payload.EnableTelemetry = false
*payload.LogoURL = ""
if payload.AuthenticationMethod != nil {
settings.AuthenticationMethod = portainer.AuthenticationMethod(*payload.AuthenticationMethod)
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
)
@@ -42,6 +43,10 @@ func (handler *Handler) userDelete(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user authentication token", err}
}
if userID == 1 {
return &httperror.HandlerError{http.StatusForbidden, "This feature is not available in the demo version of Portainer", httperrors.ErrNotAvailableInDemo}
}
if tokenData.ID == portainer.UserID(userID) {
return &httperror.HandlerError{http.StatusForbidden, "Cannot remove your own user account. Contact another administrator", errAdminCannotRemoveSelf}
}

View File

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

View File

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

View File

@@ -70,6 +70,7 @@ type Server struct {
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
Handler *handler.Handler
DemoEnvironment bool
SSL bool
SSLCert string
SSLKey string
@@ -101,7 +102,7 @@ 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.DemoEnvironment)
var roleHandler = roles.NewHandler(requestBouncer)
roleHandler.DataStore = server.DataStore

View File

@@ -47,6 +47,7 @@ type (
AdminPasswordFile *string
Assets *string
Data *string
DemoEnvironment *bool
EnableEdgeComputeFeatures *bool
EndpointURL *string
Labels *[]Pair

View File

@@ -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' } },

View File

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

View File

@@ -237,7 +237,6 @@ class CustomTemplatesViewController {
case 2:
deployable = endpoint.mode.provider === this.DOCKER_STANDALONE;
break;
}
return deployable;

View File

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