diff options
12 files changed, 185 insertions, 59 deletions
diff --git a/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/DevSecurityOverrideFilter.kt b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/DevSecurityOverrideFilter.kt new file mode 100644 index 00000000..ba2cf2ae --- /dev/null +++ b/opendc-web/opendc-web-api/src/main/kotlin/org/opendc/web/api/util/DevSecurityOverrideFilter.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.api.util + +import io.quarkus.arc.properties.IfBuildProperty +import java.security.Principal +import javax.ws.rs.container.ContainerRequestContext +import javax.ws.rs.container.ContainerRequestFilter +import javax.ws.rs.container.PreMatching +import javax.ws.rs.core.SecurityContext +import javax.ws.rs.ext.Provider + +/** + * Helper class to disable security for the OpenDC web API when in development mode. + */ +@Provider +@PreMatching +@IfBuildProperty(name = "opendc.security.enabled", stringValue = "false") +class DevSecurityOverrideFilter : ContainerRequestFilter { + override fun filter(requestContext: ContainerRequestContext) { + requestContext.securityContext = object : SecurityContext { + override fun getUserPrincipal(): Principal = Principal { "anon" } + + override fun isSecure(): Boolean = false + + override fun isUserInRole(role: String): Boolean = true + + override fun getAuthenticationScheme(): String = "basic" + } + } +} diff --git a/opendc-web/opendc-web-api/src/main/resources/application-dev.properties b/opendc-web/opendc-web-api/src/main/resources/application-dev.properties index 1c1c6950..08d11609 100644 --- a/opendc-web/opendc-web-api/src/main/resources/application-dev.properties +++ b/opendc-web/opendc-web-api/src/main/resources/application-dev.properties @@ -27,10 +27,14 @@ quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect quarkus.hibernate-orm.database.generation=drop-and-create quarkus.hibernate-orm.sql-load-script=init-dev.sql +# OpenID +quarkus.oidc.enabled=false +quarkus.oidc.auth-server-url= +quarkus.oidc.client-id= + # OpenDC web UI quarkus.opendc-ui.path=/ -quarkus.opendc-ui.auth.domain=${OPENDC_AUTH0_DOMAIN} -quarkus.opendc-ui.auth.client-id=${OPENDC_AUTH0_CLIENT_ID} -quarkus.opendc-ui.auth.audience=${OPENDC_AUTH0_AUDIENCE} - quarkus.resteasy.path=/api + +opendc.security.enabled=false + diff --git a/opendc-web/opendc-web-ui/public/img/avatar.svg b/opendc-web/opendc-web-ui/public/img/avatar.svg new file mode 100644 index 00000000..73726f9b --- /dev/null +++ b/opendc-web/opendc-web-ui/public/img/avatar.svg @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 24.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 36 36" style="enable-background:new 0 0 36 36;" xml:space="preserve"> +<style type="text/css"> + .st0{fill-rule:evenodd;clip-rule:evenodd;fill:#F0F0F0;} + .st1{fill-rule:evenodd;clip-rule:evenodd;fill:#D2D2D2;} + .st2{fill:#B8BBBE;} + .st3{fill:#D2D2D2;} +</style> +<rect class="st0" width="36" height="36"/> +<path class="st1" d="M17.7,20.1c-3.5,0-6.4-2.9-6.4-6.4s2.9-6.4,6.4-6.4s6.4,2.9,6.4,6.4S21.3,20.1,17.7,20.1z"/> +<path class="st2" d="M13.3,36l0-6.7c-2,0.4-2.9,1.4-3.1,3.5L10.1,36H13.3z"/> +<path class="st3" d="M10.1,36l0.1-3.2c0.2-2.1,1.1-3.1,3.1-3.5l0,6.7h9.4l0-6.7c2,0.4,2.9,1.4,3.1,3.5l0.1,3.2h4.7 + c-0.4-3.9-1.3-9-2.9-11c-1.1-1.4-2.3-2.2-3.5-2.6s-1.8-0.6-6.3-0.6s-6.1,0.7-6.1,0.7c-1.2,0.4-2.4,1.2-3.4,2.6 + C6.7,27,5.8,32.2,5.4,36H10.1z"/> +<path class="st2" d="M25.9,36l-0.1-3.2c-0.2-2.1-1.1-3.1-3.1-3.5l0,6.7H25.9z"/> +</svg> diff --git a/opendc-web/opendc-web-ui/src/api/index.js b/opendc-web/opendc-web-ui/src/api/index.js index 1a9877d0..75751658 100644 --- a/opendc-web/opendc-web-ui/src/api/index.js +++ b/opendc-web/opendc-web-ui/src/api/index.js @@ -20,7 +20,7 @@ * SOFTWARE. */ -const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL +import { apiUrl } from '../config' /** * Send the specified request to the OpenDC API. @@ -31,14 +31,19 @@ const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL * @param body The body of the request. */ export async function request(auth, path, method = 'GET', body) { + const headers = { + 'Content-Type': 'application/json', + } + const { getAccessTokenSilently } = auth - const token = await getAccessTokenSilently() + if (getAccessTokenSilently) { + const token = await getAccessTokenSilently() + headers['Authorization'] = `Bearer ${token}` + } + const response = await fetch(`${apiUrl}/${path}`, { method: method, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json', - }, + headers: headers, body: body && JSON.stringify(body), }) const json = await response.json() diff --git a/opendc-web/opendc-web-ui/src/auth.js b/opendc-web/opendc-web-ui/src/auth.js index e670476c..3d6cf87c 100644 --- a/opendc-web/opendc-web-ui/src/auth.js +++ b/opendc-web/opendc-web-ui/src/auth.js @@ -23,15 +23,27 @@ import PropTypes from 'prop-types' import { Auth0Provider, useAuth0 } from '@auth0/auth0-react' import { useEffect } from 'react' +import { auth } from './config' /** - * Obtain the authentication context. + * Helper function to provide the authentication context in case Auth0 is not + * configured. */ -export function useAuth() { - return useAuth0() +function useAuthDev() { + return { + isAuthenticated: false, + isLoading: false, + logout: () => {}, + loginWithRedirect: () => {}, + } } /** + * Obtain the authentication context. + */ +export const useAuth = auth.domain ? useAuth0 : useAuthDev + +/** * Force the user to be authenticated or redirect to the homepage. */ export function useRequireAuth() { @@ -51,16 +63,20 @@ export function useRequireAuth() { * AuthProvider which provides an authentication context. */ export function AuthProvider({ children }) { - return ( - <Auth0Provider - domain={process.env.NEXT_PUBLIC_AUTH0_DOMAIN} - clientId={process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID} - redirectUri={global.window && global.window.location.origin} - audience={process.env.NEXT_PUBLIC_AUTH0_AUDIENCE} - > - {children} - </Auth0Provider> - ) + if (auth.domain) { + return ( + <Auth0Provider + domain={auth.domain} + clientId={auth.clientId} + redirectUri={auth.redirectUri} + audience={auth.audience} + > + {children} + </Auth0Provider> + ) + } + + return children } AuthProvider.propTypes = { diff --git a/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js b/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js index 02e5d265..3e58b209 100644 --- a/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js +++ b/opendc-web/opendc-web-ui/src/components/AppHeaderTools.js @@ -39,7 +39,9 @@ import { useAuth } from '../auth' import { GithubIcon, HelpIcon } from '@patternfly/react-icons' function AppHeaderTools() { - const auth = useAuth() + const { logout, user, isAuthenticated, isLoading } = useAuth() + const username = isAuthenticated || isLoading ? user?.name : 'Anonymous' + const avatar = isAuthenticated || isLoading ? user?.picture : '/img/avatar.svg' const [isKebabDropdownOpen, setKebabDropdownOpen] = useState(false) const kebabDropdownItems = [ @@ -56,7 +58,11 @@ function AppHeaderTools() { const [isDropdownOpen, setDropdownOpen] = useState(false) const userDropdownItems = [ <DropdownGroup key="group 2"> - <DropdownItem key="group 2 logout" onClick={() => auth.logout({ returnTo: window.location.origin })}> + <DropdownItem + key="group 2 logout" + isDisabled={!isAuthenticated} + onClick={() => logout({ returnTo: window.location.origin })} + > Logout </DropdownItem> </DropdownGroup>, @@ -105,7 +111,7 @@ function AppHeaderTools() { isOpen={isDropdownOpen} toggle={ <DropdownToggle onToggle={() => setDropdownOpen(!isDropdownOpen)}> - {auth?.user?.name ?? ( + {username ?? ( <Skeleton fontSize="xs" width="150px" @@ -119,8 +125,8 @@ function AppHeaderTools() { /> </PageHeaderToolsItem> </PageHeaderToolsGroup> - {auth?.user?.picture ? ( - <Avatar src={auth.user.picture} alt="Avatar image" /> + {avatar ? ( + <Avatar src={avatar} alt="Avatar image" /> ) : ( <Skeleton className="pf-c-avatar" shape="circle" width="2.25rem" screenreaderText="Loading avatar" /> )} diff --git a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js index 8897f2d0..18c3db3c 100644 --- a/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js +++ b/opendc-web/opendc-web-ui/src/components/topologies/sidebar/rack/MachineComponent.js @@ -5,12 +5,7 @@ import { Machine } from '../../../../shapes' const UnitIcon = ({ id, type }) => ( // eslint-disable-next-line @next/next/no-img-element - <img - src={'/img/topology/' + id + '-icon.png'} - alt={'Machine contains ' + type + ' units'} - height={24} - width={24} - /> + <img src={'/img/topology/' + id + '-icon.png'} alt={'Machine contains ' + type + ' units'} height={24} width={24} /> ) UnitIcon.propTypes = { diff --git a/opendc-web/opendc-web-ui/src/config.js b/opendc-web/opendc-web-ui/src/config.js new file mode 100644 index 00000000..52952d69 --- /dev/null +++ b/opendc-web/opendc-web-ui/src/config.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * URL to OpenDC API. + */ +export const apiUrl = process.env.NEXT_PUBLIC_API_BASE_URL + +/** + * Authentication configuration. + */ +export const auth = { + domain: process.env.NEXT_PUBLIC_AUTH0_DOMAIN, + clientId: process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID, + audience: process.env.NEXT_PUBLIC_AUTH0_AUDIENCE, + redirectUri: global.window && global.window.location.origin, +} + +/** + * Sentry DSN for web frontend. + */ +export const sentryDsn = process.env.NEXT_PUBLIC_SENTRY_DSN diff --git a/opendc-web/opendc-web-ui/src/data/project.js b/opendc-web/opendc-web-ui/src/data/project.js index b1db3da5..60a8fab6 100644 --- a/opendc-web/opendc-web-ui/src/data/project.js +++ b/opendc-web/opendc-web-ui/src/data/project.js @@ -23,7 +23,7 @@ import { useQuery, useMutation } from 'react-query' import { addProject, deleteProject, fetchProject, fetchProjects } from '../api/projects' import { addPortfolio, deletePortfolio, fetchPortfolio, fetchPortfolios } from '../api/portfolios' -import { addScenario, deleteScenario, fetchScenario, fetchScenariosOfPortfolio } from '../api/scenarios' +import { addScenario, deleteScenario, fetchScenario } from '../api/scenarios' /** * Configure the query defaults for the project endpoints. diff --git a/opendc-web/opendc-web-ui/src/pages/_app.js b/opendc-web/opendc-web-ui/src/pages/_app.js index 4861f5c1..bac9a5af 100644 --- a/opendc-web/opendc-web-ui/src/pages/_app.js +++ b/opendc-web/opendc-web-ui/src/pages/_app.js @@ -30,6 +30,7 @@ import { AuthProvider, useRequireAuth } from '../auth' import * as Sentry from '@sentry/react' import { Integrations } from '@sentry/tracing' import { QueryClientProvider } from 'react-query' +import { sentryDsn } from '../config' import '@patternfly/react-core/dist/styles/base.css' import '@patternfly/react-styles/css/utilities/Alignment/alignment.css' @@ -67,17 +68,14 @@ Inner.propTypes = { }).isRequired, } -const dsn = process.env.NEXT_PUBLIC_SENTRY_DSN // Initialize Sentry if the user has configured a DSN -if (process.browser && dsn) { - if (dsn) { - Sentry.init({ - environment: process.env.NODE_ENV, - dsn: dsn, - integrations: [new Integrations.BrowserTracing()], - tracesSampleRate: 0.1, - }) - } +if (process.browser && sentryDsn) { + Sentry.init({ + environment: process.env.NODE_ENV, + dsn: sentryDsn, + integrations: [new Integrations.BrowserTracing()], + tracesSampleRate: 0.1, + }) } export default function App(props) { diff --git a/opendc-web/opendc-web-ui/src/pages/projects/index.js b/opendc-web/opendc-web-ui/src/pages/projects/index.js index bb1fbd69..40792275 100644 --- a/opendc-web/opendc-web-ui/src/pages/projects/index.js +++ b/opendc-web/opendc-web-ui/src/pages/projects/index.js @@ -23,38 +23,29 @@ import React, { useMemo, useState } from 'react' import Head from 'next/head' import ProjectFilterPanel from '../../components/projects/FilterPanel' -import { useAuth } from '../../auth' import { AppPage } from '../../components/AppPage' import { PageSection, PageSectionVariants, Text, TextContent } from '@patternfly/react-core' import { useProjects, useDeleteProject } from '../../data/project' import ProjectTable from '../../components/projects/ProjectTable' import NewProject from '../../components/projects/NewProject' -const getVisibleProjects = (projects, filter, userId) => { +const getVisibleProjects = (projects, filter) => { switch (filter) { case 'SHOW_ALL': return projects case 'SHOW_OWN': - return projects.filter((project) => - project.authorizations.some((a) => a.userId === userId && a.level === 'OWN') - ) + return projects.filter((project) => project.role === 'OWNER') case 'SHOW_SHARED': - return projects.filter((project) => - project.authorizations.some((a) => a.userId === userId && a.level !== 'OWN') - ) + return projects.filter((project) => project.role !== 'OWNER') default: return projects } } function Projects() { - const { user } = useAuth() const { status, data: projects } = useProjects() const [filter, setFilter] = useState('SHOW_ALL') - const visibleProjects = useMemo( - () => getVisibleProjects(projects ?? [], filter, user?.sub), - [projects, filter, user?.sub] - ) + const visibleProjects = useMemo(() => getVisibleProjects(projects ?? [], filter), [projects, filter]) const { mutate: deleteProject } = useDeleteProject() diff --git a/opendc-web/opendc-web-ui/src/redux/index.js b/opendc-web/opendc-web-ui/src/redux/index.js index fa0c9d23..53cd9144 100644 --- a/opendc-web/opendc-web-ui/src/redux/index.js +++ b/opendc-web/opendc-web-ui/src/redux/index.js @@ -6,6 +6,7 @@ import thunk from 'redux-thunk' import rootReducer from './reducers' import rootSaga from './sagas' import { createReduxEnhancer } from '@sentry/react' +import { sentryDsn } from '../config' let store @@ -20,7 +21,7 @@ function initStore(initialState, ctx) { let middleware = applyMiddleware(...middlewares) - if (process.env.NEXT_PUBLIC_SENTRY_DSN) { + if (sentryDsn) { middleware = compose(middleware, createReduxEnhancer()) } |
