From ec9482bacb05deb8cef5a9efd9a8ae500822ee08 Mon Sep 17 00:00:00 2001 From: YanJin Date: Tue, 14 Dec 2021 16:57:41 +0100 Subject: [PATCH 1/6] ui: Add Error reducer to store the isAuthError --- ui/src/ducks/app/authError.js | 29 +++++++++++++++++++++++++++++ ui/src/ducks/reducer.js | 4 ++++ 2 files changed, 33 insertions(+) create mode 100644 ui/src/ducks/app/authError.js diff --git a/ui/src/ducks/app/authError.js b/ui/src/ducks/app/authError.js new file mode 100644 index 0000000000..d9f543ba32 --- /dev/null +++ b/ui/src/ducks/app/authError.js @@ -0,0 +1,29 @@ +//@flow +export type AuthErrorState = { + isAuthError: boolean, +}; + +type AuthErrorAction = { + type: 'AUTH_ERROR', +}; + +const defaultState: AuthErrorState = { + isAuthError: false, +}; + +export default function reducer( + state: AuthErrorState = defaultState, + action: AuthErrorAction, +) { + switch (action.type) { + case 'AUTH_ERROR': { + return { ...state, isAuthError: true }; + } + default: + return { ...state }; + } +} + +export function authErrorAction() { + return { type: 'AUTH_ERROR' }; +} diff --git a/ui/src/ducks/reducer.js b/ui/src/ducks/reducer.js index 7b7951a18f..ff4f961cbb 100644 --- a/ui/src/ducks/reducer.js +++ b/ui/src/ducks/reducer.js @@ -9,6 +9,8 @@ import pods from './app/pods'; import type { PodsState } from './app/pods'; import volumes from './app/volumes'; import type { VolumesState } from './app/volumes'; +import authError from './app/authError'; +import type { AuthErrorState } from './app/authError'; import login from './login'; import type { LoginState } from './login'; import layout from './app/layout'; @@ -31,6 +33,7 @@ const rootReducer = combineReducers({ salt, monitoring, volumes, + authError, }), oidc: oidcReducer, history: historyReducer, @@ -49,6 +52,7 @@ export type RootState = { layout: LayoutState, salt: SaltState, monitoring: MonitoringState, + authError: AuthErrorState, }, }; From ffa7c19455090963ba5585c84995f412adbc1362 Mon Sep 17 00:00:00 2001 From: YanJin Date: Tue, 14 Dec 2021 16:58:11 +0100 Subject: [PATCH 2/6] ui: Dispatch authErrorAction at the top level of redux --- ui/src/FederableApp.js | 10 +++++++++- ui/src/containers/PrivateRoute.js | 17 +++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/ui/src/FederableApp.js b/ui/src/FederableApp.js index 8b44f9c9d5..1968f3c795 100644 --- a/ui/src/FederableApp.js +++ b/ui/src/FederableApp.js @@ -18,13 +18,21 @@ import { useTypedSelector } from './hooks'; import { setHistory as setReduxHistory } from './ducks/history'; import { setApiConfigAction } from './ducks/config'; import { initToggleSideBarAction } from './ducks/app/layout'; +import { authErrorAction } from './ducks/app/authError'; +import { AuthError } from './services/errorhandler'; const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose; -const sagaMiddleware = createSagaMiddleware(); +const sagaMiddleware = createSagaMiddleware({ + onError: (error) => { + if (error instanceof AuthError) { + store.dispatch(authErrorAction()); + } + }, +}); const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware)); export const store = createStore(reducer, enhancer); diff --git a/ui/src/containers/PrivateRoute.js b/ui/src/containers/PrivateRoute.js index e93c2b8e57..004c5c024a 100644 --- a/ui/src/containers/PrivateRoute.js +++ b/ui/src/containers/PrivateRoute.js @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import { Route } from 'react-router-dom'; -import { ErrorPage500, ErrorPageAuth } from '@scality/core-ui'; +import { useHistory } from 'react-router'; +import { ErrorPage500, ErrorPageAuth, ErrorPage401 } from '@scality/core-ui'; import { useTypedSelector } from '../hooks'; import { ComponentWithFederatedImports } from '@scality/module-federation'; import { useDispatch } from 'react-redux'; @@ -13,16 +14,28 @@ const InternalPrivateRoute = ({ ...rest }) => { const { language, api } = useTypedSelector((state) => state.config); + const { isAuthError } = useTypedSelector((state) => state.app.authError); const url_support = api?.url_support; const { userData } = moduleExports['./auth/AuthProvider'].useAuth(); const dispatch = useDispatch(); + const history = useHistory(); useMemo(() => { dispatch(updateAPIConfigAction(userData)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [!userData]); - if (userData.token && userData.username) { + if (isAuthError) { + return ( + { + history.push('/'); + }} + /> + ); + } else if (userData.token && userData.username) { return ; } else { return ( From f7093d80fdec9061ea889f113fccf04c9dc1d591 Mon Sep 17 00:00:00 2001 From: YanJin Date: Tue, 14 Dec 2021 16:59:12 +0100 Subject: [PATCH 3/6] ui: Add handleUnAuthorizedError function to throw out Auth error --- ui/src/services/errorhandler.js | 13 ++++++++++++ ui/src/services/k8s/core.js | 23 +++++++++++---------- ui/src/services/k8s/volumes.js | 7 ++++--- ui/src/services/salt/api.js | 36 +++++++++++++++++++++++++-------- 4 files changed, 57 insertions(+), 22 deletions(-) create mode 100644 ui/src/services/errorhandler.js diff --git a/ui/src/services/errorhandler.js b/ui/src/services/errorhandler.js new file mode 100644 index 0000000000..46cfde5b36 --- /dev/null +++ b/ui/src/services/errorhandler.js @@ -0,0 +1,13 @@ +export class AuthError extends Error {} + +export function handleUnAuthorizedError({ error }) { + if ( + error?.response?.statusCode === 401 || + error?.response?.statusCode === 403 || + error?.response?.status === 401 || + error?.response?.status === 403 + ) { + throw new AuthError(); + } + return { error }; +} diff --git a/ui/src/services/k8s/core.js b/ui/src/services/k8s/core.js index 93272ba56e..0b8d85e684 100644 --- a/ui/src/services/k8s/core.js +++ b/ui/src/services/k8s/core.js @@ -1,10 +1,11 @@ +import { handleUnAuthorizedError } from '../errorhandler'; import { coreV1, appsV1 } from './api'; export async function getNodes() { try { return await coreV1.listNode(); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } @@ -12,7 +13,7 @@ export async function getPods() { try { return await coreV1.listPodForAllNamespaces(); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } @@ -25,7 +26,7 @@ export async function getKubeSystemNamespace() { 'metadata.name=kube-system', ); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } @@ -33,7 +34,7 @@ export async function createNode(payload) { try { return await coreV1.createNode(payload); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } @@ -57,7 +58,7 @@ export async function listNamespaces({ options, ); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } @@ -72,7 +73,7 @@ export async function queryPodInNamespace(namespace, podLabel) { `app=${podLabel}`, ); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } @@ -86,7 +87,7 @@ export async function createNamespacedConfigMap(name, namespace, restProps) { try { return await coreV1.createNamespacedConfigMap(namespace, body); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } @@ -115,7 +116,7 @@ export async function patchNamespacedConfigMap( { headers: { 'Content-Type': cTypeHeader } }, ); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } @@ -123,7 +124,7 @@ export async function getNamespacedDeployment(name, namespace) { try { return await appsV1.readNamespacedDeployment(name, namespace); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } @@ -131,7 +132,7 @@ export async function readNode(name) { try { return await coreV1.readNode(name); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } @@ -139,6 +140,6 @@ export async function readNamespacedConfigMap(nameConfigMap, namespace) { try { return await coreV1.readNamespacedConfigMap(nameConfigMap, namespace); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } diff --git a/ui/src/services/k8s/volumes.js b/ui/src/services/k8s/volumes.js index 7ad9430df2..7e42ddfcd1 100644 --- a/ui/src/services/k8s/volumes.js +++ b/ui/src/services/k8s/volumes.js @@ -1,4 +1,5 @@ //@flow +import { handleUnAuthorizedError } from '../errorhandler'; import { coreV1, storage } from './api'; export async function getPersistentVolumes() { @@ -8,7 +9,7 @@ export async function getPersistentVolumes() { try { return await coreV1.listPersistentVolume(); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } @@ -19,7 +20,7 @@ export async function getStorageClass() { try { return await storage.listStorageClass(); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } @@ -30,6 +31,6 @@ export async function getPersistentVolumeClaims() { try { return await coreV1.listPersistentVolumeClaimForAllNamespaces(); } catch (error) { - return { error }; + return handleUnAuthorizedError({ error }); } } diff --git a/ui/src/services/salt/api.js b/ui/src/services/salt/api.js index 6d0dbc4609..a7b482b84b 100644 --- a/ui/src/services/salt/api.js +++ b/ui/src/services/salt/api.js @@ -1,6 +1,7 @@ //@flow import { User } from 'oidc-client'; import ApiClient from '../ApiClient'; +import { handleUnAuthorizedError } from '../errorhandler'; let saltApiClient = null; @@ -25,7 +26,7 @@ export type SaltToken = { ], }; -export async function authenticate(user: User): Promise { +export async function authenticate(user: User) { if (!saltApiClient) { throw new Error('Salt api client should be defined.'); } @@ -34,14 +35,20 @@ export async function authenticate(user: User): Promise { username: `oidc:${user.email}`, token: user.token, }; - return saltApiClient.post('/login', payload); + + const result = await saltApiClient.post('/login', payload); + if (result.error) { + return handleUnAuthorizedError({ error: result.error }); + } else { + return result; + } } export async function deployNode(node: string, version: string) { if (!saltApiClient) { throw new Error('Salt api client should be defined.'); } - return saltApiClient.post('/', { + const result = saltApiClient.post('/', { client: 'runner_async', fun: 'state.orchestrate', arg: ['metalk8s.orchestrate.deploy_node'], @@ -50,17 +57,27 @@ export async function deployNode(node: string, version: string) { pillar: { orchestrate: { node_name: node } }, }, }); + if (result.error) { + return handleUnAuthorizedError({ error: result.error }); + } else { + return result; + } } export async function printJob(jid: string) { if (!saltApiClient) { throw new Error('Salt api client should be defined.'); } - return saltApiClient.post('/', { + const result = saltApiClient.post('/', { client: 'runner', fun: 'jobs.print_job', arg: [jid], }); + if (result.error) { + return handleUnAuthorizedError({ error: result.error }); + } else { + return result; + } } export type IPInterfaces = { @@ -94,15 +111,13 @@ We may get error message instead of IPInterfaces Object } */ -export async function getNodesIPsInterfaces( - nodeNames: string[], -): Promise<{ +export async function getNodesIPsInterfaces(nodeNames: string[]): Promise<{ return: [{ [nodeName: string]: boolean | IPInterfaces | string }], }> { if (!saltApiClient) { throw new Error('Salt api client should be defined.'); } - return saltApiClient.post('/', { + const result = saltApiClient.post('/', { client: 'local', tgt: nodeNames.join(','), tgt_type: 'list', @@ -113,4 +128,9 @@ export async function getNodesIPsInterfaces( 'ip_interfaces', ], }); + if (result.error) { + return handleUnAuthorizedError({ error: result.error }); + } else { + return result; + } } From ecbbea1d951f955d9d59bf670725e23b068d10ee Mon Sep 17 00:00:00 2001 From: YanJin Date: Tue, 14 Dec 2021 17:09:04 +0100 Subject: [PATCH 4/6] ui/package: Bump the core-ui version to v0.25.0 to have the new 401 page --- shell-ui/package-lock.json | 4 ++-- shell-ui/package.json | 2 +- ui/package-lock.json | 4 ++-- ui/package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/shell-ui/package-lock.json b/shell-ui/package-lock.json index 11e421e420..f8dd28127d 100644 --- a/shell-ui/package-lock.json +++ b/shell-ui/package-lock.json @@ -1958,8 +1958,8 @@ "dev": true }, "@scality/core-ui": { - "version": "github:scality/core-ui#877d6440521a3a3fcd00fb88d0ce7039bf9b221f", - "from": "github:scality/core-ui#v0.24.2" + "version": "github:scality/core-ui#9f1d3de45649ee0c420b1a804b77d15cb2d9c363", + "from": "github:scality/core-ui#v0.25.0" }, "@scality/module-federation": { "version": "github:scality/module-federation#a4f1cf882646c8d859b9081dc8b66e5647962456", diff --git a/shell-ui/package.json b/shell-ui/package.json index 4fd890d4d4..0c5db2f04b 100644 --- a/shell-ui/package.json +++ b/shell-ui/package.json @@ -47,7 +47,7 @@ "webpack-dev-server": "^3.11.2" }, "dependencies": { - "@scality/core-ui": "github:scality/core-ui.git#v0.24.2", + "@scality/core-ui": "github:scality/core-ui.git#v0.25.0", "@scality/module-federation": "github:scality/module-federation.git#1.0.0", "oidc-client": "^1.11.3", "oidc-react": "^1.1.5", diff --git a/ui/package-lock.json b/ui/package-lock.json index 08890d8c66..aadcb4ea02 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -4708,8 +4708,8 @@ } }, "@scality/core-ui": { - "version": "github:scality/core-ui#877d6440521a3a3fcd00fb88d0ce7039bf9b221f", - "from": "github:scality/core-ui#v0.24.2" + "version": "github:scality/core-ui#9f1d3de45649ee0c420b1a804b77d15cb2d9c363", + "from": "github:scality/core-ui#v0.25.0" }, "@scality/module-federation": { "version": "github:scality/module-federation#a4f1cf882646c8d859b9081dc8b66e5647962456", diff --git a/ui/package.json b/ui/package.json index 013bb79eb7..e85355d1e0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,7 +9,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/react-fontawesome": "^0.1.14", "@kubernetes/client-node": "github:scality/kubernetes-client-javascript.git#browser-0.10.2-63-g579d066", - "@scality/core-ui": "github:scality/core-ui.git#v0.24.2", + "@scality/core-ui": "github:scality/core-ui.git#v0.25.0", "@scality/module-federation": "github:scality/module-federation.git#1.0.0", "axios": "^0.21.1", "formik": "2.2.5", From 90edfd9c55cdf8d0d28f96bab26a3e02c99b7418 Mon Sep 17 00:00:00 2001 From: YanJin Date: Tue, 14 Dec 2021 17:17:12 +0100 Subject: [PATCH 5/6] Changelog: Handle 401 unauthorized error in MetalK8s UI --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50784ee089..532c067586 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,8 @@ highly-available when we have multiple Prometheus instances (PR[#3573](https://github.com/scality/metalk8s/pull/3573)) +- Handle 401 unauthorized error in MetalK8s UI + (PR[#3640](https://github.com/scality/metalk8s/pull/3640)) ## Bug fixes - [#3601](https://github.com/scality/metalk8s/issues/3601) - Marks From 2eb5c9c61dc5cc1585a37ce31e166a62b614b76a Mon Sep 17 00:00:00 2001 From: YanJin Date: Thu, 16 Dec 2021 15:18:32 +0100 Subject: [PATCH 6/6] shell-ui: Add style to top level container Following the changing in Errorpage from 100vh to 100%, we have to make sure the error page takes the remaining entire height --- shell-ui/src/FederatedApp.jsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/shell-ui/src/FederatedApp.jsx b/shell-ui/src/FederatedApp.jsx index 102ddb7306..9b20ba44d6 100644 --- a/shell-ui/src/FederatedApp.jsx +++ b/shell-ui/src/FederatedApp.jsx @@ -37,11 +37,7 @@ import { useConfigRetriever, useDiscoveredViews, } from './initFederation/ConfigurationProviders'; -import { - Route, - Switch, - Router, -} from 'react-router-dom'; +import { Route, Switch, Router } from 'react-router-dom'; import { ShellConfigProvider, useShellConfig, @@ -203,13 +199,17 @@ function WithInitFederationProviders({ children }: { children: Node }) { export default function App(): Node { return ( - - - - - - - +
+ + + + + + + +
); }