1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-21 14:50:08 +03:00

B #5695: Disable group selector on OneProvision GUI (#1841)

(cherry picked from commit f0e093f49e36b4fe287d78a47fe2750923b1755a)
This commit is contained in:
Sergio Betanzos 2022-03-15 15:58:03 +01:00 committed by Tino Vazquez
parent 248422cb50
commit 7e068a886a
18 changed files with 118 additions and 150 deletions

View File

@ -80,7 +80,6 @@ export default function ansiHTML(text) {
if (ot) {
// If current sequence has been opened, close it.
if (~ansiCodes.indexOf(seq)) {
// eslint-disable-line no-extra-boolean-cast
ansiCodes.pop()
return '</span>'

View File

@ -87,7 +87,6 @@ const Content = ({ data, setFormData }) => {
...section,
name,
label: <Translate word={name} />,
// eslint-disable-next-line react/display-name
renderContent: () => (
<TabContent {...{ data, setFormData, hypervisor, control }} />
),

View File

@ -51,6 +51,7 @@ const AuthLayout = ({ subscriptions = [], children }) => {
return () => {
endpoints.forEach((endpoint) => {
endpoint.unsubscribe()
endpoint.abort()
})
}
}, [dispatch, jwt])

View File

@ -39,7 +39,7 @@ import { sentenceCase } from 'client/utils'
const Header = () => {
const { isOneAdmin } = useAuth()
const { fixMenu } = useGeneralApi()
const { appTitle, title, isBeta } = useGeneral()
const { appTitle, title, isBeta, withGroupSwitcher } = useGeneral()
const appAsSentence = useMemo(() => sentenceCase(appTitle), [appTitle])
return (
@ -104,7 +104,7 @@ const Header = () => {
>
<User />
<View />
{!isOneAdmin && <Group />}
{!isOneAdmin && withGroupSwitcher && <Group />}
<Zone />
</Stack>
</Toolbar>

View File

@ -30,6 +30,7 @@ export const BY = {
export const _APPS = defaultApps
export const APPS = Object.keys(defaultApps)
export const APPS_IN_BETA = [_APPS.sunstone.name]
export const APPS_WITH_SWITCHER = [_APPS.sunstone.name]
export const APP_URL = defaultAppName ? `/${defaultAppName}` : ''
export const WEBSOCKET_URL = `${APP_URL}/websockets`
export const STATIC_FILES_URL = `${APP_URL}/client/assets`
@ -52,6 +53,7 @@ export const LANGUAGES_URL = `${STATIC_FILES_URL}/languages`
export const ONEADMIN_ID = '0'
export const SERVERADMIN_ID = '1'
export const ONEADMIN_GROUP_ID = '0'
export const FILTER_POOL = {
PRIMARY_GROUP_RESOURCES: '-4',

View File

@ -197,6 +197,8 @@ module.exports = {
/* errors */
SessionExpired: 'Sorry, your session has expired',
OnlyForOneadminGroup:
'Only members of the oneadmin group can access OneProvision functionality',
SomethingWrong: 'Something go wrong',
CannotConnectOneFlow: 'Cannot connect to OneFlow server',
CannotConnectOneProvision: 'Cannot connect to OneProvision server',

View File

@ -44,9 +44,9 @@ function Login() {
const classes = loginStyles()
const isMobile = useMediaQuery((theme) => theme.breakpoints.only('xs'))
const { logout } = useAuthApi()
const { error: otherError, isLoginInProgress: needGroupToContinue } =
useAuth()
const { logout } = useAuthApi()
const [changeAuthGroup, changeAuthGroupState] = useChangeAuthGroupMutation()
const [login, loginState] = useLoginMutation()

View File

@ -1,51 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed 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 { ReactElement } from 'react'
import SunstoneApp from 'client/apps/sunstone'
import ProvisionApp from 'client/apps/provision'
import LoadingScreen from 'client/components/LoadingScreen'
import { isDevelopment, isBackend } from 'client/utils'
import { _APPS, APPS } from 'client/constants'
/**
* Render App by url: http://<host:port>/fireedge/<APP>.
*
* @param {object} props - Props from server
* @returns {ReactElement} Returns App
*/
const DevelopmentApp = (props) => {
let appName = ''
if (isDevelopment() && !isBackend()) {
appName = window.location.pathname
.split(/\//gi)
.filter((sub) => sub?.length > 0)
.find((resource) => APPS.includes(resource))
}
return (
{
[_APPS.provision.name]: <ProvisionApp {...props} />,
[_APPS.sunstone.name]: <SunstoneApp {...props} />,
}[appName] ?? <LoadingScreen />
)
}
DevelopmentApp.displayName = 'DevelopmentApp'
export default DevelopmentApp

View File

@ -1,23 +0,0 @@
/* ------------------------------------------------------------------------- *
* Copyright 2002-2021, OpenNebula Project, OpenNebula Systems *
* *
* Licensed 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 { render } from 'react-dom'
import { createStore } from 'client/store'
import App from 'client/dev/_app'
const { store } = createStore({ initState: window.__PRELOADED_STATE__ })
render(<App store={store} />, document.getElementById('root'))

View File

@ -13,39 +13,45 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { createApi } from '@reduxjs/toolkit/query/react'
import { Actions, Commands } from 'server/routes/api/auth/routes'
import { dismissSnackbar } from 'client/features/General/actions'
import { actions } from 'client/features/Auth/slice'
import userApi from 'client/features/OneApi/user'
import { storage } from 'client/utils'
import { APP_URL, JWT_NAME, FILTER_POOL, ONEADMIN_ID } from 'client/constants'
import http from 'client/utils/rest'
import { requestConfig, storage } from 'client/utils'
import { JWT_NAME, FILTER_POOL, ONEADMIN_ID } from 'client/constants'
const { ALL_RESOURCES, PRIMARY_GROUP_RESOURCES } = FILTER_POOL
const authApi = createApi({
reducerPath: 'authApi',
baseQuery: fetchBaseQuery({
baseUrl: `${APP_URL}/api/`,
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.jwt
baseQuery: async ({ params, command, needState }, { getState, signal }) => {
try {
const config = requestConfig(params, command)
const response = await http.request({ ...config, signal })
const state = needState ? getState() : {}
// If we have a token set in state,
// let's assume that we should be passing it.
token && headers.set('authorization', `Bearer ${token}`)
return { data: response.data ?? {}, meta: { state } }
} catch (axiosError) {
const { message, data = {}, status, statusText } = axiosError
const { message: messageFromServer, data: errorFromOned } = data
return headers
},
}),
const error = message ?? errorFromOned ?? messageFromServer ?? statusText
return { error: { status: status, data: error } }
}
},
endpoints: (builder) => ({
getAuthUser: builder.query({
/**
* @returns {object} Information about authenticated user
* @throws Fails when response isn't code 200
*/
query: () => ({ url: 'user/info' }),
transformResponse: (response) => response?.data?.USER,
query: () => ({ command: { path: '/user/info' } }),
transformResponse: (response) => response?.USER,
async onQueryStarted(_, { queryFulfilled, dispatch }) {
try {
const { data: user } = await queryFulfilled
@ -55,23 +61,31 @@ const authApi = createApi({
}),
login: builder.mutation({
/**
* @param {object} data - User credentials
* @param {string} data.user - Username
* @param {string} data.token - Password
* @param {boolean} [data.remember] - Remember session
* @param {string} [data.token2fa] - Token for Two factor authentication
* Login in the interface.
*
* @param {object} params - User credentials
* @param {string} params.user - Username
* @param {string} params.token - Password
* @param {boolean} [params.remember] - Remember session
* @param {string} [params.token2fa] - Token for Two factor authentication
* @returns {object} Response data from request
* @throws Fails when response isn't code 200
*/
query: (data) => ({ url: 'auth', method: 'POST', body: data }),
transformResponse: (response) => {
const { id, token } = response?.data
query: (params) => {
const name = Actions.AUTHENTICATION
const command = { name, ...Commands[name] }
return { params, command, needState: true }
},
transformResponse: (response, meta) => {
const { id, token } = response
const { withGroupSwitcher } = meta?.state?.general ?? {}
const isOneAdmin = id === ONEADMIN_ID
return {
jwt: token,
user: { ID: id },
isLoginInProgress: !!token && !isOneAdmin,
isLoginInProgress: withGroupSwitcher && !!token && !isOneAdmin,
}
},
async onQueryStarted({ remember }, { queryFulfilled, dispatch }) {

View File

@ -18,13 +18,14 @@ import { createSlice } from '@reduxjs/toolkit'
import { actions as authActions } from 'client/features/Auth/slice'
import * as actions from 'client/features/General/actions'
import { generateKey } from 'client/utils'
import { APPS_IN_BETA } from 'client/constants'
import { APPS_IN_BETA, APPS_WITH_SWITCHER } from 'client/constants'
const initial = {
zone: 0,
title: null,
appTitle: null,
isBeta: false,
withGroupSwitcher: false,
isLoading: false,
isFixMenu: false,
@ -56,10 +57,12 @@ const { name, reducer } = createSlice({
...state,
title: payload,
}))
.addCase(actions.changeAppTitle, (state, { payload }) => {
const isBeta = APPS_IN_BETA?.includes(String(payload).toLowerCase())
.addCase(actions.changeAppTitle, (state, { payload: appTitle }) => {
const lowerAppTitle = String(appTitle).toLowerCase()
const isBeta = APPS_IN_BETA?.includes(lowerAppTitle)
const withGroupSwitcher = APPS_WITH_SWITCHER?.includes(lowerAppTitle)
return { ...state, appTitle: payload, isBeta }
return { ...state, appTitle, isBeta, withGroupSwitcher }
})
.addCase(actions.changeZone, (state, { payload }) => ({
...state,

View File

@ -99,12 +99,7 @@ const oneApi = createApi({
})
)
return {
error: {
status: status,
data: message ?? data?.data ?? statusText,
},
}
return { error: { status: status, data: error } }
}
},
refetchOnMountOrArgChange: 30,

View File

@ -14,8 +14,9 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { isRejectedWithValue, Middleware, Dispatch } from '@reduxjs/toolkit'
import { actions } from 'client/features/Auth/slice'
import { T } from 'client/constants'
import * as Auth from 'client/features/Auth/slice'
import { T, ONEADMIN_GROUP_ID } from 'client/constants'
/**
* @param {{ dispatch: Dispatch }} params - Redux parameters
@ -26,7 +27,27 @@ export const unauthenticatedMiddleware =
(next) =>
(action) => {
if (isRejectedWithValue(action) && action.payload.status === 401) {
dispatch(actions.logout(T.SessionExpired))
dispatch(Auth.actions.logout(T.SessionExpired))
}
return next(action)
}
/**
* @param {{ dispatch: Dispatch, getState: function():object }} params - Redux parameters
* @returns {Middleware} - Middleware to logout when user isn't in oneadmin group
*/
export const onlyForOneadminMiddleware =
({ dispatch, getState }) =>
(next) =>
(action) => {
const groups = getState()?.[Auth.name]?.user?.GROUPS?.ID
if (!Auth.actions.logout.match(action) && groups) {
const ensuredGroups = [groups].flat()
!ensuredGroups.includes(ONEADMIN_GROUP_ID) &&
dispatch(Auth.actions.logout(T.OnlyForOneadminGroup))
}
return next(action)

View File

@ -17,8 +17,12 @@ import { hydrate, render } from 'react-dom'
import { createStore } from 'client/store'
import App from 'client/apps/provision'
import { onlyForOneadminMiddleware } from 'client/features/middleware'
const { store } = createStore({ initState: window.__PRELOADED_STATE__ })
const { store } = createStore({
initState: window.__PRELOADED_STATE__,
extraMiddleware: [onlyForOneadminMiddleware],
})
const rootHTML = document.getElementById('root')?.innerHTML
const renderMethod = rootHTML !== '' ? hydrate : render

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and *
* limitations under the License. *
* ------------------------------------------------------------------------- */
import { configureStore, EnhancedStore } from '@reduxjs/toolkit'
import { configureStore, Middleware, EnhancedStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query/react'
import { isDevelopment } from 'client/utils'
@ -27,9 +27,10 @@ import { unauthenticatedMiddleware } from 'client/features/middleware'
/**
* @param {object} props - Props
* @param {object} props.initState - Initial state
* @param {Middleware[]} props.extraMiddleware - Extra middleware to apply on store
* @returns {{ store: EnhancedStore }} Configured Redux Store
*/
export const createStore = ({ initState = {} }) => {
export const createStore = ({ initState = {}, extraMiddleware = [] }) => {
const store = configureStore({
reducer: {
[Auth.name]: Auth.reducer,
@ -42,6 +43,7 @@ export const createStore = ({ initState = {} }) => {
getDefaultMiddleware({
immutableCheck: true,
}).concat([
...extraMiddleware,
unauthenticatedMiddleware,
authApi.middleware,
oneApi.middleware,

View File

@ -17,10 +17,11 @@
const {
httpMethod,
from: fromData,
} = require('server/utils/constants/defaults')
} = require('../../../utils/constants/defaults')
const { POST } = httpMethod
const { postBody } = fromData
const basepath = '/auth'
const AUTHENTICATION = 'authentication'

View File

@ -45,32 +45,24 @@ languages.map((language) =>
const router = Router()
router.get('*', (req, res) => {
let app = 'dev'
let title = 'FireEdge'
const defaultTitle = 'FireEdge'
const context = {}
let store = ''
let component = ''
let css = ''
let storeRender = ''
// production
if (
env &&
(!env.NODE_ENV || (env.NODE_ENV && env.NODE_ENV !== defaultWebpackMode))
) {
const apps = Object.keys(defaultApps)
const parseUrl = req.url
.split(/\//gi)
.filter((sub) => sub && sub.length > 0)
const isProduction =
!env?.NODE_ENV || (env?.NODE_ENV && env?.NODE_ENV !== defaultWebpackMode)
parseUrl.forEach((element) => {
if (element && apps.includes(element)) {
app = element
title = element
}
})
const apps = Object.keys(defaultApps)
const appName = req.url
.split(/\//gi)
.filter((sub) => sub?.length > 0)
.find((resource) => apps.includes(resource))
const App = require(`../../../client/apps/${app}/index.js`).default
if (isProduction) {
const App = require(`../../../client/apps/${appName}/index.js`).default
const sheets = new ServerStyleSheets()
const composeEnhancer =
(root && root.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose
@ -92,11 +84,11 @@ router.get('*', (req, res) => {
<!DOCTYPE html>
<html lang="en">
<head>
<title>${upperCaseFirst(title)} by OpenNebula</title>
<link rel="icon" type="image/png" href="${STATIC_FILES_URL}/favicon/${app}/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="${STATIC_FILES_URL}/favicon/${app}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="${STATIC_FILES_URL}/favicon/${app}/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="${STATIC_FILES_URL}/favicon/${app}/favicon-16x16.png">
<title>${upperCaseFirst(appName ?? defaultTitle)} by OpenNebula</title>
<link rel="icon" type="image/png" href="${STATIC_FILES_URL}/favicon/${appName}/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="${STATIC_FILES_URL}/favicon/${appName}/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="${STATIC_FILES_URL}/favicon/${appName}/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="${STATIC_FILES_URL}/favicon/${appName}/favicon-16x16.png">
<meta name="theme-color" content="#ffffff">
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
@ -106,7 +98,11 @@ router.get('*', (req, res) => {
<div id="root">${component}</div>
${storeRender}
<script>${`langs = ${JSON.stringify(scriptLanguages)}`}</script>
<script src='${APP_URL}/client/bundle.${app}.js'></script>
${
isProduction
? `<script src='${APP_URL}/client/bundle.${appName}.js'></script>`
: `<script src='${APP_URL}/client/bundle.dev.js'></script>`
}
</body>
</html>
`

View File

@ -14,6 +14,12 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
const {
defaultWebpackMode,
defaultApps,
defaultAppName,
} = require('./src/server/utils/constants/defaults')
const getDevConfiguration = () => {
try {
const path = require('path')
@ -21,11 +27,6 @@ const getDevConfiguration = () => {
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
const TimeFixPlugin = require('time-fix-plugin')
const {
defaultWebpackMode,
defaultAppName,
} = require('./src/server/utils/constants/defaults')
const appName = defaultAppName ? `/${defaultAppName}` : ''
/** @type {webpack.Configuration} */
@ -33,7 +34,9 @@ const getDevConfiguration = () => {
mode: defaultWebpackMode,
entry: [
'webpack-hot-middleware/client',
path.resolve(__dirname, 'src/client/dev/index.js'),
...Object.keys(defaultApps).map((app) =>
path.resolve(__dirname, `src/client/${app}.js`)
),
],
output: {
filename: 'bundle.dev.js',