From 1a65dbf85fe24e76a774aaf226f9849e9a177569 Mon Sep 17 00:00:00 2001 From: xAt0mZ Date: Tue, 26 Nov 2019 05:01:39 +0100 Subject: [PATCH] fix(app): permissions lost for UI on browser refresh (#3354) * fix(app): permissions lost for UI on browser refresh * fix(app): permissions retrieval moved to global app resolve --- app/app.js | 47 +++-------------- app/portainer/__module.js | 61 ++++++++++++++++++---- app/portainer/services/authentication.js | 54 ++++++++++--------- app/portainer/services/stateManager.js | 20 +++---- app/portainer/views/auth/authController.js | 13 ----- 5 files changed, 92 insertions(+), 103 deletions(-) diff --git a/app/app.js b/app/app.js index 7b3419893..95cf796bf 100644 --- a/app/app.js +++ b/app/app.js @@ -1,27 +1,13 @@ -import _ from 'lodash-es'; import $ from 'jquery'; import '@babel/polyfill' angular.module('portainer') -.run(['$rootScope', '$state', '$interval', 'LocalStorage', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper', -function ($rootScope, $state, $interval, LocalStorage, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) { +.run(['$rootScope', '$state', '$interval', 'LocalStorage', 'EndpointProvider', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper', +function ($rootScope, $state, $interval, LocalStorage, EndpointProvider, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) { 'use strict'; EndpointProvider.initialize(); - StateManager.initialize() - .then(function success(state) { - if (state.application.authentication) { - initAuthentication(authManager, Authentication, $rootScope, $state); - } - if (state.application.analytics) { - initAnalytics(Analytics, $rootScope); - } - }) - .catch(function error(err) { - Notifications.error('Failure', err, 'Unable to retrieve application settings'); - }); - $rootScope.$state = $state; // Workaround to prevent the loading bar from going backward @@ -37,6 +23,10 @@ function ($rootScope, $state, $interval, LocalStorage, Authentication, authManag HttpRequestHelper.resetAgentHeaders(); }); + $state.defaultErrorHandler(function() { + // Do not log transitionTo errors + }); + // Keep-alive Edge endpoints by sending a ping request every minute $interval(function() { ping(EndpointProvider, SystemService); @@ -58,28 +48,3 @@ function ping(EndpointProvider, SystemService) { SystemService.ping(endpoint.Id); } } - -function initAuthentication(authManager, Authentication, $rootScope, $state) { - authManager.checkAuthOnRefresh(); - Authentication.init(); - - // The unauthenticated event is broadcasted by the jwtInterceptor when - // hitting a 401. We're using this instead of the usual combination of - // authManager.redirectWhenUnauthenticated() + unauthenticatedRedirector - // to have more controls on which URL should trigger the unauthenticated state. - $rootScope.$on('unauthenticated', function (event, data) { - if (!_.includes(data.config.url, '/v2/') && !_.includes(data.config.url, '/api/v4/')) { - $state.go('portainer.auth', { error: 'Your session has expired' }); - } - }); -} - -function initAnalytics(Analytics, $rootScope) { - Analytics.offline(false); - Analytics.registerScriptTags(); - Analytics.registerTrackers(); - $rootScope.$on('$stateChangeSuccess', function (event, toState) { - Analytics.trackPage(toState.url); - Analytics.pageView(); - }); -} diff --git a/app/portainer/__module.js b/app/portainer/__module.js index 5d96da47a..0d61052b2 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -1,3 +1,30 @@ +import _ from 'lodash-es'; + +async function initAuthentication(authManager, Authentication, $rootScope, $state) { + authManager.checkAuthOnRefresh(); + // The unauthenticated event is broadcasted by the jwtInterceptor when + // hitting a 401. We're using this instead of the usual combination of + // authManager.redirectWhenUnauthenticated() + unauthenticatedRedirector + // to have more controls on which URL should trigger the unauthenticated state. + $rootScope.$on('unauthenticated', function (event, data) { + if (!_.includes(data.config.url, '/v2/') && !_.includes(data.config.url, '/api/v4/')) { + $state.go('portainer.auth', { error: 'Your session has expired' }); + } + }); + + await Authentication.init(); +} + +function initAnalytics(Analytics, $rootScope) { + Analytics.offline(false); + Analytics.registerScriptTags(); + Analytics.registerTrackers(); + $rootScope.$on('$stateChangeSuccess', function (event, toState) { + Analytics.trackPage(toState.url); + Analytics.pageView(); + }); +} + angular.module('portainer.app', []) .config(['$stateRegistryProvider', function ($stateRegistryProvider) { 'use strict'; @@ -6,10 +33,30 @@ angular.module('portainer.app', []) name: 'root', abstract: true, resolve: { - requiresLogin: ['StateManager', function (StateManager) { - var applicationState = StateManager.getState(); - return applicationState.application.authentication; - }] + initStateManager: ['StateManager', 'Authentication', 'Notifications', 'Analytics', 'authManager', '$rootScope', '$state', '$async', '$q', + (StateManager, Authentication, Notifications, Analytics, authManager, $rootScope, $state, $async, $q) => { + const deferred = $q.defer(); + const appState = StateManager.getState(); + if (!appState.loading) { + deferred.resolve(); + } else { + StateManager.initialize() + .then(function success(state) { + if (state.application.analytics) { + initAnalytics(Analytics, $rootScope); + } + if (state.application.authentication) { + return $async(initAuthentication, authManager, Authentication, $rootScope, $state); + } + }) + .then(() => deferred.resolve()) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve application settings'); + deferred.reject(err); + }); + } + return deferred.promise; + }] }, views: { 'sidebar@': { @@ -60,9 +107,6 @@ angular.module('portainer.app', []) controllerAs: 'ctrl' }, 'sidebar@': {} - }, - data: { - requiresLogin: false } }; @@ -170,9 +214,6 @@ angular.module('portainer.app', []) name: 'portainer.init', abstract: true, url: '/init', - data: { - requiresLogin: false - }, views: { 'sidebar@': {} } diff --git a/app/portainer/services/authentication.js b/app/portainer/services/authentication.js index 6272b72b2..154ed4ece 100644 --- a/app/portainer/services/authentication.js +++ b/app/portainer/services/authentication.js @@ -1,7 +1,6 @@ angular.module('portainer.app') -.factory('Authentication', [ -'$async', 'Auth', 'OAuth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider', 'UserService', -function AuthenticationFactory($async, Auth, OAuth, jwtHelper, LocalStorage, StateManager, EndpointProvider, UserService) { +.factory('Authentication', ['$async', '$state', 'Auth', 'OAuth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider', 'UserService', +function AuthenticationFactory($async, $state, Auth, OAuth, jwtHelper, LocalStorage, StateManager, EndpointProvider, UserService) { 'use strict'; var service = {}; @@ -15,19 +14,32 @@ function AuthenticationFactory($async, Auth, OAuth, jwtHelper, LocalStorage, Sta service.getUserDetails = getUserDetails; service.isAdmin = isAdmin; service.hasAuthorizations = hasAuthorizations; - service.retrievePermissions = retrievePermissions; + + async function initAsync() { + try { + const jwt = LocalStorage.getJWT(); + if (jwt) { + await setUser(jwt); + } + } catch (error) { + throw error; + } + } + + function logout() { + StateManager.clean(); + EndpointProvider.clean(); + LocalStorage.clean(); + LocalStorage.storeLoginStateUUID(''); + } function init() { - var jwt = LocalStorage.getJWT(); - - if (jwt) { - setUser(jwt); - } + return $async(initAsync); } async function OAuthLoginAsync(code) { const response = await OAuth.validate({ code: code }).$promise; - setUser(response.jwt); + await setUser(response.jwt); } function OAuthLogin(code) { @@ -36,20 +48,13 @@ function AuthenticationFactory($async, Auth, OAuth, jwtHelper, LocalStorage, Sta async function loginAsync(username, password) { const response = await Auth.login({ username: username, password: password }).$promise; - setUser(response.jwt); + await setUser(response.jwt); } function login(username, password) { return $async(loginAsync, username, password); } - function logout() { - StateManager.clean(); - EndpointProvider.clean(); - LocalStorage.clean(); - LocalStorage.storeLoginStateUUID(''); - } - function isAuthenticated() { var jwt = LocalStorage.getJWT(); return jwt && !jwtHelper.isTokenExpired(jwt); @@ -59,20 +64,19 @@ function AuthenticationFactory($async, Auth, OAuth, jwtHelper, LocalStorage, Sta return user; } - function retrievePermissions() { - return UserService.user(user.ID) - .then((data) => { - user.endpointAuthorizations = data.EndpointAuthorizations; - user.portainerAuthorizations = data.PortainerAuthorizations; - }); + async function retrievePermissions() { + const data = await UserService.user(user.ID); + user.endpointAuthorizations = data.EndpointAuthorizations; + user.portainerAuthorizations = data.PortainerAuthorizations; } - function setUser(jwt) { + async function setUser(jwt) { LocalStorage.storeJWT(jwt); var tokenPayload = jwtHelper.decodeToken(jwt); user.username = tokenPayload.username; user.ID = tokenPayload.id; user.role = tokenPayload.role; + await retrievePermissions(); } function isAdmin() { diff --git a/app/portainer/services/stateManager.js b/app/portainer/services/stateManager.js index 65ee29476..f2fd53f35 100644 --- a/app/portainer/services/stateManager.js +++ b/app/portainer/services/stateManager.js @@ -2,8 +2,8 @@ import _ from 'lodash-es'; import moment from 'moment'; angular.module('portainer.app') -.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'LocalStorage', 'SettingsService', 'StatusService', 'APPLICATION_CACHE_VALIDITY', 'AgentPingService', -function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, SettingsService, StatusService, APPLICATION_CACHE_VALIDITY, AgentPingService) { +.factory('StateManager', ['$q', 'SystemService', 'InfoHelper', 'EndpointProvider', 'LocalStorage', 'SettingsService', 'StatusService', 'APPLICATION_CACHE_VALIDITY', 'AgentPingService', +function StateManagerFactory($q, SystemService, InfoHelper, EndpointProvider, LocalStorage, SettingsService, StatusService, APPLICATION_CACHE_VALIDITY, AgentPingService) { 'use strict'; var manager = {}; @@ -124,12 +124,8 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin var cacheValidity = now - applicationState.validity; if (cacheValidity > APPLICATION_CACHE_VALIDITY) { loadApplicationState() - .then(function success() { - deferred.resolve(state); - }) - .catch(function error(err) { - deferred.reject(err); - }); + .then(() => deferred.resolve(state)) + .catch((err) => deferred.reject(err)); } else { state.application = applicationState; state.loading = false; @@ -137,12 +133,8 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin } } else { loadApplicationState() - .then(function success() { - deferred.resolve(state); - }) - .catch(function error(err) { - deferred.reject(err); - }); + .then(() => deferred.resolve(state)) + .catch((err) => deferred.reject(err)); } return deferred.promise; diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index 27a625785..db9215a0a 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -32,7 +32,6 @@ class AuthenticationController { }; this.retrieveAndSaveEnabledExtensionsAsync = this.retrieveAndSaveEnabledExtensionsAsync.bind(this); - this.retrievePermissionsAsync = this.retrievePermissionsAsync.bind(this); this.checkForEndpointsAsync = this.checkForEndpointsAsync.bind(this); this.checkForLatestVersionAsync = this.checkForLatestVersionAsync.bind(this); this.postLoginSteps = this.postLoginSteps.bind(this); @@ -103,16 +102,6 @@ class AuthenticationController { * POST LOGIN STEPS SECTION */ - async retrievePermissionsAsync() { - try { - await this.Authentication.retrievePermissions(); - } catch (err) { - this.state.permissionsError = true; - this.logout(); - this.error(err, 'Unable to retrieve permissions.'); - } - } - async retrieveAndSaveEnabledExtensionsAsync() { try { await this.ExtensionService.retrieveAndSaveEnabledExtensions(); @@ -154,7 +143,6 @@ class AuthenticationController { } async postLoginSteps() { - await this.retrievePermissionsAsync(); await this.retrieveAndSaveEnabledExtensionsAsync(); await this.checkForEndpointsAsync(false); await this.checkForLatestVersionAsync(); @@ -264,7 +252,6 @@ class AuthenticationController { if (this.$stateParams.logout || this.$stateParams.error) { this.logout(); this.state.AuthenticationError = this.$stateParams.error; - return; } if (this.Authentication.isAuthenticated()) {