mirror of https://github.com/portainer/portainer
feat(adminmonitor): redirect to timeout page if admin is not created in 5 mins [EE-2691] (#6688)
This PR solves the issue that the Portainer instance will be always accessible in certain cases, like `restart: always` setting with docker run, even if the administrator is not created in the first 5 minutes. The solution is that the user will be redirected to a timeout page when any actions, such as refresh the page and click button, are made after administrator initialisation window(5 minutes) timeout.pull/6709/head
parent
167825ff3f
commit
2059a9e064
|
@ -3,29 +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
|
||||
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 shutdown the application unless it has been initialized.
|
||||
// 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, shutdownCtx context.Context) *Monitor {
|
||||
return &Monitor{
|
||||
timeout: timeout,
|
||||
datastore: datastore,
|
||||
shutdownCtx: shutdownCtx,
|
||||
timeout: timeout,
|
||||
datastore: datastore,
|
||||
shutdownCtx: shutdownCtx,
|
||||
adminInitDisabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,7 +57,11 @@ func (m *Monitor) Start() {
|
|||
logFatalf("%s", err)
|
||||
}
|
||||
if !initialized {
|
||||
logFatalf("[FATAL] [internal,init] No administrator account was created in %f mins. Shutting down the Portainer instance for security reasons", m.timeout.Minutes())
|
||||
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")
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.adminInitDisabled = true
|
||||
return
|
||||
}
|
||||
case <-cancellationCtx.Done():
|
||||
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
|
||||
|
@ -80,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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -42,28 +42,13 @@ func Test_canStopStartedMonitor(t *testing.T) {
|
|||
assert.Nil(t, monitor.cancellationFunc, "cancellation function should absent in stopped monitor")
|
||||
}
|
||||
|
||||
func Test_start_shouldFatalAfterTimeout_ifNotInitialized(t *testing.T) {
|
||||
func Test_start_shouldDisableInstanceAfterTimeout_ifNotInitialized(t *testing.T) {
|
||||
timeout := 10 * time.Millisecond
|
||||
|
||||
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
|
||||
|
||||
ch := make(chan struct{})
|
||||
var fataled bool
|
||||
origLogFatalf := logFatalf
|
||||
|
||||
logFatalf = func(s string, v ...interface{}) {
|
||||
fataled = true
|
||||
close(ch)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
logFatalf = origLogFatalf
|
||||
}()
|
||||
|
||||
monitor := New(timeout, datastore, context.Background())
|
||||
monitor.Start()
|
||||
<-time.After(2 * timeout)
|
||||
<-ch
|
||||
|
||||
assert.True(t, fataled, "monitor should been timeout and fatal")
|
||||
<-time.After(20 * timeout)
|
||||
assert.True(t, monitor.WasInstanceDisabled(), "monitor should have been timeout and instance is disabled")
|
||||
}
|
||||
|
|
|
@ -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,18 @@ func isHTML(acceptContent []string) bool {
|
|||
}
|
||||
|
||||
func (handler *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if handler.wasInstanceDisabled() {
|
||||
if r.RequestURI == "/" || r.RequestURI == "/index.html" {
|
||||
http.Redirect(w, r, "/timeout.html", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if strings.HasPrefix(r.RequestURI, "/timeout.html") {
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !isHTML(r.Header["Accept"]) {
|
||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||
} else {
|
||||
|
|
|
@ -176,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)
|
||||
|
||||
|
@ -300,8 +300,7 @@ func (server *Server) Start() error {
|
|||
WebhookHandler: webhookHandler,
|
||||
}
|
||||
|
||||
handler := 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)
|
||||
|
|
|
@ -15,6 +15,7 @@ export function configApp($urlRouterProvider, $httpProvider, localStorageService
|
|||
return LocalStorage.getJWT();
|
||||
},
|
||||
});
|
||||
|
||||
$httpProvider.interceptors.push('jwtInterceptor');
|
||||
$httpProvider.interceptors.push('EndpointStatusInterceptor');
|
||||
$httpProvider.defaults.headers.post['Content-Type'] = 'application/json';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import axiosOrigin, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
import { loadProgressBar } from 'axios-progress-bar';
|
||||
import 'axios-progress-bar/dist/nprogress.css';
|
||||
|
||||
import 'axios-progress-bar/dist/nprogress.css';
|
||||
import PortainerError from '@/portainer/error';
|
||||
import { get as localStorageGet } from '@/portainer/hooks/useLocalStorage';
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ angular.module('portainer.app').controller('InitAdminController', [
|
|||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
handleError(err);
|
||||
Notifications.error('Failure', err, 'Unable to create administrator user');
|
||||
})
|
||||
.finally(function final() {
|
||||
|
@ -67,6 +68,16 @@ angular.module('portainer.app').controller('InitAdminController', [
|
|||
});
|
||||
};
|
||||
|
||||
function handleError(err) {
|
||||
if (err.status === 303) {
|
||||
const headers = err.headers();
|
||||
const REDIRECT_REASON_TIMEOUT = 'AdminInitTimeout';
|
||||
if (headers && headers['redirect-reason'] === REDIRECT_REASON_TIMEOUT) {
|
||||
window.location.href = '/timeout.html';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createAdministratorFlow() {
|
||||
UserService.administratorExists()
|
||||
.then(function success(exists) {
|
||||
|
@ -94,6 +105,7 @@ angular.module('portainer.app').controller('InitAdminController', [
|
|||
try {
|
||||
await restoreAsyncFn();
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
Notifications.error('Failure', err, 'Unable to restore the backup');
|
||||
$scope.state.backupInProgress = false;
|
||||
|
||||
|
@ -105,6 +117,7 @@ angular.module('portainer.app').controller('InitAdminController', [
|
|||
Notifications.success('The backup has successfully been restored');
|
||||
$state.go('portainer.auth');
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
Notifications.error('Failure', err, 'Unable to check for status');
|
||||
await wait(2);
|
||||
location.reload();
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" ng-app="<%= name %>" ng-strict-di>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Portainer</title>
|
||||
<meta name="description" content="" />
|
||||
<meta name="author" content="<%= author %>" />
|
||||
<base id="base" />
|
||||
|
||||
<!-- Fav and touch icons -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<%=require('./assets/ico/apple-touch-icon.png')%>" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="<%=require('./assets/ico/favicon-32x32.png')%>" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="<%=require('./assets/ico/favicon-16x16.png')%>" />
|
||||
<link rel="mask-icon" href="<%=require('./assets/ico/safari-pinned-tab.svg')%>" color="#5bbad5" />
|
||||
<link rel="shortcut icon" href="<%=require('./assets/ico/favicon.ico')%>" />
|
||||
<meta name="msapplication-config" content="<%=require('./assets/ico/browserconfig.xml')%>" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page-wrapper">
|
||||
<!-- timeout info box -->
|
||||
<div class="container simple-box">
|
||||
<div class="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1">
|
||||
<!-- simple box logo -->
|
||||
<div class="row">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo" />
|
||||
<img ng-if="!logo" src="<%= require('./assets/images/logo_alt.svg') %>" class="simple-box-logo" alt="Portainer" />
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<!-- toggle -->
|
||||
<div style="padding-bottom: 24px">
|
||||
<a>
|
||||
<span style="padding-left: 10px">New Portainer installation</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- init admin init timeout notification -->
|
||||
<div class="simple-box" style="padding-left: 30px">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted" style="margin-left: 2px">
|
||||
Your Portainer instance timed out for security purposes. To re-enable your Portainer instance, you will need to restart Portainer.
|
||||
</span>
|
||||
<br /><br />
|
||||
<span class="text-muted small" style="margin-left: 2px">
|
||||
For further information, view our <a href="https://docs.portainer.io/v/ce-2.11/start/install" target="_blank">documentation</a>.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !init admin init timeout notification -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- !timeout info box -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -108,6 +108,14 @@ module.exports = {
|
|||
},
|
||||
manifest: './assets/ico/manifest.json',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './app/timeout.ejs',
|
||||
filename: 'timeout.html',
|
||||
templateParameters: {
|
||||
name: pkg.name,
|
||||
author: pkg.author,
|
||||
},
|
||||
}),
|
||||
new WebpackBuildNotifierPlugin({
|
||||
title: 'Portainer build',
|
||||
logo: path.resolve('./assets/favicon-32x32.png'),
|
||||
|
|
Loading…
Reference in New Issue