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

F #5422: Upload files fireedge (#1567)

This commit is contained in:
Jorge Miguel Lobo Escalona 2021-11-08 18:47:35 +01:00 committed by GitHub
parent 7228e670cd
commit 434be6e5af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 647 additions and 44 deletions

View File

@ -0,0 +1,98 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
const { from: fromData, httpMethod } = require('server/utils/constants/defaults')
const { show, list, upload, update, deleteFile } = require('./functions')
const { GET, POST, PUT, DELETE } = httpMethod
const publicRoutes = {
[GET]: {
null: {
action: show,
params: {
file: {
from: fromData.query,
name: 'file'
},
token: {
from: fromData.query,
name: 'token'
}
}
}
}
}
const privateRoutes = {
[GET]: {
null: {
action: list,
params: {
app: {
from: fromData.query,
name: 'app'
}
}
}
},
[POST]: {
null: {
action: upload,
params: {
app: {
from: fromData.query,
name: 'app'
},
files: {
from: 'files',
name: 'files'
}
}
}
},
[PUT]: {
null: {
action: update,
params: {
name: {
from: fromData.query,
name: 'name'
},
files: {
from: 'files',
name: 'files'
}
}
}
},
[DELETE]: {
null: {
action: deleteFile,
params: {
file: {
from: fromData.query,
name: 'file'
}
}
}
}
}
const fileApi = {
publicRoutes,
privateRoutes
}
module.exports = fileApi

View File

