1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-01-05 09:17:41 +03:00

F #6676: Add X509 login mechanism for FireEdge (#3211)

This commit is contained in:
Jorge Miguel Lobo Escalona 2024-08-29 12:20:52 +02:00 committed by GitHub
parent 5d6b8571b0
commit 875fe29ade
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 132 additions and 96 deletions

View File

@ -14,6 +14,11 @@
* limitations under the License. *
* ------------------------------------------------------------------------- */
import {
entrypoint404,
entrypointApi,
entrypointApp,
} from './routes/entrypoints'
import {
defaultAppName,
defaultApps,
@ -22,36 +27,26 @@ import {
defaultPort,
defaultWebpackMode,
} from './utils/constants/defaults'
import {
entrypoint404,
entrypointApi,
entrypointApp,
} from './routes/entrypoints'
import { getLoggerMiddleware, initLogger } from './utils/logger'
import {
genFireedgeKey,
genPathResources,
getCert,
getKey,
validateServerIsSecure,
setDnsResultOrder,
} from './utils/server'
import { getLoggerMiddleware, initLogger } from './utils/logger'
import compression from 'compression'
import cors from 'cors'
import { env } from 'process'
import express from 'express'
import { getFireedgeConfig } from './utils/yml'
import guacamole from './routes/websockets/guacamole'
import helmet from 'helmet'
import http from 'http'
import https from 'https'
import { messageTerminal } from './utils/general'
import opennebulaWebsockets from './routes/websockets/opennebula'
import { readFileSync } from 'fs-extra'
import { resolve } from 'path'
import vmrc from './routes/websockets/vmrc'
import { env } from 'process'
import webpack from 'webpack'
import guacamole from './routes/websockets/guacamole'
import opennebulaWebsockets from './routes/websockets/opennebula'
import vmrc from './routes/websockets/vmrc'
import { messageTerminal } from './utils/general'
import { getFireedgeConfig } from './utils/yml'
setDnsResultOrder()
@ -66,10 +61,6 @@ genFireedgeKey()
// set logger
initLogger(appConfig.debug_level, appConfig.truncate_max_length)
// destructure imports
const unsecureServer = http.createServer
const secureServer = https.createServer
const app = express()
const basename = defaultAppName ? `/${defaultAppName}` : ''
@ -137,15 +128,7 @@ app.get('/*', (_, res) => res.redirect(`/${defaultAppName}/sunstone`))
// 404 - public
app.get('*', entrypoint404)
const appServer = validateServerIsSecure()
? secureServer(
{
key: readFileSync(getKey(), 'utf8'),
cert: readFileSync(getCert(), 'utf8'),
},
app
)
: unsecureServer(app)
const appServer = http.createServer(app)
const websockets = opennebulaWebsockets(appServer) || []

View File

@ -28,6 +28,7 @@ const {
connectOpennebula,
updaterResponse,
remoteLogin,
x509Login,
} = require('server/routes/api/auth/utils')
const { defaults, httpCodes } = require('server/utils/constants')
@ -135,22 +136,35 @@ const coreAuth = (
* @param {object} res - http response
* @param {Function} next - express stepper
* @param {object} params - params of http request
* @param {object} userData - user of http request
* @param {object} _ - user of http request
* @param {Function} oneConnection - function of xmlrpc
* @param {string} typeAuth - auth type
*/
const remoteAuth = (
res = {},
next = defaultEmptyFunction,
params = {},
userData = {},
oneConnection = defaultEmptyFunction
_,
oneConnection = defaultEmptyFunction,
typeAuth
) => {
const { user } = params
setRes(res)
setNext(next)
setNodeConnect(oneConnection)
updaterResponse(new Map(internalServerError).toObject())
user ? remoteLogin(user) : next()
if (user) {
switch (typeAuth) {
case 'x509':
x509Login(user)
break
default:
remoteLogin(user)
break
}
} else {
next()
}
}
/**
@ -172,12 +186,14 @@ const selectTypeAuth = (
oneConnection = defaultEmptyFunction
) => {
const appConfig = getFireedgeConfig()
switch (appConfig?.auth) {
case 'remote':
return remoteAuth(res, next, params, userData, oneConnection)
default:
return coreAuth(res, next, params, userData, oneConnection)
const typeAuth = {
remote: () => remoteAuth(res, next, params, userData, oneConnection),
x509: () => remoteAuth(res, next, params, userData, oneConnection, 'x509'),
opennebula: () => coreAuth(res, next, params, userData, oneConnection),
}
const auth = typeAuth[appConfig?.auth] || typeAuth.opennebula
return auth()
}
module.exports = {

View File

@ -566,6 +566,44 @@ const remoteLogin = (userData = '') => {
}
}
/**
* X.509 login route function.
*
* @param {string} userData - user remote data /DC=es/O=one/CN=user|/DC=us/O=two/CN=user
*/
const x509Login = (userData = '') => {
const serverAdminData = getServerAdmin()
const { username, token } = serverAdminData
if (username && token && userData) {
const reverseStringFields = (str) =>
/,/.test(str) ? str.split(',').reverse().join('/') : str
const parsedUserData = reverseStringFields(userData)
const oneConnect = connectOpennebula(`${username}:${username}`, token.token)
oneConnect({
action: USER_POOL_INFO,
parameters: getDefaultParamsOfOpennebulaCommand(USER_POOL_INFO, GET),
callback: (_, value) => {
const users = value?.USER_POOL?.USER || []
if (users.length) {
const userFound = users.find(
(data) =>
data.PASSWORD.includes(parsedUserData) &&
data.AUTH_DRIVER === 'x509'
)
if (userFound) {
setZones()
getServerAdminAndWrapUser(userFound)
} else {
next()
}
}
},
fillHookResource: false,
})
}
}
/**
* Login route function.
*
@ -613,5 +651,6 @@ module.exports = {
getCreatedTokenOpennebula,
createTokenServerAdmin,
remoteLogin,
x509Login,
getServerAdmin,
}

View File

@ -23,7 +23,7 @@ const thunk = require('redux-thunk').default
const { ServerStyleSheets } = require('@mui/styles')
const { request: axios } = require('axios')
const { writeInLogger } = require('server/utils/logger')
const { MissingRemoteHeaderError } = require('server/utils/errors')
const { MissingHeaderError } = require('server/utils/errors')
// server
const {
@ -37,7 +37,12 @@ const {
defaultApps,
defaultAppName,
defaultHeaderRemote,
defaultHeaderx509,
defaultApiTimeout,
defaultProtocol,
defaultIP,
defaultPort,
httpMethod,
} = require('server/utils/constants/defaults')
// client
@ -60,6 +65,7 @@ const router = Router()
const defaultConfig = {
currentTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}
const { POST } = httpMethod
router.get('*', async (req, res) => {
const remoteJWT = {}
@ -77,29 +83,45 @@ router.get('*', async (req, res) => {
const encodedFavIcon = await getEncodedFavicon()
const appConfig = getFireedgeConfig()
if (appConfig?.auth === 'remote') {
const authType = appConfig?.auth
const validAuthTypes = ['remote', 'x509']
if (validAuthTypes.includes(authType)) {
remoteJWT.remote = true
remoteJWT.remoteRedirect = appConfig?.auth_redirect ?? '.'
const finderHeader = () => {
const headers = Object.keys(req.headers)
return headers.find((header) => defaultHeaderRemote.includes(header))
switch (authType) {
case validAuthTypes[1]:
return headers.find((header) => defaultHeaderx509.includes(header))
default:
return headers.find((header) => defaultHeaderRemote.includes(header))
}
}
const findHeader = finderHeader()
try {
if (!findHeader) {
throw new MissingRemoteHeaderError(JSON.stringify(req.headers))
throw new MissingHeaderError(JSON.stringify(req.headers))
}
const remoteUser = req.get(findHeader)
const jwt = await axios({
method: 'POST',
url: `${req.protocol}://${req.get('host')}/${defaultAppName}/api/auth`,
const paramsAxios = {
method: POST,
url: `${defaultProtocol}://${defaultIP}:${
appConfig.port || defaultPort
}/${defaultAppName}/api/auth`,
data: {
user: req.get(findHeader),
user: remoteUser,
},
validateStatus: (status) => status >= 200 && status <= 400,
})
}
const jwt = await axios(paramsAxios)
if (!global.remoteUsers) {
global.remoteUsers = {}
}

View File

@ -18,12 +18,13 @@
const { parse } = require('url')
const { createProxyMiddleware } = require('http-proxy-middleware')
const { getFireedgeConfig } = require('server/utils/yml')
const {
genPathResources,
validateServerIsSecure,
} = require('server/utils/server')
const { genPathResources } = require('server/utils/server')
const { writeInLogger } = require('server/utils/logger')
const { endpointVmrc, defaultPort } = require('server/utils/constants/defaults')
const {
endpointVmrc,
defaultPort,
defaultProtocol,
} = require('server/utils/constants/defaults')
genPathResources()
@ -35,7 +36,7 @@ const logger = (message = '', format = '%s') =>
const appConfig = getFireedgeConfig()
const port = appConfig.port || defaultPort
const protocol = validateServerIsSecure() ? 'https' : 'http'
const protocol = defaultProtocol
const url = `${protocol}://localhost:${port}`
const vmrcProxy = createProxyMiddleware(endpointVmrc, {
target: url,

View File

@ -57,6 +57,7 @@ const defaults = {
defaultSizeRotate: '100k',
defaultAppName: appName,
defaultHeaderRemote: ['http_x_auth_username', 'x_auth_username'],
defaultHeaderx509: ['x-client-dn'],
defaultConfigErrorMessage: {
color: 'red',
message: 'file not found: %s',
@ -150,6 +151,7 @@ const defaults = {
default2FAOpennebulaTmpVar: `TMP_${default2FAOpennebulaVar}`,
defaultMessageProblemOpennebula: 'Problem with connection or xml parser',
defaultIP: defaultIp,
defaultProtocol: protocol,
defaultSeverities: [
`${severityPrepend}1`,
`${severityPrepend}2`,

View File

@ -35,13 +35,13 @@ class JWTError extends OpenNebulaError {
}
}
class MissingRemoteHeaderError extends OpenNebulaError {
class MissingHeaderError extends OpenNebulaError {
/**
* @param {string} headers - error message description.
*/
constructor(headers = '') {
super(`missing header: ${defaultHeaderRemote.join()} in ${headers}`)
this.name = 'MissingRemoteHeaderError'
super(`Missing Header: ${defaultHeaderRemote.join()} in ${headers}`)
this.name = 'MissingHeaderError'
}
}
@ -55,9 +55,20 @@ class MissingFireEdgeKeyError extends OpenNebulaError {
}
}
class InternalLoginError extends OpenNebulaError {
/**
*
*/
constructor() {
super('Internal Login Error')
this.name = 'InternalLoginError'
}
}
module.exports = {
JWTError,
MissingFireEdgeKeyError,
OpenNebulaError,
MissingRemoteHeaderError,
MissingHeaderError,
InternalLoginError,
}

