diff --git a/api/adminmonitor/admin_monitor.go b/api/adminmonitor/admin_monitor.go index 5b3e45868..e83dd47d5 100644 --- a/api/adminmonitor/admin_monitor.go +++ b/api/adminmonitor/admin_monitor.go @@ -3,31 +3,36 @@ package adminmonitor import ( "context" "log" + "net/http" + "strings" "sync" "time" + httperror "github.com/portainer/libhttp/error" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" ) var logFatalf = log.Fatalf +const RedirectReasonAdminInitTimeout string = "AdminInitTimeout" + type Monitor struct { - timeout time.Duration - datastore dataservices.DataStore - shutdownCtx context.Context - cancellationFunc context.CancelFunc - mu sync.Mutex - timeoutSignal chan<- interface{} + timeout time.Duration + datastore dataservices.DataStore + shutdownCtx context.Context + cancellationFunc context.CancelFunc + mu sync.Mutex + adminInitDisabled bool } // New creates a monitor that when started will wait for the timeout duration and then sends the timeout signal to disable the application -func New(timeout time.Duration, datastore dataservices.DataStore, timeoutSignal chan<- interface{}, shutdownCtx context.Context) *Monitor { +func New(timeout time.Duration, datastore dataservices.DataStore, shutdownCtx context.Context) *Monitor { return &Monitor{ - timeout: timeout, - datastore: datastore, - shutdownCtx: shutdownCtx, - timeoutSignal: timeoutSignal, + timeout: timeout, + datastore: datastore, + shutdownCtx: shutdownCtx, + adminInitDisabled: false, } } @@ -53,9 +58,9 @@ func (m *Monitor) Start() { } if !initialized { log.Println("[INFO] [internal,init] The Portainer instance timed out for security purposes. To re-enable your Portainer instance, you will need to restart Portainer") - if m.timeoutSignal != nil { - close(m.timeoutSignal) - } + m.mu.Lock() + defer m.mu.Unlock() + m.adminInitDisabled = true return } case <-cancellationCtx.Done(): @@ -86,3 +91,25 @@ func (m *Monitor) WasInitialized() (bool, error) { } return len(users) > 0, nil } + +func (m *Monitor) WasInstanceDisabled() bool { + m.mu.Lock() + defer m.mu.Unlock() + return m.adminInitDisabled +} + +// WithRedirect checks whether administrator initialisation timeout. If so, it will return the error with redirect reason. +// Otherwise, it will pass through the request to next +func (m *Monitor) WithRedirect(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if m.WasInstanceDisabled() { + if strings.HasPrefix(r.RequestURI, "/api") && r.RequestURI != "/api/status" && r.RequestURI != "/api/settings/public" { + w.Header().Set("redirect-reason", RedirectReasonAdminInitTimeout) + httperror.WriteError(w, http.StatusSeeOther, "Administrator initialization timeout", nil) + return + } + } + + next.ServeHTTP(w, r) + }) +} diff --git a/api/adminmonitor/admin_monitor_test.go b/api/adminmonitor/admin_monitor_test.go index 3ea6fbed1..08727fc2b 100644 --- a/api/adminmonitor/admin_monitor_test.go +++ b/api/adminmonitor/admin_monitor_test.go @@ -11,18 +11,18 @@ import ( ) func Test_stopWithoutStarting(t *testing.T) { - monitor := New(1*time.Minute, nil, nil, nil) + monitor := New(1*time.Minute, nil, nil) monitor.Stop() } func Test_stopCouldBeCalledMultipleTimes(t *testing.T) { - monitor := New(1*time.Minute, nil, nil, nil) + monitor := New(1*time.Minute, nil, nil) monitor.Stop() monitor.Stop() } func Test_startOrStopCouldBeCalledMultipleTimesConcurrently(t *testing.T) { - monitor := New(1*time.Minute, nil, nil, context.Background()) + monitor := New(1*time.Minute, nil, context.Background()) go monitor.Start() monitor.Start() @@ -34,7 +34,7 @@ func Test_startOrStopCouldBeCalledMultipleTimesConcurrently(t *testing.T) { } func Test_canStopStartedMonitor(t *testing.T) { - monitor := New(1*time.Minute, nil, nil, context.Background()) + monitor := New(1*time.Minute, nil, context.Background()) monitor.Start() assert.NotNil(t, monitor.cancellationFunc, "cancellation function is missing in started monitor") @@ -42,16 +42,13 @@ func Test_canStopStartedMonitor(t *testing.T) { assert.Nil(t, monitor.cancellationFunc, "cancellation function should absent in stopped monitor") } -func Test_start_shouldSendSignalAfterTimeout_ifNotInitialized(t *testing.T) { +func Test_start_shouldDisableInstanceAfterTimeout_ifNotInitialized(t *testing.T) { timeout := 10 * time.Millisecond - initTimeoutSignal := make(chan interface{}) - datastore := i.NewDatastore(i.WithUsers([]portainer.User{})) - monitor := New(timeout, datastore, initTimeoutSignal, context.Background()) + monitor := New(timeout, datastore, context.Background()) monitor.Start() <-time.After(20 * timeout) - _, ok := <-initTimeoutSignal - assert.False(t, ok, "monitor should have been timeout and sent init timeout signal out") + assert.True(t, monitor.WasInstanceDisabled(), "monitor should have been timeout and instance is disabled") } diff --git a/api/http/handler/file/handler.go b/api/http/handler/file/handler.go index 72f8604e8..137a87c52 100644 --- a/api/http/handler/file/handler.go +++ b/api/http/handler/file/handler.go @@ -10,14 +10,16 @@ import ( // Handler represents an HTTP API handler for managing static files. type Handler struct { http.Handler + wasInstanceDisabled func() bool } // NewHandler creates a handler to serve static files. -func NewHandler(assetPublicPath string) *Handler { +func NewHandler(assetPublicPath string, wasInstanceDisabled func() bool) *Handler { h := &Handler{ Handler: handlers.CompressHandler( http.FileServer(http.Dir(assetPublicPath)), ), + wasInstanceDisabled: wasInstanceDisabled, } return h @@ -33,6 +35,11 @@ func isHTML(acceptContent []string) bool { } func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if handler.wasInstanceDisabled() && (r.RequestURI == "/" || r.RequestURI == "/index.html") { + http.Redirect(w, r, "/timeout.html", http.StatusTemporaryRedirect) + return + } + if !isHTML(r.Header["Accept"]) { w.Header().Set("Cache-Control", "max-age=31536000") } else { diff --git a/api/http/middlewares/admin_monitor.go b/api/http/middlewares/admin_monitor.go deleted file mode 100644 index 4f5554976..000000000 --- a/api/http/middlewares/admin_monitor.go +++ /dev/null @@ -1,62 +0,0 @@ -package middlewares - -import ( - "log" - "net/http" - "strings" - "sync" - - httperror "github.com/portainer/libhttp/error" -) - -const RedirectReasonAdminInitTimeout string = "AdminInitTimeout" - -// AdminMonitor is an entity used to maintain the administrator initialization status -type AdminMonitor struct { - lock sync.RWMutex - adminInitDisabled bool -} - -// NewAdminMonitor creates a new gate wrapper -func NewAdminMonitor(timeoutSignal <-chan interface{}) *AdminMonitor { - monitor := &AdminMonitor{ - adminInitDisabled: false, - } - - go func() { - <-timeoutSignal - log.Println("[INFO] Please restart Portainer instance and initialize the administrator") - monitor.DisableInstance() - }() - return monitor -} - -func (o *AdminMonitor) DisableInstance() { - o.lock.Lock() - defer o.lock.Unlock() - o.adminInitDisabled = true -} - -func (o *AdminMonitor) WasDisabled() bool { - o.lock.RLock() - defer o.lock.RUnlock() - return o.adminInitDisabled -} - -// WithRedirect checks whether administrator initialisation timeout. If so, it will return the error with redirect reason. -// Otherwise, it will pass through the request to next -func (o *AdminMonitor) WithRedirect(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if o.WasDisabled() { - if r.RequestURI == "/" { - http.Redirect(w, r, "/timeout.html", http.StatusTemporaryRedirect) - } else if strings.HasPrefix(r.RequestURI, "/api") && r.RequestURI != "/api/status" && r.RequestURI != "/api/settings/public" { - w.Header().Set("redirect-reason", RedirectReasonAdminInitTimeout) - httperror.WriteError(w, http.StatusSeeOther, "Administrator initialization timeout", nil) - return - } - } - - next.ServeHTTP(w, r) - }) -} diff --git a/api/http/middlewares/admin_monitor_test.go b/api/http/middlewares/admin_monitor_test.go deleted file mode 100644 index 3719f5f45..000000000 --- a/api/http/middlewares/admin_monitor_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package middlewares - -import ( - "io" - "net/http" - "net/http/httptest" - "testing" - "time" -) - -func Test_beforeAdminInitDisabled(t *testing.T) { - // scenario: - // Before admin init timeout, no redirect happen - - timeoutSignal := make(chan interface{}) - adminMonitorMiddleware := NewAdminMonitor(timeoutSignal) - - request := httptest.NewRequest(http.MethodPost, "/api/settings/public", nil) - response := httptest.NewRecorder() - - go func() { - time.Sleep(2 * time.Second) - close(timeoutSignal) - }() - - adminMonitorMiddleware.WithRedirect(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("success")) - })).ServeHTTP(response, request) - - body, _ := io.ReadAll(response.Body) - if string(body) != "success" { - t.Error("Didn't receive expected result from the hanlder") - } -} - -func Test_afterAdminInitDisabled(t *testing.T) { - // scenario: - // After admin init timeout, redirect should happen - - timeoutSignal := make(chan interface{}) - adminMonitorMiddleware := NewAdminMonitor(timeoutSignal) - - request := httptest.NewRequest(http.MethodPost, "/api/users/admin/check", nil) - response := httptest.NewRecorder() - - go func() { - time.Sleep(100 * time.Millisecond) - close(timeoutSignal) - }() - - time.Sleep(300 * time.Millisecond) - adminMonitorMiddleware.WithRedirect(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - })).ServeHTTP(response, request) - - if response.Code != http.StatusTemporaryRedirect { - t.Error("Didn't redirect as expected") - } -} diff --git a/api/http/server.go b/api/http/server.go index 5b99c4406..ad07cafc4 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -51,7 +51,6 @@ import ( "github.com/portainer/portainer/api/http/handler/users" "github.com/portainer/portainer/api/http/handler/webhooks" "github.com/portainer/portainer/api/http/handler/websocket" - "github.com/portainer/portainer/api/http/middlewares" "github.com/portainer/portainer/api/http/offlinegate" "github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" @@ -119,9 +118,7 @@ func (server *Server) Start() error { authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager authHandler.OAuthService = server.OAuthService - initTimeoutSignal := make(chan interface{}) - adminMonitorMiddleware := middlewares.NewAdminMonitor(initTimeoutSignal) - adminMonitor := adminmonitor.New(5*time.Minute, server.DataStore, initTimeoutSignal, server.ShutdownCtx) + 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) @@ -179,7 +176,7 @@ func (server *Server) Start() error { var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.JWTService, server.KubeClusterAccessService, server.KubernetesClientFactory) - var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public")) + var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"), adminMonitor.WasInstanceDisabled) var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeClusterAccessService) @@ -303,7 +300,7 @@ func (server *Server) Start() error { WebhookHandler: webhookHandler, } - handler := adminMonitorMiddleware.WithRedirect(offlineGate.WaitingMiddleware(time.Minute, server.Handler)) + handler := adminMonitor.WithRedirect(offlineGate.WaitingMiddleware(time.Minute, server.Handler)) if server.HTTPEnabled { go func() { log.Printf("[INFO] [http,server] [message: starting HTTP server on port %s]", server.BindAddress) diff --git a/app/admin-timeout.html b/app/admin-timeout.html deleted file mode 100644 index 0856f7495..000000000 --- a/app/admin-timeout.html +++ /dev/null @@ -1,65 +0,0 @@ - - -
- -