@ -0,0 +1,330 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
const { resolve, extname, parse, sep } = require('path')
const { global } = require('window-or-global')
const { jwtDecode } = require('server/utils/jwt')
const {
existsSync,
mkdirsSync,
moveSync
} = require('fs-extra')
const {
defaultEmptyFunction
} = require('server/utils/constants/defaults')
const {
ok,
internalServerError,
badRequest
} = require('server/utils/constants/http-codes')
const { Actions: ActionUser } = require('server/utils/constants/commands/user')
const { httpResponse, checkValidApp, getFiles, existsFile, removeFile } = require('server/utils/server')
const httpBadRequest = httpResponse(badRequest, '', '')
const groupAdministrator = ['0', '1']
/**
* Check if user is a administrator.
*
* @param {*} oneConnection - one connection function
* @param {*} id - user ID
* @param {*} success - callback success
* @param {*} error - callback error
*/
const checkUserAdmin = (
oneConnection = defaultEmptyFunction,
id = '',
success = defaultEmptyFunction,
error = defaultEmptyFunction
) => {
if (
typeof oneConnection === 'function' &&
id &&
typeof success === 'function' &&
typeof error === 'function'
) {
oneConnection(
ActionUser.USER_INFO,
[parseInt(id, 10)],
(err, value) => {
if (!err && value && value.USER && value.USER.GROUPS && value.USER.GROUPS.ID) {
let admin = false
const groups = Array.isArray(value.USER.GROUPS.ID) ? value.USER.GROUPS.ID : [value.USER.GROUPS.ID]
for (const group of groups) {
if (groupAdministrator.includes(group)) {
admin = true
break
}
}
success(admin)
} else {
error(err)
}
},
false
)
} else {
error()
}
}
/**
* Upload File.
*
* @param {object} res - response http
* @param {Function} next - express stepper
* @param {string} params - data response http
* @param {object} userData - user of http request
*/
const upload = (res = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
let rtn = httpBadRequest
if (
global.paths.CPI &&
params &&
params.app &&
checkValidApp(params.app) &&
params.files &&
userData &&
userData.id
) {
const pathUserData = `${params.app}/${userData.id}`
const pathUser = `${global.paths.CPI}/${pathUserData}`
if (!existsSync(pathUser)) {
mkdirsSync(pathUser)
}
let method = ok
let message = ''
const data = []
for (const file of params.files) {
if (file && file.originalname && file.path && file.filename) {
const extFile = extname(file.originalname)
try {
const filenameApi = `${pathUserData}/${file.filename}${extFile}`
const filename = `${pathUser}/${file.filename}${extFile}`
moveSync(file.path, filename)
data.push(filenameApi)
} catch (error) {
method = internalServerError
message = error && error.message
break
}
}
}
rtn = httpResponse(method, data.length ? data : '', message)
}
res.locals.httpCode = rtn
next()
}
/**
* List files by user.
*
* @param {object} res - response http
* @param {Function} next - express stepper
* @param {string} params - data response http
* @param {object} userData - user of http request
* @param {Function} oneConnection - one connection XMLRPC
*/
const list = (res = {}, next = defaultEmptyFunction, params = {}, userData = {}, oneConnection = defaultEmptyFunction) => {
const { user, password, id } = userData
const rtn = httpBadRequest
if (
params &&
params.app &&
checkValidApp(params.app) &&
user &&
password &&
id
) {
const oneConnect = oneConnection(user, password)
checkUserAdmin(
oneConnect,
id,
(admin = false) => {
let data = []
let pathUserData = `${params.app}/${id}`
if (admin) {
pathUserData = `${params.app}`
}
const pathUser = `${global.paths.CPI}/${pathUserData}`
data = getFiles(pathUser, true).map(
file => file.replace(`${global.paths.CPI}/`, '')
)
res.locals.httpCode = httpResponse(ok, data)
next()
},
() => {
res.locals.httpCode = internalServerError
next()
}
)
} else {
res.locals.httpCode = rtn
next()
}
}
/**
* Show file.
*
* @param {object} res - response http
* @param {Function} next - express stepper
* @param {string} params - data response http
* @param {object} userData - user of http request
*/
const show = (res = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
const rtn = httpBadRequest
const { file, token } = params
if (token && file && jwtDecode(token)) {
if (file) {
const pathFile = `${global.paths.CPI}/${file}`
existsFile(
pathFile,
() => {
res.locals.httpCode = httpResponse(ok, '', '', resolve(pathFile))
next()
},
() => {
res.locals.httpCode = httpResponse(internalServerError, '', '')
next()
}
)
}
} else {
res.locals.httpCode = rtn
next()
}
}
/**
* Check if user is a file owner.
*
* @param {string} file - filename
* @param {number} id - user id
* @returns {boolean} - if user is the file owner
*/
const checkFile = (file = '', id = '') => {
let rtn = false
if (file) {
const parsedFile = parse(file)
if (parsedFile && parsedFile.dir) {
const splitParsedFile = parsedFile.dir.split(sep)
if (Array.isArray(splitParsedFile) && checkValidApp(splitParsedFile[0]) && splitParsedFile[1] === id) {
rtn = true
}
}
}
return rtn
}
/**
* Delete File.
*
* @param {object} res - response http
* @param {Function} next - express stepper
* @param {string} params - data response http
* @param {object} userData - user of http request
*/
const deleteFile = (res = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
const rtn = httpBadRequest
if (
global.paths.CPI &&
params &&
params.file &&
userData &&
userData.id &&
checkFile(params.file, userData.id)
) {
const pathFile = `${global.paths.CPI}/${params.file}`
existsFile(
pathFile,
() => {
res.locals.httpCode = httpResponse(removeFile(pathFile) ? ok : internalServerError, '', '')
next()
},
() => {
res.locals.httpCode = httpResponse(internalServerError, '', '')
next()
}
)
} else {
res.locals.httpCode = rtn
next()
}
}
/**
* Update File.
*
* @param {object} res - response http
* @param {Function} next - express stepper
* @param {string} params - data response http
* @param {object} userData - user of http request
*/
const update = (res = {}, next = defaultEmptyFunction, params = {}, userData = {}) => {
const rtn = httpBadRequest
if (
global.paths.CPI &&
params &&
params.name &&
params.files &&
userData &&
userData.id &&
checkFile(params.name, userData.id)
) {
const nameFile = params.name
const pathFile = `${global.paths.CPI}/${nameFile}`
existsFile(
pathFile,
() => {
let method = ok
let data = ''
let message = ''
for (const file of params.files) {
if (file && file.originalname && file.path && file.filename) {
try {
moveSync(file.path, pathFile, { overwrite: true })
data = nameFile
} catch (error) {
method = internalServerError
message = error && error.message
break
}
}
}
res.locals.httpCode = httpResponse(method, data.length ? data : '', message)
next()
},
() => {
res.locals.httpCode = httpResponse(internalServerError, '', '')
next()
}
)
} else {
res.locals.httpCode = rtn
next()
}
}
const functionRoutes = {
upload,
deleteFile,
update,
show,
list
}
module.exports = functionRoutes

