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
Oscar Zhou 3 years ago committed by GitHub
parent 167825ff3f
commit 2059a9e064
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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…
Cancel
Save