Skip to content

Commit

Permalink
feat(ui): persist oidc auth details (#10389)
Browse files Browse the repository at this point in the history
  • Loading branch information
jayeshchoudhary authored Mar 13, 2023
1 parent 81ceb1d commit 3bad67d
Show file tree
Hide file tree
Showing 8 changed files with 488 additions and 213 deletions.
240 changes: 63 additions & 177 deletions pinot-controller/src/main/resources/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,49 +17,43 @@
* under the License.
*/

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { CircularProgress, createStyles, makeStyles, MuiThemeProvider } from '@material-ui/core';
import { Switch, Route, HashRouter as Router, Redirect } from 'react-router-dom';
import theme from './theme';
import React, { useEffect, useState } from 'react';
import { Switch, Route, Redirect, useHistory } from 'react-router-dom';
import Layout from './components/Layout';
import RouterData from './router';
import PinotMethodUtils from './utils/PinotMethodUtils';
import CustomNotification from './components/CustomNotification';
import { NotificationContextProvider } from './components/Notification/NotificationContextProvider';
import app_state from './app_state';
import { useAuthProvider } from './components/auth/AuthProvider';
import { AppLoadingIndicator } from './components/AppLoadingIndicator';
import { AuthWorkflow } from 'Models';

const useStyles = makeStyles(() =>
createStyles({
loader: {
position: 'fixed',
left: '50%',
top: '30%'
},
})
);
export const App = () => {
const [clusterName, setClusterName] = useState('');
const [loading, setLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(null);
const [role, setRole] = useState('');
const { authenticated, authWorkflow } = useAuthProvider();
const history = useHistory();

useEffect(() => {
// authentication already handled by authProvider
if (authenticated) {
setIsAuthenticated(true);
}
}, [authenticated]);

const App = () => {
const [clusterName, setClusterName] = React.useState('');
const [loading, setLoading] = React.useState(true);
const oidcSignInFormRef = React.useRef<HTMLFormElement>(null);
const [isAuthenticated, setIsAuthenticated] = React.useState(null);
const [issuer, setIssuer] = React.useState(null);
const [redirectUri, setRedirectUri] = React.useState(null);
const [clientId, setClientId] = React.useState(null);
const [authWorkflow, setAuthWorkflow] = React.useState(null);
const [authorizationEndpoint, setAuthorizationEndpoint] = React.useState(
null
);
const [role, setRole] = React.useState('');
useEffect(() => {
if(authWorkflow === AuthWorkflow.BASIC) {
setLoading(false);
}
}, [authWorkflow])

const fetchUserRole = async()=>{
const fetchUserRole = async () => {
const userListResponse = await PinotMethodUtils.getUserList();
let userObj = userListResponse.users;
let userData = [];
for (let key in userObj) {
if(userObj.hasOwnProperty(key)){
if (userObj.hasOwnProperty(key)) {
userData.push(userObj[key]);
}
}
Expand Down Expand Up @@ -91,94 +85,16 @@ const App = () => {
};

const getRouterData = () => {
if(app_state.queryConsoleOnlyView){
return RouterData.filter((routeObj)=>{return routeObj.path === '/query'});
if (app_state.queryConsoleOnlyView) {
return RouterData.filter((routeObj) => { return routeObj.path === '/query' });
}
if (app_state.hideQueryConsoleTab) {
return RouterData.filter((routeObj) => routeObj.path !== '/query');
}
return RouterData;
};

const getAuthInfo = async () => {
const authInfoResponse = await PinotMethodUtils.getAuthInfo();
// Issuer URL, if available
setIssuer(
authInfoResponse && authInfoResponse.issuer ? authInfoResponse.issuer : ''
);
// Redirect URI, if available
setRedirectUri(
authInfoResponse && authInfoResponse.redirectUri
? authInfoResponse.redirectUri
: ''
);
// Client Id, if available
setClientId(
authInfoResponse && authInfoResponse.clientId
? authInfoResponse.clientId
: ''
);
// Authentication workflow
setAuthWorkflow(
authInfoResponse && authInfoResponse.workflow
? authInfoResponse.workflow
: AuthWorkflow.NONE
);
};

const initAuthWorkflow = async () => {
switch (authWorkflow) {
case AuthWorkflow.NONE: {
// No authentication required
setIsAuthenticated(true);

break;
}
case AuthWorkflow.BASIC: {
// Basic authentication, handled by login page
setLoading(false);

break;
}
case AuthWorkflow.OIDC: {
// OIDC authentication, check to see if access token is available in the URL
const accessToken = PinotMethodUtils.getAccessTokenFromHashParams();
if (accessToken) {
app_state.authWorkflow = AuthWorkflow.OIDC;
app_state.authToken = accessToken;

setIsAuthenticated(true);
} else {
// Set authorization endpoint
setAuthorizationEndpoint(`${issuer}/auth`);

setLoading(false);
}

break;
}
default: {
// Empty
}
}
};

React.useEffect(() => {
getAuthInfo();
}, []);

React.useEffect(() => {
initAuthWorkflow();
}, [authWorkflow]);

React.useEffect(() => {
if (authorizationEndpoint && oidcSignInFormRef && oidcSignInFormRef.current) {
// Authorization endpoint available; submit sign in form
oidcSignInFormRef.current.submit();
}
}, [authorizationEndpoint]);

React.useEffect(() => {
useEffect(() => {
if (isAuthenticated) {
fetchClusterConfig();
fetchClusterName();
Expand All @@ -187,9 +103,14 @@ const App = () => {
}, [isAuthenticated]);

const loginRender = (Component, props) => {
if(isAuthenticated) {
history.push("/");
return;
}

return (
<div className="p-8">
<Component {...props} setIsAuthenticated={setIsAuthenticated}/>
<Component {...props} setIsAuthenticated={setIsAuthenticated} />
</div>
)
};
Expand All @@ -204,71 +125,36 @@ const App = () => {
)
};

const classes = useStyles();
if (loading) {
return <AppLoadingIndicator />;
}

return (
<MuiThemeProvider theme={theme}>
<NotificationContextProvider>
<CustomNotification />
{/* OIDC auth workflow */}
{authWorkflow && authWorkflow === AuthWorkflow.OIDC && !isAuthenticated ? (
<>
{/* OIDC sign in form */}
<form
hidden
action={authorizationEndpoint}
method="post"
ref={oidcSignInFormRef}
>
<input readOnly name="client_id" value={clientId} />
<input readOnly name="redirect_uri" value={redirectUri} />
<input readOnly name="scope" value="email openid" />
<input readOnly name="state" value="true-redirect-uri" />
<input readOnly name="response_type" value="id_token token" />
<input readOnly name="nonce" value="random_string" />
<input type="submit" value="" />
</form>
</>
) : (
<>
{/* Non-OIDC/authenticated workflow */}
{loading ? (
<CircularProgress className={classes.loader} size={80} />
) : (
<Router>
<Switch>
{getRouterData().map(({ path, Component }, key) => (
<Route
exact
path={path}
key={key}
render={(props) => {
if (path === '/login') {
return loginRender(Component, props);
} else if (isAuthenticated) {
// default render
return componentRender(Component, props, role);
} else {
return <Redirect to="/login" />;
}
}}
/>
))}
<Route path="*">
<Redirect
to={PinotMethodUtils.getURLWithoutAccessToken(
app_state.queryConsoleOnlyView ? '/query' : '/'
)}
/>
</Route>
</Switch>
</Router>
)}
</>
)}
</NotificationContextProvider>
</MuiThemeProvider>
<Switch>
{getRouterData().map(({ path, Component }, key) => (
<Route
exact
path={path}
key={key}
render={(props) => {
if (path === '/login') {
return loginRender(Component, props);
} else if (isAuthenticated) {
// default render
return componentRender(Component, props, role);
} else {
return <Redirect to="/login" />;
}
}}
/>
))}
<Route path="*">
<Redirect
to={PinotMethodUtils.getURLWithoutAccessToken(
app_state.queryConsoleOnlyView ? '/query' : '/'
)}
/>
</Route>
</Switch>
);
};

ReactDOM.render(<App />, document.getElementById('app'));
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import React from 'react';
import { CircularProgress, makeStyles } from "@material-ui/core";

export const useAppLoadingIndicatorStyles = makeStyles({
appLoadingIndicator: {
height: "100vh",
width: "100%",
display: "flex",
flex: 1,
alignItems: "center",
justifyContent: "center",
},
});

export const AppLoadingIndicator = () => {
const classes = useAppLoadingIndicatorStyles();
return (
<div
className={classes.appLoadingIndicator}
>
<CircularProgress size={80} color="primary" />
</div>
)
}
Loading

0 comments on commit 3bad67d

Please sign in to comment.