View File

@ -0,0 +1,91 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
const { addFunctionAsRoute, setFunctionRoute } = require('server/utils/server')
const { privateRoutes: filePrivateRoutes, publicRoutes: filePublicRoutes } = require('./files')
const { FILES } = require('./string-routes')
const privateRoutes = []
const publicRoutes = []
/**
* Set private routes.
*
* @param {object} routes - object of routes
* @param {string} path - principal route
* @param {Function} action - function of route
*/
const setPrivateRoutes = (routes = {}, path = '', action = () => undefined) => {
if (Object.keys(routes).length > 0 && routes.constructor === Object) {
Object.keys(routes).forEach((route) => {
privateRoutes.push(
setFunctionRoute(route, path,
(req, res, next, connection, userId, user) => {
action(req, res, next, routes[route], user, connection)
}
)
)
})
}
}
/**
* Set public routes.
*
* @param {object} routes - object of routes
* @param {string} path - principal route
* @param {Function} action - function of route
*/
const setPublicRoutes = (routes = {}, path = '', action = () => undefined) => {
if (Object.keys(routes).length > 0 && routes.constructor === Object) {
Object.keys(routes).forEach((route) => {
publicRoutes.push(
setFunctionRoute(route, path,
(req, res, next, connection, userId, user) => {
action(req, res, next, routes[route], user, connection)
}
)
)
})
}
}
/**
* Add routes.
*
* @returns {Array} routes
*/
const generatePrivateRoutes = () => {
setPrivateRoutes(filePrivateRoutes, FILES, addFunctionAsRoute)
return privateRoutes
}
/**
* Add routes.
*
* @returns {Array} routes
*/
const generatePublicRoutes = () => {
setPublicRoutes(filePublicRoutes, FILES, addFunctionAsRoute)
return publicRoutes
}
const functionRoutes = {
private: generatePrivateRoutes(),
public: generatePublicRoutes()
}
module.exports = functionRoutes

View File

@ -0,0 +1,23 @@
/* ------------------------------------------------------------------------- *
* 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. *
* ------------------------------------------------------------------------- */
const FILES = 'files'
const Actions = {
FILES
}
module.exports = Actions

View File