View File

@ -30,7 +30,6 @@ const {
createHash,
} = require('crypto')
const {
existsSync,
readFileSync,
createWriteStream,
readdirSync,
@ -79,9 +78,6 @@ const {
const { internalServerError } = httpCodes
const { POST } = httpMethod
let cert = ''
let key = ''
/**
* Sets the default DNS lookup order to prefer IPv4 addresses first
* if the Node.js version is 16.0.0 or higher.
@ -97,37 +93,6 @@ const setDnsResultOrder = () => {
setDnsResultOrder()
/**
* Validate if server app have certs.
*
* @returns {boolean} file certs
*/
const validateServerIsSecure = () => {
const folder = 'cert/'
const dirCerts =
env && env.NODE_ENV === defaultWebpackMode
? ['../', '../', '../', folder]
: ['../', folder]
const pathfile = resolve(__dirname, ...dirCerts)
cert = `${pathfile}/cert.pem`
key = `${pathfile}/key.pem`
return existsSync && key && cert && existsSync(key) && existsSync(cert)
}
/**
* Get certificate SSL.
*
* @returns {string} ssl path
*/
const getCert = () => cert
/**
* Get key of certificate SSL.
*
* @returns {string} key ssl path
*/
const getKey = () => key
/**
* Validate the route http method.
*
@ -1045,11 +1010,8 @@ module.exports = {
replaceEscapeSequence,
createFile,
httpResponse,
validateServerIsSecure,
genPathResources,
genFireedgeKey,
getCert,
getKey,
parsePostData,
getRequestFiles,
getRequestParameters,