Merge pull request #11165 from codyrancher/nav-guards

Pulling out authentication code into router navigation guards
This commit is contained in:
codyrancher 2024-06-04 17:30:22 -07:00 committed by GitHub
commit 78f10c1c9f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 140 additions and 68 deletions

View File

@ -6,7 +6,7 @@ import BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.po';
*/
export default class MgmtUsersListPo extends BaseResourceList {
create() {
return this.masthead().actions().eq(0).click();
return cy.get('[data-testid="masthead-create"]').click();
}
refreshGroupMembership(): AsyncButtonPo {

View File

@ -11,10 +11,11 @@ const usersPo = new UsersPo('_');
const userCreate = usersPo.createEdit();
const sideNav = new ProductNavPo();
const runTimestamp = +new Date();
const runPrefix = `e2e-test-${ runTimestamp }`;
const downloadsFolder = Cypress.config('downloadsFolder');
const globalRoleName = `${ runPrefix }-my-global-role`;
let runTimestamp;
let runPrefix;
let globalRoleName;
describe('Roles', { tags: ['@usersAndAuths', '@adminUser'] }, () => {
beforeEach(() => {
@ -23,6 +24,12 @@ describe('Roles', { tags: ['@usersAndAuths', '@adminUser'] }, () => {
});
it('can create a Global Role', () => {
// We want to define these here because if this test fails after it created the global role all subsequent
// retries will reference the wrong global-role because a second roll will with the same name but different id will be created
runTimestamp = +new Date();
runPrefix = `e2e-test-${ runTimestamp }`;
globalRoleName = `${ runPrefix }-my-global-role`;
// create global role
const fragment = 'GLOBAL';

View File

@ -1,6 +1,7 @@
import Vue from 'vue';
import Router from 'vue-router';
import Routes from '@shell/config/router/routes';
import { installNavigationGuards } from '@shell/config/router/navigation-guards';
Vue.use(Router);
@ -12,9 +13,11 @@ export const routerOptions = {
fallback: false
};
export function extendRouter(config) {
export function extendRouter(config, context) {
const base = (config._app && config._app.basePath) || routerOptions.base;
const router = new Router({ ...routerOptions, base });
installNavigationGuards(router, context);
return router;
}

View File

@ -0,0 +1,71 @@
import { SETUP } from '@shell/config/query-params';
import { SETTING } from '@shell/config/settings';
import { MANAGEMENT, NORMAN } from '@shell/config/types';
import { tryInitialSetup } from '@shell/utils/auth';
import { routeNameMatched } from '@shell/utils/router';
export function install(router, context) {
router.beforeEach((from, to, next) => attemptFirstLogin(from, to, next, context));
}
export async function attemptFirstLogin(from, to, next, { store }) {
if (routeNameMatched(to, 'unauthenticated')) {
return next();
}
// Initial ?setup=admin-password can technically be on any route
let initialPass = to.query[SETUP];
let firstLogin = null;
try {
const res = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.FIRST_LOGIN);
const plSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.PL);
firstLogin = res?.value === 'true';
if (!initialPass && plSetting?.value === 'Harvester') {
initialPass = 'admin';
}
} catch (e) {
}
if ( firstLogin === null ) {
try {
const res = await store.dispatch('rancher/find', {
type: NORMAN.SETTING,
id: SETTING.FIRST_LOGIN,
opt: { url: `/v3/settings/${ SETTING.FIRST_LOGIN }` }
});
firstLogin = res?.value === 'true';
const plSetting = await store.dispatch('rancher/find', {
type: NORMAN.SETTING,
id: SETTING.PL,
opt: { url: `/v3/settings/${ SETTING.PL }` }
});
if (!initialPass && plSetting?.value === 'Harvester') {
initialPass = 'admin';
}
} catch (e) {
}
}
// TODO show error if firstLogin and default pass doesn't work
if ( firstLogin ) {
const ok = await tryInitialSetup(store, initialPass);
if (ok) {
if (initialPass) {
store.dispatch('auth/setInitialPass', initialPass);
}
return next({ name: 'auth-setup' });
} else {
return next({ name: 'auth-login' });
}
}
next();
}

View File

@ -0,0 +1,14 @@
import { install as installLoadInitialSettings } from '@shell/config/router/navigation-guards/load-initial-settings';
import { install as installAttemptFirstLogin } from '@shell/config/router/navigation-guards/attempt-first-login';
/**
* Install our router navigation guards. i.e. router.beforeEach(), router.afterEach()
*/
export function installNavigationGuards(router, context) {
// NOTE: the order of the installation matters.
// Be intentional when adding, removing or modifying the guards that are installed.
const navigationGuardInstallers = [installLoadInitialSettings, installAttemptFirstLogin];
navigationGuardInstallers.forEach((installer) => installer(router, context));
}

View File

@ -0,0 +1,13 @@
import { fetchInitialSettings } from '@shell/utils/settings';
export function install(router, context) {
router.beforeEach((from, to, next) => loadInitialSettings(from, to, next));
}
export async function loadInitialSettings(from, to, next) {
try {
await fetchInitialSettings();
} catch (ex) {}
next();
}

View File

@ -15,8 +15,8 @@ import { installInjectedPlugins } from 'initialize/install-plugins.js';
*/
async function extendApp(vueApp) {
const config = { rancherEnv: process.env.rancherEnv, dashboardVersion: process.env.version };
const router = extendRouter(config);
const store = extendStore();
const router = extendRouter(config, { store });
// Add this.$router into store actions/mutations
store.$router = router;

View File

@ -1,6 +1,4 @@
import { SETUP } from '@shell/config/query-params';
import { SETTING } from '@shell/config/settings';
import { MANAGEMENT, NORMAN, DEFAULT_WORKSPACE } from '@shell/config/types';
import { DEFAULT_WORKSPACE } from '@shell/config/types';
import { applyProducts } from '@shell/store/type-map';
import { ClusterNotFoundError, RedirectToError } from '@shell/utils/error';
import { get } from '@shell/utils/object';
@ -9,73 +7,15 @@ import { AFTER_LOGIN_ROUTE, WORKSPACE } from '@shell/store/prefs';
import { BACK_TO } from '@shell/config/local-storage';
import { NAME as FLEET_NAME } from '@shell/config/product/fleet.js';
import {
validateResource, setProduct, isLoggedIn, notLoggedIn, noAuth, tryInitialSetup, findMe
validateResource, setProduct, isLoggedIn, notLoggedIn, noAuth, findMe
} from '@shell/utils/auth';
import { getClusterFromRoute, getProductFromRoute, getPackageFromRoute } from '@shell/utils/router';
import { fetchInitialSettings } from '@shell/utils/settings';
let beforeEachSetup = false;
export default async function({
route, store, redirect, from, $plugin, next
}) {
// Initial ?setup=admin-password can technically be on any route
let initialPass = route.query[SETUP];
let firstLogin = null;
try {
// Load settings, which will either be just the public ones if not logged in, or all if you are
await fetchInitialSettings(store);
const res = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.FIRST_LOGIN);
const plSetting = store.getters['management/byId'](MANAGEMENT.SETTING, SETTING.PL);
firstLogin = res?.value === 'true';
if (!initialPass && plSetting?.value === 'Harvester') {
initialPass = 'admin';
}
} catch (e) {
}
if ( firstLogin === null ) {
try {
const res = await store.dispatch('rancher/find', {
type: NORMAN.SETTING,
id: SETTING.FIRST_LOGIN,
opt: { url: `/v3/settings/${ SETTING.FIRST_LOGIN }` }
});
firstLogin = res?.value === 'true';
const plSetting = await store.dispatch('rancher/find', {
type: NORMAN.SETTING,
id: SETTING.PL,
opt: { url: `/v3/settings/${ SETTING.PL }` }
});
if (!initialPass && plSetting?.value === 'Harvester') {
initialPass = 'admin';
}
} catch (e) {
}
}
// TODO show error if firstLogin and default pass doesn't work
if ( firstLogin ) {
const ok = await tryInitialSetup(store, initialPass);
if (ok) {
if (initialPass) {
store.dispatch('auth/setInitialPass', initialPass);
}
return redirect({ name: 'auth-setup' });
} else {
return redirect({ name: 'auth-login' });
}
}
if ( store.getters['auth/enabled'] !== false && !store.getters['auth/loggedIn'] ) {
// `await` so we have one successfully request whilst possibly logged in (ensures fromHeader is populated from `x-api-cattle-auth`)
await store.dispatch('auth/getUser');

View File

@ -75,6 +75,30 @@ export const getResourceFromRoute = (to) => {
return resource;
};
/**
* Given a route it will look through the matching parent routes to see if any match the fn (predicate) criteria
*
* @param {*} to a VueRouter Route object
* @param {*} fn fn is a predicate which is passed a matched route. It will return true to indicate there was a matching route and false otherwise
* @returns true if a matching route was found, false otherwise
*/
export const routeMatched = (to, fn) => {
const matched = to?.matched || [];
return !!matched.find(fn);
};
/**
* Given a route and a name it will look through the matching parent routes to see if any have the specified name
*
* @param {*} to a VueRouter Route object
* @param {*} routeName the name of a route you're checking to see if it was matched.
* @returns true if a matching route was found, false otherwise
*/
export const routeNameMatched = (to, routeName) => {
return routeMatched(to, (matched) => (matched?.name === routeName));
};
function findMeta(route, key) {
if (route?.meta) {
const meta = Array.isArray(route.meta) ? route.meta : [route.meta];