@ -22,7 +22,6 @@ const events = require('events')
const { Document, scalarOptions, stringify } = require('yaml')
const {
writeFileSync,
removeSync,
readdirSync,
statSync,
existsSync,
@ -149,21 +148,6 @@ const createYMLContent = (content = '') => {
return rtn
}
/**
* Delete file.
*
* @param {string} path - the path for delete
*/
const removeFile = (path = '') => {
if (path) {
try {
removeSync(path, { force: true })
} catch (error) {
messageTerminal(defaultError(error && error.message))
}
}
}
/**
* Rename folder.
*
@ -308,7 +292,6 @@ const functionRoutes = {
createYMLContent,
createTemporalFile,
createFolderWithFiles,
removeFile,
renameFolder,
moveToFolder,
findRecursiveFolder,

View File

@ -34,12 +34,12 @@ const {
parsePostData,
getFilesbyEXT,
existsFile,
executeCommand
executeCommand,
removeFile
} = require('server/utils/server')
const {
createTemporalFile,
createYMLContent,
removeFile,
getEndpoint,
getSpecificConfig
} = require('./functions')

View File

@ -37,7 +37,8 @@ const {
getDirectories,
getFilesbyEXT,
executeCommand,
executeCommandAsync
executeCommandAsync,
removeFile
} = require('server/utils/server')
const { checkEmptyObject } = require('server/utils/general')
const {
@ -50,7 +51,6 @@ const {
createTemporalFile,
createFolderWithFiles,
createYMLContent,
removeFile,
renameFolder,
moveToFolder,
findRecursiveFolder,

View File

@ -27,12 +27,12 @@ const {
const {
httpResponse,
parsePostData,
executeCommand
executeCommand,
removeFile
} = require('server/utils/server')
const {
createYMLContent,
createTemporalFile,
removeFile,
getEndpoint,
getSpecificConfig
} = require('./functions')

View File

@ -92,21 +92,25 @@ router.all(
}
const zoneData = getDataZone(zone, defaultOpennebulaZones)
if (zoneData) {
const user = getUserOpennebula()
const password = getPassOpennebula()
const userId = getIdUserOpennebula()
const { rpc } = zoneData
/**
* Instance of connection to opennebula.
*
* @param {string} user - opennegula user
* @param {string} pass - opennebula pass
* @param {string} password - opennebula pass
* @returns {Function} opennebula executer calls to XMLRPC
*/
const connectOpennebula = (
user = getUserOpennebula(),
pass = getPassOpennebula()
) => opennebulaConnect(user, pass, rpc)
user,
password
) => opennebulaConnect(user, password, rpc)
const { resource } = req.params
const routeFunction = checkIfIsARouteFunction(resource, httpMethod)
const routeFunction = checkIfIsARouteFunction(resource, httpMethod, !!userId.length)
res.locals.httpCode = httpResponse(methodNotAllowed)
const dataSources = {
@ -126,14 +130,13 @@ router.all(
)
req.serverDataSource = dataSources
if (valRouteFunction) {
const userIdOpennebula = getIdUserOpennebula()
valRouteFunction(
req,
res,
next,
connectOpennebula,
userIdOpennebula,
{ id: userIdOpennebula, user: getUserOpennebula(), password: getPassOpennebula() }
userId,
{ id: userId, user, password }
)
} else {
next()
@ -190,7 +193,6 @@ router.all(
}
//* worker thread */
const user = getUserOpennebula()
const paramsCommand = getOpennebulaMethod(dataSources)
let workerPath = [__dirname]
if (env && env.NODE_ENV === defaultWebpackMode) {
@ -213,7 +215,7 @@ router.all(
{
globalState: (global && global.paths) || {},
user,
password: getPassOpennebula(),
password,
rpc,
command,
paramsCommand
@ -231,7 +233,11 @@ router.all(
(req, res) => {
clearStates()
const { httpCode } = res.locals
res.status(httpCode.id).json(httpCode)
if (httpCode.file) {
res.sendFile(httpCode.file)
} else {
res.status(httpCode.id).json(httpCode)
}
}
)

View File

@ -66,6 +66,7 @@ const defaults = {
defaultFilesRoutes: [
'2fa',
'auth',
'files',
'oneflow',
'support',
'vcenter',

View File

@ -89,13 +89,14 @@ const checkMethodRouteFunction = (routeFunction, httpMethod = '') => {
*
* @param {string} route - route
* @param {string} httpMethod - http method
* @param {boolean} authenticated - user authenticated
* @returns {object} route function
*/
const checkIfIsARouteFunction = (route, httpMethod) => {
const checkIfIsARouteFunction = (route, httpMethod, authenticated) => {
let rtn = false
if (route && route.length) {
const { private: functionPrivate, public: functionPublic } = functionRoutes
const functions = [...functionPrivate, ...functionPublic]
const functions = authenticated ? functionPrivate : functionPublic
/**
* Finder command.
*
@ -110,6 +111,7 @@ const checkIfIsARouteFunction = (route, httpMethod) => {
rtnCommand.httpMethod === httpMethod &&
rtnCommand.action &&
typeof rtnCommand.action === 'function'
const find = functions.find(finderCommand)
if (find) {
rtn = find

View File

@ -35,7 +35,7 @@ const createJWT = (
exp = ''
) => {
let rtn = null
if (iss && aud && jti && iat && exp && global && global.paths && global.paths.FIREEDGE_KEY) {
if (iss && aud && jti && iat && exp) {
const payload = {
iss,
aud,
@ -43,11 +43,45 @@ const createJWT = (
iat,
exp
}
rtn = jwtEncode(payload)
}
return rtn
}
/**
* Encode JWT.
*
* @param {object} payload - data object
* @returns {object} - jwt or null
*/
const jwtEncode = (payload = {}) => {
let rtn = null
if (global && global.paths && global.paths.FIREEDGE_KEY) {
rtn = jwt.encode(payload, global.paths.FIREEDGE_KEY)
}
return rtn
}
/**
* Decode JWT.
*
* @param {string} token - token JWT
* @returns {object} data JWT
*/
const jwtDecode = (token = '') => {
if (global && global.paths && global.paths.FIREEDGE_KEY) {
try {
return jwt.decode(token, global.paths.FIREEDGE_KEY)
} catch (messageError) {
messageTerminal({
color: 'red',
message: 'invalid: %s',
error: messageError
})
}
}
}
/**
* Validate auth (JWT).
*
@ -60,10 +94,9 @@ const validateAuth = (req = {}) => {
const authorization = req.headers.authorization
const removeBearer = /^Bearer /i
const token = authorization.replace(removeBearer, '')
const fireedgeKey = global && global.paths && global.paths.FIREEDGE_KEY
if (token && fireedgeKey) {
if (token) {
try {
const payload = jwt.decode(token, fireedgeKey)
const payload = jwtDecode(token)
if (
payload &&
'iss' in payload &&
@ -84,7 +117,7 @@ const validateAuth = (req = {}) => {
} catch (error) {
}
} else {
const messageError = (!token && 'jwt') || (!fireedgeKey && 'fireedge_key')
const messageError = token || (global && global.paths && global.paths.FIREEDGE_KEY)
if (messageError) {
messageTerminal({
color: 'red',
@ -117,6 +150,7 @@ const check2Fa = (secret = '', token = '') => {
}
module.exports = {
jwtDecode,
createJWT,
validateAuth,
check2Fa

View File

@ -21,13 +21,14 @@ const { global } = require('window-or-global')
const { resolve } = require('path')
// eslint-disable-next-line node/no-deprecated-api
const { createCipheriv, createCipher, createDecipheriv, createDecipher, createHash } = require('crypto')
const { existsSync, readFileSync, createWriteStream, readdirSync, statSync } = require('fs-extra')
const { existsSync, readFileSync, createWriteStream, readdirSync, statSync, removeSync } = require('fs-extra')
const { internalServerError } = require('./constants/http-codes')
const { messageTerminal } = require('server/utils/general')
const { validateAuth } = require('server/utils/jwt')
const { writeInLogger } = require('server/utils/logger')
const { spawnSync, spawn } = require('child_process')
const {
defaultApps,
from: fromData,
defaultAppName,
defaultConfigFile,
@ -209,9 +210,10 @@ const getKey = () => key
* @param {object} response - response http
* @param {string} data - data for response http
* @param {string} message - message
* @param {string} file - file
* @returns {object} {data, message, id}
*/
const httpResponse = (response = null, data = '', message = '') => {
const httpResponse = (response = null, data = '', message = '', file = '') => {
let rtn = Map(internalServerError).toObject()
rtn.data = data
if (response) {
@ -223,6 +225,11 @@ const httpResponse = (response = null, data = '', message = '') => {
if (message) {
rtn.message = message
}
if (file) {
rtn.message && delete rtn.message
rtn.data && delete rtn.data
rtn.file = file
}
return rtn
}
@ -816,6 +823,32 @@ const executeCommand = (command = '', resource = '', prependCommand = '', option
}
return rtn
}
/**
* Check app name.
*
* @param {string} appName - app name
* @returns {object} app
*/
const checkValidApp = (appName = '') => defaultApps[appName]
/**
* Delete file.
*
* @param {string} path - the path for delete
* @returns {boolean} flag if file is deleted
*/
const removeFile = (path = '') => {
let rtn = false
if (path) {
try {
removeSync(path, { force: true })
rtn = true
} catch (error) {
messageTerminal(defaultError(error && error.message))
}
}
return rtn
}
/**
* Run Asynchronous commands for CLI.
@ -892,5 +925,7 @@ module.exports = {
getFiles,
getFilesbyEXT,
executeCommand,
executeCommandAsync
executeCommandAsync,
checkValidApp,
removeFile
}