From dbd715a37295b25781013cd8cbffa085341d79a2 Mon Sep 17 00:00:00 2001 From: d-g-town <66391417+d-g-town@users.noreply.github.com> Date: Thu, 2 May 2024 11:01:25 -0400 Subject: [PATCH] dgtown/auth 2 (#4595) --- api/server/router/addons.go | 1 - dashboard/.eslintrc.json | 7 +- dashboard/src/main/Main.tsx | 251 +++--------------- dashboard/src/main/MainWrapper.tsx | 33 ++- dashboard/src/main/auth/Login.tsx | 4 +- dashboard/src/main/auth/Register.tsx | 4 +- dashboard/src/main/auth/SetInfo.tsx | 4 +- dashboard/src/shared/auth/AuthnContext.tsx | 186 +++++++++++++ .../src/shared/auth/AuthorizationHoc.tsx | 6 +- .../{AuthContext.tsx => AuthzContext.tsx} | 22 +- dashboard/src/shared/auth/RouteGuard.tsx | 52 ++-- dashboard/src/shared/auth/useAuth.ts | 14 +- dashboard/src/test-utils.tsx | 4 +- 13 files changed, 299 insertions(+), 289 deletions(-) create mode 100644 dashboard/src/shared/auth/AuthnContext.tsx rename dashboard/src/shared/auth/{AuthContext.tsx => AuthzContext.tsx} (75%) diff --git a/api/server/router/addons.go b/api/server/router/addons.go index 07e0b55378..45ad17144a 100644 --- a/api/server/router/addons.go +++ b/api/server/router/addons.go @@ -54,6 +54,5 @@ func getAddonRoutes( var routes []*router.Route - return routes, newPath } diff --git a/dashboard/.eslintrc.json b/dashboard/.eslintrc.json index 4a68613626..7b63167778 100644 --- a/dashboard/.eslintrc.json +++ b/dashboard/.eslintrc.json @@ -33,12 +33,7 @@ ], "@typesecript-eslint/consistent-type-imports": "off", "@typescript-eslint/strict-boolean-expressions": "off", - "@typescript-eslint/no-misused-promises": [ - "error", - { - "checksVoidReturn": false - } - ], + "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/no-floating-promises": [ "error", { diff --git a/dashboard/src/main/Main.tsx b/dashboard/src/main/Main.tsx index fd300562f0..9f32e4fc98 100644 --- a/dashboard/src/main/Main.tsx +++ b/dashboard/src/main/Main.tsx @@ -1,4 +1,4 @@ -import React, { Component } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { Redirect, Route, Switch } from "react-router-dom"; import Loading from "components/Loading"; @@ -7,249 +7,72 @@ import api from "shared/api"; import { Context } from "shared/Context"; import { PorterUrls, type PorterUrl } from "shared/routing"; -import Login from "./auth/Login"; -import Register from "./auth/Register"; -import ResetPasswordFinalize from "./auth/ResetPasswordFinalize"; -import ResetPasswordInit from "./auth/ResetPasswordInit"; -import SetInfo from "./auth/SetInfo"; -import VerifyEmail from "./auth/VerifyEmail"; +import { useAuthn } from "../shared/auth/AuthnContext"; import CurrentError from "./CurrentError"; import Home from "./home/Home"; type PropsType = {}; -type StateType = { - loading: boolean; - isLoggedIn: boolean; - isEmailVerified: boolean; - hasInfo: boolean; - initialized: boolean; - local: boolean; - userId: number; - version: string; -}; - -export default class Main extends Component { - state = { - loading: true, - isLoggedIn: false, - isEmailVerified: false, - hasInfo: false, - initialized: localStorage.getItem("init") === "true", - local: false, - userId: null as number, - version: null as string, - }; - - componentDidMount() { +const Main: React.FC = () => { + const { + currentError, + setCurrentError, + setEdition, + setEnableGitlab, + currentProject, + currentCluster, + } = useContext(Context); + const { handleLogOut } = useAuthn(); + const [version, setVersion] = useState(""); + + useEffect(() => { // Get capabilities to case on user info requirements api .getMetadata("", {}, {}) .then((res) => { - this.setState({ - version: res.data?.version, - }); + setVersion(res.data?.version); }) - .catch((err) => { - console.log(err); - }); + .catch(() => {}); - const { setUser, setCurrentError } = this.context; const urlParams = new URLSearchParams(window.location.search); const error = urlParams.get("error"); error && setCurrentError(error); - api - .checkAuth("", {}, {}) - .then((res) => { - if (res?.data) { - setUser(res.data.id, res.data.email); - this.setState({ - isLoggedIn: true, - isEmailVerified: res.data.email_verified, - initialized: true, - hasInfo: res.data.company_name && true, - loading: false, - userId: res.data.id, - }); - } else { - this.setState({ isLoggedIn: false, loading: false }); - } - }) - .catch((err) => { - this.setState({ isLoggedIn: false, loading: false }); - }); api .getMetadata("", {}, {}) .then((res) => { - this.context.setEdition(res.data?.version); - this.setState({ local: !res.data?.provisioner }); - this.context.setEnableGitlab(!!res.data?.gitlab); + setEdition(res.data?.version); + setEnableGitlab(!!res.data?.gitlab); }) - .catch((err) => { - console.log(err); - }); - } + .catch(() => {}); + }, []); - initialize = () => { - this.setState({ isLoggedIn: true, initialized: true }); - localStorage.setItem("init", "true"); - }; - - authenticate = () => { - api - .checkAuth("", {}, {}) - .then((res) => { - if (res?.data) { - this.context.setUser(res?.data?.id, res?.data?.email); - this.setState({ - isLoggedIn: true, - isEmailVerified: res?.data?.email_verified, - initialized: true, - hasInfo: res.data.company_name && true, - loading: false, - userId: res.data.id, - }); - } else { - this.setState({ isLoggedIn: false, loading: false }); - } - }) - .catch((err) => { - this.setState({ isLoggedIn: false, loading: false }); - }); - }; - - handleLogOut = () => { - // Clears local storage for proper rendering of clusters - // Attempt user logout - api - .logOutUser("", {}, {}) - .then(() => { - this.context.clearContext(); - this.setState({ isLoggedIn: false, initialized: true }); - localStorage.clear(); - }) - .catch((err) => - this.context.setCurrentError(err.response?.data.errors[0]) - ); - }; - - renderMain = () => { - if (this.state.loading || !this.state.version) { + const renderMain = (): JSX.Element => { + if (!version) { return ; } - // if logged in but not verified, block until email verification - if ( - !this.state.local && - this.state.isLoggedIn && - !this.state.isEmailVerified - ) { - return ( - - { - return ; - }} - /> - - ); - } - - // Handle case where new user signs up via OAuth and has not set name and company - if ( - this.state.version === "production" && - !this.state.hasInfo && - this.state.userId > 9312 && - this.state.isLoggedIn - ) { - return ( - - { - return ( - - ); - }} - /> - - ); - } - return ( - { - if (!this.state.isLoggedIn) { - return ; - } else { - return ; - } - }} - /> - { - if (!this.state.isLoggedIn) { - return ; - } else { - return ; - } - }} - /> - { - if (!this.state.isLoggedIn) { - return ; - } else { - return ; - } - }} - /> - { - if (!this.state.isLoggedIn) { - return ; - } else { - return ; - } - }} - /> { - if (this.state.isLoggedIn) { - return ; - } else { - return ; - } + return ; }} /> { const baseRoute = routeProps.match.params.baseRoute; - if ( - this.state.isLoggedIn && - this.state.initialized && - PorterUrls.includes(baseRoute) - ) { + if (PorterUrls.includes(baseRoute)) { return ( ); } else { @@ -261,14 +84,12 @@ export default class Main extends Component { ); }; - render() { - return ( - <> - {this.renderMain()} - - - ); - } -} + return ( + <> + {renderMain()} + + + ); +}; -Main.contextType = Context; +export default Main; diff --git a/dashboard/src/main/MainWrapper.tsx b/dashboard/src/main/MainWrapper.tsx index c3bfb813a0..eb5a3e9c77 100644 --- a/dashboard/src/main/MainWrapper.tsx +++ b/dashboard/src/main/MainWrapper.tsx @@ -1,28 +1,27 @@ -import React, { Component } from "react"; +import React from "react"; +import { withRouter, type RouteComponentProps } from "react-router"; +import AuthzProvider from "shared/auth/AuthzContext"; +import MainWrapperErrorBoundary from "shared/error_handling/MainWrapperErrorBoundary"; + +import AuthnProvider from "../shared/auth/AuthnContext"; import { ContextProvider } from "../shared/Context"; import Main from "./Main"; -import { RouteComponentProps, withRouter } from "react-router"; -import AuthProvider from "shared/auth/AuthContext"; -import MainWrapperErrorBoundary from "shared/error_handling/MainWrapperErrorBoundary"; type PropsType = RouteComponentProps & {}; -type StateType = {}; - -class MainWrapper extends Component { - render() { - let { history, location } = this.props; - return ( - - +const MainWrapper: React.FC = ({ history, location }) => { + return ( + + +
- - - ); - } -} + + + + ); +}; export default withRouter(MainWrapper); diff --git a/dashboard/src/main/auth/Login.tsx b/dashboard/src/main/auth/Login.tsx index 6263caec3e..9f537961ed 100644 --- a/dashboard/src/main/auth/Login.tsx +++ b/dashboard/src/main/auth/Login.tsx @@ -21,7 +21,7 @@ import GoogleIcon from "assets/GoogleIcon"; import logo from "assets/logo.png"; type Props = { - authenticate: () => void; + authenticate: () => Promise; }; const getWindowDimensions = () => { @@ -56,7 +56,7 @@ const Login: React.FC = ({ authenticate }) => { window.location.href = res.data.redirect; } else { setUser(res?.data?.id, res?.data?.email); - authenticate(); + authenticate().catch(() => {}); } }) .catch((err) => { diff --git a/dashboard/src/main/auth/Register.tsx b/dashboard/src/main/auth/Register.tsx index 36312f279f..52366d15c6 100644 --- a/dashboard/src/main/auth/Register.tsx +++ b/dashboard/src/main/auth/Register.tsx @@ -21,7 +21,7 @@ import logo from "assets/logo.png"; import InfoPanel from "./InfoPanel"; type Props = { - authenticate: () => void; + authenticate: () => Promise; }; const getWindowDimensions = () => { @@ -141,7 +141,7 @@ const Register: React.FC = ({ authenticate }) => { window.location.href = res.data.redirect; } else { setUser(res?.data?.id, res?.data?.email); - authenticate(); + authenticate().catch(() => {}); try { window.dataLayer?.push({ diff --git a/dashboard/src/main/auth/SetInfo.tsx b/dashboard/src/main/auth/SetInfo.tsx index 248a81974a..c11e5349fe 100644 --- a/dashboard/src/main/auth/SetInfo.tsx +++ b/dashboard/src/main/auth/SetInfo.tsx @@ -19,7 +19,7 @@ import logo from "assets/logo.png"; import InfoPanel from "./InfoPanel"; type Props = { - authenticate: () => void; + authenticate: () => Promise; handleLogOut: () => void; }; @@ -69,7 +69,7 @@ const SetInfo: React.FC = ({ authenticate, handleLogOut }) => { { id: user.id } ) .then((res: any) => { - authenticate(); + authenticate().catch(() => {}); try { window.dataLayer?.push({ diff --git a/dashboard/src/shared/auth/AuthnContext.tsx b/dashboard/src/shared/auth/AuthnContext.tsx new file mode 100644 index 0000000000..ebf1161fe2 --- /dev/null +++ b/dashboard/src/shared/auth/AuthnContext.tsx @@ -0,0 +1,186 @@ +import React, { useContext, useEffect, useState } from "react"; +import { Redirect, Route, Switch } from "react-router-dom"; + +import api from "shared/api"; +import { Context } from "shared/Context"; + +import Loading from "../../components/Loading"; +import Login from "../../main/auth/Login"; +import Register from "../../main/auth/Register"; +import ResetPasswordFinalize from "../../main/auth/ResetPasswordFinalize"; +import ResetPasswordInit from "../../main/auth/ResetPasswordInit"; +import SetInfo from "../../main/auth/SetInfo"; +import VerifyEmail from "../../main/auth/VerifyEmail"; + +type AuthnState = { + userId: number; + handleLogOut: () => void; +}; + +export const AuthnContext = React.createContext(null); + +export const useAuthn = (): AuthnState => { + const context = useContext(AuthnContext); + if (context == null) { + throw new Error("useAuthn must be used within an AuthnContext"); + } + return context; +}; + +const AuthnProvider = ({ + children, +}: { + children: JSX.Element; +}): JSX.Element => { + const { setUser, clearContext, setCurrentError } = useContext(Context); + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [isEmailVerified, setIsEmailVerified] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [userId, setUserId] = useState(-1); + const [hasInfo, setHasInfo] = useState(false); + const [local, setLocal] = useState(false); + + const authenticate = async (): Promise => { + api + .checkAuth("", {}, {}) + .then((res) => { + if (res?.data) { + setUser?.(res.data.id, res.data.email); + setIsLoggedIn(true); + setIsEmailVerified(res.data.email_verified); + setHasInfo(res.data.company_name && true); + setIsLoading(false); + setUserId(res.data.id); + } else { + setIsLoggedIn(false); + setIsEmailVerified(false); + setHasInfo(false); + setIsLoading(false); + setUserId(-1); + } + }) + .catch(() => { + setIsLoggedIn(false); + setIsEmailVerified(false); + setHasInfo(false); + setIsLoading(false); + setUserId(-1); + }); + }; + + const handleLogOut = (): void => { + // Clears local storage for proper rendering of clusters + // Attempt user logout + api + .logOutUser("", {}, {}) + .then(() => { + setIsLoggedIn(false); + setIsEmailVerified(false); + clearContext?.(); + localStorage.clear(); + }) + .catch((err) => { + setCurrentError?.(err.response?.data.errors[0]); + }); + }; + + useEffect(() => { + authenticate().catch(() => {}); + + // check if porter server is local (does not require email verification) + api + .getMetadata("", {}, {}) + .then((res) => { + setLocal(!res.data?.provisioner); + }) + .catch(() => {}); + }, []); + + if (isLoading) { + return ; + } + + // return unauthenticated routes + if (!isLoggedIn) { + return ( + + { + return ; + }} + /> + { + return ; + }} + /> + { + return ; + }} + /> + { + return ; + }} + /> + { + return ; + }} + /> + + ); + } + + // if logged in but not verified, block until email verification + if (!local && !isEmailVerified) { + return ( + + { + return ; + }} + /> + + ); + } + + // Handle case where new user signs up via OAuth and has not set name and company + if (!hasInfo && userId > 9312) { + return ( + + { + return ( + + ); + }} + /> + + ); + } + + return ( + + {children} + + ); +}; + +export default AuthnProvider; diff --git a/dashboard/src/shared/auth/AuthorizationHoc.tsx b/dashboard/src/shared/auth/AuthorizationHoc.tsx index a8bae0ae39..77ca8eb94c 100644 --- a/dashboard/src/shared/auth/AuthorizationHoc.tsx +++ b/dashboard/src/shared/auth/AuthorizationHoc.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useContext } from "react"; -import { AuthContext } from "./AuthContext"; +import { AuthzContext } from "./AuthzContext"; import { isAuthorized } from "./authorization-helpers"; import { ScopeType, Verbs } from "./types"; @@ -8,7 +8,7 @@ export const GuardedComponent = ( resource: string, verb: Verbs | Array ) => (Component: any) => (props: ComponentProps) => { - const authContext = useContext(AuthContext); + const authContext = useContext(AuthzContext); if (isAuthorized(authContext.currentPolicy, scope, resource, verb)) { return ; @@ -36,7 +36,7 @@ export function withAuth

( })`; const C = (props: P) => { - const authContext = useContext(AuthContext); + const authContext = useContext(AuthzContext); const isAuth = useCallback( (scope: ScopeType, resource: string, verb: Verbs | Array) => diff --git a/dashboard/src/shared/auth/AuthContext.tsx b/dashboard/src/shared/auth/AuthzContext.tsx similarity index 75% rename from dashboard/src/shared/auth/AuthContext.tsx rename to dashboard/src/shared/auth/AuthzContext.tsx index aac1266318..a2ef86e8d6 100644 --- a/dashboard/src/shared/auth/AuthContext.tsx +++ b/dashboard/src/shared/auth/AuthzContext.tsx @@ -1,16 +1,24 @@ import React, { useContext, useEffect, useState } from "react"; + import api from "shared/api"; import { Context } from "shared/Context"; + import { POLICY_HIERARCHY_TREE, populatePolicy } from "./authorization-helpers"; -import { PolicyDocType } from "./types"; +import { type PolicyDocType } from "./types"; -type AuthContext = { +type AuthzContext = { currentPolicy: PolicyDocType; }; -export const AuthContext = React.createContext({} as AuthContext); +export const AuthzContext = React.createContext( + {} as AuthzContext +); -const AuthProvider: React.FC = ({ children }) => { +const AuthzProvider = ({ + children, +}: { + children: JSX.Element; +}): JSX.Element => { const { user, currentProject } = useContext(Context); const [currentPolicy, setCurrentPolicy] = useState(null); @@ -42,10 +50,10 @@ const AuthProvider: React.FC = ({ children }) => { }, [user, currentProject?.id]); return ( - + {children} - + ); }; -export default AuthProvider; +export default AuthzProvider; diff --git a/dashboard/src/shared/auth/RouteGuard.tsx b/dashboard/src/shared/auth/RouteGuard.tsx index d03bbe48b1..8a80fc56aa 100644 --- a/dashboard/src/shared/auth/RouteGuard.tsx +++ b/dashboard/src/shared/auth/RouteGuard.tsx @@ -1,16 +1,17 @@ -import UnauthorizedPage from "components/UnauthorizedPage"; import React, { useContext, useMemo } from "react"; -import { Route, RouteProps } from "react-router"; -import { AuthContext } from "./AuthContext"; -import { isAuthorized } from "./authorization-helpers"; -import { ScopeType, Verbs } from "./types"; +import { Route, type RouteProps } from "react-router"; import Loading from "components/Loading"; +import UnauthorizedPage from "components/UnauthorizedPage"; + +import { isAuthorized } from "./authorization-helpers"; +import { AuthzContext } from "./AuthzContext"; +import { type ScopeType, type Verbs } from "./types"; type GuardedRouteProps = { scope: ScopeType; resource: string; - verb: Verbs | Array; + verb: Verbs | Verbs[]; }; const GuardedRoute: React.FC = ({ @@ -21,7 +22,7 @@ const GuardedRoute: React.FC = ({ children, ...rest }) => { - const { currentPolicy } = useContext(AuthContext); + const { currentPolicy } = useContext(AuthzContext); const auth = useMemo(() => { return isAuthorized(currentPolicy, scope, resource, verb); }, [currentPolicy, scope, resource, verb]); @@ -39,24 +40,27 @@ const GuardedRoute: React.FC = ({ return ; }; -export const fakeGuardedRoute = ( - scope: string, - resource: string, - verb: Verbs | Array -) => (Component: any) => (props: ComponentProps) => { - const { currentPolicy } = useContext(AuthContext); - const auth = useMemo(() => { - return isAuthorized(currentPolicy, scope, resource, verb); - }, [currentPolicy, scope, resource, verb]); +export const fakeGuardedRoute = + ( + scope: string, + resource: string, + verb: Verbs | Verbs[] + ) => + (Component: any) => + (props: ComponentProps) => { + const { currentPolicy } = useContext(AuthzContext); + const auth = useMemo(() => { + return isAuthorized(currentPolicy, scope, resource, verb); + }, [currentPolicy, scope, resource, verb]); - if (!currentPolicy) { - return ; - } - if (auth) { - return ; - } + if (!currentPolicy) { + return ; + } + if (auth) { + return ; + } - return ; -}; + return ; + }; export default GuardedRoute; diff --git a/dashboard/src/shared/auth/useAuth.ts b/dashboard/src/shared/auth/useAuth.ts index fe8b524a76..8585f01946 100644 --- a/dashboard/src/shared/auth/useAuth.ts +++ b/dashboard/src/shared/auth/useAuth.ts @@ -1,17 +1,15 @@ import { useCallback, useContext } from "react"; -import { AuthContext } from "./AuthContext"; + import { isAuthorized } from "./authorization-helpers"; -import { ScopeType, Verbs } from "./types"; +import { AuthzContext } from "./AuthzContext"; +import { type ScopeType, type Verbs } from "./types"; const useAuth = () => { - const authContext = useContext(AuthContext); + const authContext = useContext(AuthzContext); const isAuth = useCallback( - ( - scope: ScopeType, - resource: string | string[], - verb: Verbs | Array - ) => isAuthorized(authContext.currentPolicy, scope, resource, verb), + (scope: ScopeType, resource: string | string[], verb: Verbs | Verbs[]) => + isAuthorized(authContext.currentPolicy, scope, resource, verb), [authContext.currentPolicy] ); diff --git a/dashboard/src/test-utils.tsx b/dashboard/src/test-utils.tsx index 455015d096..e257bed321 100644 --- a/dashboard/src/test-utils.tsx +++ b/dashboard/src/test-utils.tsx @@ -8,7 +8,7 @@ import { useHistory, useLocation } from "react-router"; import { BrowserRouter } from "react-router-dom"; import { createGlobalStyle, ThemeProvider } from "styled-components"; -import AuthProvider from "shared/auth/AuthContext"; +import AuthzProvider from "shared/auth/AuthzContext"; import { ContextProvider } from "shared/Context"; import standard from "shared/themes/standard"; @@ -19,7 +19,7 @@ const AllTheProviders: React.FC<{ children: React.ReactNode }> = ({ - {children} + {children}