1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-23 22:50:09 +03:00

F#3951: oneflow fireedge (#130)

* get and delete oneflow fireedge
* POST oneflow fireedge
* schemas oneflow fireedge
* http code accepted
* fix login with 2FA
* fix oneflow connection with 2fa
* separate service and service_template
* add dev mode front
* fix package json

Signed-off-by: Jorge Lobo <jlobo@opennebula.io>
This commit is contained in:
Jorge Miguel Lobo Escalona 2020-07-29 11:00:26 +02:00 committed by GitHub
parent d5e3ba08bc
commit b4aa12c441
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1225 additions and 189 deletions

View File

@ -1,6 +1,6 @@
# Sunstone React
# Fire Edge
New Sunstone incarnation in React.
Fire Edge server and client
## Build & Run
@ -18,7 +18,7 @@ New Sunstone incarnation in React.
REST Interface. Usually Returns OpenNebula resource info. Login returns a JSON Web Token (`JWT`).
- Login: POST: `http://localhost:3000/api/auth` with params: `user` and `pass`.
- Other: check file `src/config/command-params.js`
- Other: check file `src/config/command-params.js`. if requires it to be in a specific zone you must put it at the end but before the query /feredation=ZONE_ID (replace ZONE_ID for the zone id)
- **zeroMQ**
Websocket connection call to: `ws://127.0.0.1:3000/?token=JWT`
@ -32,25 +32,4 @@ for this error run `sudo sysctl fs.inotify.max_user_watches=582222 && sudo sysct
for this error run `killall -9 node` and start app again
## Project description
- `disk`: this folder content the transpiled code.
- `disk/public`: content the transpiled code valid for the HTML.
- `node_modules`: dependencies for backend (NODE).
- `src`: non-transpiled code.
- `src/config`: configuration files (please do not modify if you do not know how it works).
- `src/config/command-params.js`: it contains the different commands with the possible opennebula parameters.
- `src/config/defaults.js`: it contains default string parameters.
- `src/config/function-routes.js`: it contains routes that are not opennebula commands.
- `src/config/http-codes.js`: it contains all http codes for routes.
- `src/config/params.js`: it contains the parameters used by the paths, example: `http://localhost:3000/api/template/id=0/action=info`.
- `src/config/routes-api.js`: gather the different routes used by the API.
- `src/public`: contains the sunstone in react.
- `src/routes`: contains the routes for node (API and WEB).
- `src/utils`: utilities used by the application.
- `src/index.js`: entrypoint node.
- `config.yml`: enviroment config for user
- `.eslintignore`: files ignored by the eslint
- `copyStaticAssets.js`: copy the html resourses to dist/public path (.ico, .css, fonts, etc).
- `packaje.json`: contains the name of the packages used by the application.
- `webpack.config.js`: contains the JS transpiler configurations.
- `yarn.lock`: contains the dependencies using the yarn command.
...

View File

@ -16,6 +16,12 @@ MODE: production
# JWT user password encryption key (AUTH)
TOKEN_SECRET: token_secreto
# Flow Server
# ONE_FLOW_SERVER:
# PROTOCOL: 'https'
# HOST: '0.0.0.0'
# POST: 2474
# JWT life time
LIMIT_TOKEN:
MIN: 14

View File

@ -5,10 +5,11 @@
"main": "dist/index.js",
"scripts": {
"build": "webpack --mode=production && concurrently \"npm run copy_static_assets\" \"npm run build:css\"",
"build-node": "webpack --mode=production --env=node",
"build-front": "webpack --mode=production --env=front && concurrently \"npm run copy_static_assets\" \"npm run build:css\"",
"build-node": "webpack --mode=production --env=node",
"build-front": "webpack --mode=production --env=front && concurrently \"npm run copy_static_assets\" \"npm run build:css\"",
"build:css": "node-sass src/public/scss/main.scss dist/public/app.css --output-style compressed",
"dev": "webpack --mode=development && npm run copy_static_assets && concurrently \"webpack --watch\" \"npm run build:css\" \"nodemon --inspect dist\"",
"dev:front": "webpack --mode=development --env=front && npm run copy_static_assets && concurrently \"webpack --watch\" \"npm run build:css\" \"nodemon --inspect dist\"",
"start": "node dist/index",
"cypress:open": "cypress open",
"cypress:run": "cypress run --headless --browser chrome --spec \"cypress/integration/**/*.spec.js\"",
@ -31,8 +32,8 @@
"ace-builds": "^1.4.11",
"atob": "^2.1.2",
"axios": "^0.19.2",
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"btoa": "^1.2.1",
"classnames": "^2.2.6",
"colors": "^1.4.0",
"compression": "^1.7.4",
@ -43,6 +44,7 @@
"helmet": "^3.23.3",
"http": "0.0.1-security",
"immutable": "^4.0.0-rc.12",
"jsonschema": "^1.2.6",
"jsonwebtoken": "^8.5.1",
"jwt-simple": "^0.5.6",
"moment": "^2.27.0",
@ -61,11 +63,12 @@
"redux": "^4.0.4",
"redux-thunk": "^2.3.0",
"remove": "^0.1.5",
"socket.io": "^2.3.0",
"socket.io-client": "^2.3.0",
"speakeasy": "^2.0.0",
"sprintf-js": "^1.1.2",
"upcast": "^4.0.0",
"url": "^0.11.0",
"websocket": "^1.0.31",
"window-or-global": "^1.0.1",
"xml2js": "^0.4.23",
"xmlrpc": "^1.3.2",

View File

@ -19,6 +19,7 @@ const helmet = require('helmet');
const express = require('express');
const morgan = require('morgan');
const path = require('path');
const socketIO = require('socket.io');
const cors = require('cors');
const {
existsSync,
@ -106,8 +107,9 @@ const appServer =
)
: unsecureServer(app);
// connect to zeromq websocket
addWsServer(appServer);
// connect to websocket
const io = socketIO.listen(appServer);
addWsServer(io);
appServer.listen(port, () => {
const config = {

View File

@ -0,0 +1,74 @@
/* Copyright 2002-2019, 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 React from 'react';
import Login from './login';
import Settings from '../containers/Settings';
import TestApi from '../containers/TestApi';
import Dashboard from '../containers/Dashboard';
import { Clusters, Hosts, Zones } from '../containers/Infrastructure';
const endpoints = {
login: {
path: '/',
authenticated: false,
menu: false,
component: Login
},
dashboard: {
path: '/dashboard',
component: () => <Dashboard />
},
settings: {
path: '/settings',
component: () => <Settings />
},
test_api: {
path: '/test-api',
component: () => <TestApi />
},
// infrastructure
infrastructure: {
clusters: {
path: '/clusters',
component: () => <Clusters />
},
hosts: {
path: '/hosts',
component: () => <Hosts />
},
zones: {
path: '/zones',
component: () => <Zones />
}
},
// networks
networks: {
vnets: {
path: '/vnets'
},
vnets_templates: {
path: '/vnets-templates'
},
vnets_topology: {
path: '/vnets-topology'
},
vnets_secgroup: {
path: '/secgroup'
}
}
};
export default endpoints;

View File

@ -33,7 +33,7 @@ const TestApi = () => {
fullWidth
select
variant="outlined"
label={<Translate word="Select resquest" />}
label={<Translate word="Select request" />}
value={name}
onChange={handleChangeCommand}
>

View File

@ -13,6 +13,30 @@
/* limitations under the License. */
/* -------------------------------------------------------------------------- */
import WSConsole from './WSConsole';
import React, { useState, useEffect } from 'react';
import io from 'socket.io-client';
import { findStorageData } from '../../../utils';
import constants from '../../../constants';
export default { WSConsole };
const { jwtName } = constants;
const ENDPOINT = 'http://127.0.0.1:3000';
function Webconsole() {
const [response, setResponse] = useState({});
useEffect(() => {
const socket = io(ENDPOINT, {
query: {
token: findStorageData(jwtName)
}
});
socket.on('zeroMQ', data => {
setResponse(data);
});
}, []);
console.log('-->', response);
return <p />;
}
export default Webconsole;

View File

@ -34,8 +34,10 @@ const { from: fromData } = require('../../../../utils/constants/defaults');
const {
responseOpennebula,
checkOpennebulaCommand,
generateNewTemplate
generateNewTemplate,
check2Fa
} = require('../../../../utils/opennebula');
const { httpResponse } = require('../../../../utils/server');
// user config
const appConfig = getConfig();
@ -101,7 +103,7 @@ const privateRoutes = {
const { otpauth_url: otpURL, base32 } = secret;
qrcode.toDataURL(otpURL, (err, dataURL) => {
if (err) {
res.locals.httpCode = Map(internalServerError).toObject();
res.locals.httpCode = httpResponse(internalServerError);
next();
} else {
const connectOpennebula = connect();
@ -115,7 +117,9 @@ const privateRoutes = {
emptyTemplate[default2FAOpennebulaTmpVar] = base32;
dataUser[fromData.resource].id = userId;
dataUser[fromData.postBody].template = generateNewTemplate(
dataUser[
fromData.postBody
].template = generateNewTemplate(
info.USER.TEMPLATE.SUNSTONE || {},
emptyTemplate,
[default2FAOpennebulaVar]
@ -134,11 +138,9 @@ const privateRoutes = {
value,
pass => {
if (pass !== undefined && pass !== null) {
const codeOK = Map(ok).toObject();
codeOK.data = {
res.locals.httpCode = httpResponse(ok, {
img: dataURL
};
res.locals.httpCode = codeOK;
});
next();
} else {
next();
@ -184,22 +186,17 @@ const privateRoutes = {
const sunstone = info.USER.TEMPLATE.SUNSTONE;
const token = req[fromData.postBody].token;
const secret = sunstone[default2FAOpennebulaTmpVar];
const verified = speakeasy.totp.verify({
secret,
encoding: 'base32',
token
});
if (verified) {
if (check2Fa(secret, token)) {
const emptyTemplate = {};
emptyTemplate[default2FAOpennebulaVar] = secret;
const dataUser = Map(req).toObject();
dataUser[fromData.resource].id = userId;
dataUser[fromData.postBody].template = generateNewTemplate(
sunstone || {},
emptyTemplate,
[default2FAOpennebulaTmpVar]
);
dataUser[
fromData.postBody
].template = generateNewTemplate(sunstone || {}, emptyTemplate, [
default2FAOpennebulaTmpVar
]);
const getOpennebulaMethodUpdate = checkOpennebulaCommand(
defaultMethodUserUpdate,
POST
@ -214,8 +211,7 @@ const privateRoutes = {
value,
pass => {
if (pass !== undefined && pass !== null) {
const codeOK = Map(ok).toObject();
res.locals.httpCode = codeOK;
res.locals.httpCode = httpResponse(ok);
}
next();
},
@ -224,7 +220,7 @@ const privateRoutes = {
}
);
} else {
res.locals.httpCode = Map(unauthorized).toObject();
res.locals.httpCode = httpResponse(unauthorized);
next();
}
} else {
@ -271,8 +267,7 @@ const privateRoutes = {
value,
pass => {
if (pass !== undefined && pass !== null) {
const codeOK = Map(ok).toObject();
res.locals.httpCode = codeOK;
res.locals.httpCode = httpResponse(ok);
}
next();
},

View File

@ -12,26 +12,30 @@
/* See the License for the specific language governing permissions and */
/* limitations under the License. */
/* -------------------------------------------------------------------------- */
const speakeasy = require('speakeasy');
const moment = require('moment');
const { Map } = require('immutable');
const {
httpMethod,
defaultMethodLogin,
defaultMethodZones,
defaultMethodConfig,
defaultMethodUserInfo,
default2FAOpennebulaVar,
defaultNamespace,
from: fromData
} = require('../../../../utils/constants/defaults');
const { getConfig } = require('../../../../utils/yml');
const { ok, unauthorized } = require('../../../../utils/constants/http-codes');
const {
ok,
unauthorized,
accepted
} = require('../../../../utils/constants/http-codes');
const { createToken } = require('../../../../utils/jwt');
const { httpResponse } = require('../../../../utils/server');
const {
responseOpennebula,
paramsDefaultByCommandOpennebula,
checkOpennebulaCommand
checkOpennebulaCommand,
check2Fa
} = require('../../../../utils/opennebula');
const appConfig = getConfig();
@ -127,30 +131,31 @@ const validate2faAuthentication = informationUser => {
informationUser.TEMPLATE.SUNSTONE &&
informationUser.TEMPLATE.SUNSTONE[default2FAOpennebulaVar]
) {
if (tfatoken.length <= 0) {
updaterResponse(httpResponse(accepted));
next();
return;
}
const secret = informationUser.TEMPLATE.SUNSTONE[default2FAOpennebulaVar];
const verified = speakeasy.totp.verify({
secret,
encoding: 'base32',
tfatoken
});
if (!verified) {
const codeUnauthorized = Map(unauthorized).toObject();
codeUnauthorized.data = { message: 'invalid 2fa token' };
updaterResponse(codeUnauthorized);
if (!check2Fa(secret, tfatoken)) {
updaterResponse(httpResponse(unauthorized, '', 'invalid 2fa token'));
next();
}
}
};
const genJWT = informationUser => {
if (informationUser && informationUser.ID) {
if (informationUser && informationUser.ID && informationUser.PASSWORD) {
const { ID: id } = informationUser;
const dataJWT = { id, user, token: opennebulaToken };
const jwt = createToken(dataJWT, nowUnix, nowWithDays.format('X'));
if (jwt) {
const codeOK = Map(ok).toObject();
codeOK.data = { token: jwt };
updaterResponse(codeOK);
if (!global.users) {
global.users = {};
}
global.users[user] = opennebulaToken;
updaterResponse(httpResponse(ok, { token: jwt, id: informationUser.ID }));
}
}
};
@ -221,15 +226,13 @@ const userInfo = userData => {
defaultMethodLogin,
getOpennebulaMethod(dataSource),
(err, value) => {
// res, err, value, response, next
responseOpennebula(
() => undefined,
err,
value,
() => {
setZones();
setOneConfig();
// aca se tiene que hacer la llamada a las zonas y a system.config
setOneConfig(); // esto debe de estar antes de hacer el JWT
},
next
);
@ -250,8 +253,7 @@ const authenticate = val => {
const findTextError = `[${namespace + defaultMethodLogin}]`;
if (val) {
if (val.indexOf(findTextError) >= 0) {
const codeUnauthorized = Map(unauthorized).toObject();
updaterResponse(codeUnauthorized);
updaterResponse(httpResponse(unauthorized));
next();
} else {
const oneConnect = connectOpennebula();

View File

@ -19,7 +19,6 @@ const {
defaultMethodLogin
} = require('../../../../utils/constants/defaults');
const {
unauthorized,
internalServerError
} = require('../../../../utils/constants/http-codes');
const { from: fromData } = require('../../../../utils/constants/defaults');
@ -97,6 +96,7 @@ const publicRoutes = {
const dataSourceWithExpirateDate = Map(req).toObject();
// add expire time unix for opennebula creation token
dataSourceWithExpirateDate[fromData.postBody].expire = relativeTime;
dataSourceWithExpirateDate[fromData.postBody].token = '';
oneConnect(
defaultMethodLogin,
getOpennebulaMethod(dataSourceWithExpirateDate),

View File

@ -0,0 +1,133 @@
/* Copyright 2002-2019, 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 { request } = require('axios');
const btoa = require('btoa');
const { Map } = require('immutable');
const {
defaultOneFlowServer
} = require('../../../../utils/constants/defaults');
const { getConfig } = require('../../../../utils/yml');
const { httpMethod } = require('../../../../utils/constants/defaults');
const { addPrintf } = require('../../../../utils/general');
const { httpResponse } = require('../../../../utils/server');
const {
ok,
internalServerError
} = require('../../../../utils/constants/http-codes');
const { GET, DELETE } = httpMethod;
const appConfig = getConfig();
const oneFlowServiceDataConection =
appConfig.ONE_FLOW_SERVER || defaultOneFlowServer;
const parsePostData = (postData = {}) => {
const rtn = {};
Object.entries(postData).forEach(([key, value]) => {
try {
rtn[key] = JSON.parse(value, (k, val) => {
try {
return JSON.parse(val);
} catch (error) {
return val;
}
});
} catch (error) {
rtn[key] = value;
}
});
return rtn;
};
const returnSchemaError = (error = []) =>
error
.map(element => (element && element.stack ? element.stack : ''))
.toString();
const conectionOneFlow = (
res,
next = () => undefined,
method = GET,
user = '',
path = '/',
requestData = '',
postData = ''
) => {
if (res && next && method && user) {
const options = {
method,
baseURL: `${oneFlowServiceDataConection.PROTOCOL}://${oneFlowServiceDataConection.HOST}:${oneFlowServiceDataConection.PORT}`,
url: path,
headers: {
Authorization: `Basic ${btoa(user)}`
},
validateStatus: status => status
};
if (requestData) {
options.url = addPrintf(path, requestData);
}
if (postData) {
options.data = postData;
}
request(options)
.then(response => {
if (response && response.statusText) {
if (response.status >= 200 && response.status < 400) {
if (response.data) {
return response.data;
}
if (
response.config.method &&
response.config.method.toUpperCase() === DELETE
) {
const parseToNumber = validate =>
isNaN(parseInt(validate, 10))
? validate
: parseInt(validate, 10);
return Array.isArray(requestData)
? parseToNumber(requestData[0])
: parseToNumber(requestData);
}
} else if (response.data) {
throw Error(response.data);
}
}
throw Error(response.statusText);
})
.then(data => {
res.locals.httpCode = httpResponse(ok, data);
next();
})
.catch(e => {
const codeInternalServerError = Map(internalServerError).toObject();
if (e && e.message) {
codeInternalServerError.data = e.message;
}
res.locals.httpCode = httpResponse(internalServerError, e && e.message);
next();
});
}
};
const functionRoutes = {
conectionOneFlow,
parsePostData,
returnSchemaError
};
module.exports = functionRoutes;

View File

@ -12,8 +12,76 @@
/* See the License for the specific language governing permissions and */
/* limitations under the License. */
/* -------------------------------------------------------------------------- */
const {
serviceAll,
service,
serviceDelete,
serviceAddAction,
serviceAddScale,
serviceAddRoleAction
} = require('./service');
const {
serviceTemplateAll,
serviceTemplate,
serviceTemplateDelete,
serviceTemplateCreate,
serviceTemplateUpdate,
serviceTemplateAction
} = require('./service_template');
const { httpMethod } = require('../../../../utils/constants/defaults');
const privateRoutes = {};
const { GET, POST, DELETE, PUT } = httpMethod;
const privateRoutes = {
'service-all': {
httpMethod: GET,
action: serviceAll
},
service: {
httpMethod: GET,
action: service
},
'service-delete': {
httpMethod: DELETE,
action: serviceDelete
},
'service-add-action': {
httpMethod: POST,
action: serviceAddAction
},
'service-add-scale': {
httpMethod: POST,
action: serviceAddScale
},
'service-add-role-action': {
httpMethod: POST,
action: serviceAddRoleAction
},
'service_template-all': {
httpMethod: GET,
action: serviceTemplateAll
},
service_template: {
httpMethod: GET,
action: serviceTemplate
},
'service_template-delete': {
httpMethod: DELETE,
action: serviceTemplateDelete
},
'service_template-create': {
httpMethod: POST,
action: serviceTemplateCreate
},
'service_template-update': {
httpMethod: PUT,
action: serviceTemplateUpdate
},
'service_template-action': {
httpMethod: POST,
action: serviceTemplateAction
}
};
const publicRoutes = {};

View File

@ -0,0 +1,218 @@
/* Copyright 2002-2019, 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 action = {
id: '/Action',
type: 'object',
properties: {
action: {
perform: {
type: 'string',
required: true
},
params: {
merge_template: {
type: 'object',
required: false
}
}
}
}
};
const role = {
id: '/Role',
type: 'object',
properties: {
name: {
type: 'string',
required: true
},
cardinality: {
type: 'integer',
default: 1,
minimum: 0
},
vm_template: {
type: 'integer',
required: true
},
vm_template_contents: {
type: 'string',
required: false
},
parents: {
type: 'array',
items: {
type: 'string'
}
},
shutdown_action: {
type: 'string',
enum: ['shutdown', 'shutdown-hard'],
required: false
},
min_vms: {
type: 'integer',
required: false,
minimum: 0
},
max_vms: {
type: 'integer',
required: false,
minimum: 0
},
cooldown: {
type: 'integer',
required: false,
minimum: 0
},
elasticity_policies: {
type: 'array',
items: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['CHANGE', 'CARDINALITY', 'PERCENTAGE_CHANGE'],
required: true
},
adjust: {
type: 'integer',
required: true
},
min_adjust_step: {
type: 'integer',
required: false,
minimum: 1
},
period_number: {
type: 'integer',
required: false,
minimum: 0
},
period: {
type: 'integer',
required: false,
minimum: 0
},
expression: {
type: 'string',
required: true
},
cooldown: {
type: 'integer',
required: false,
minimum: 0
}
}
}
},
scheduled_policies: {
type: 'array',
items: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['CHANGE', 'CARDINALITY', 'PERCENTAGE_CHANGE'],
required: true
},
adjust: {
type: 'integer',
required: true
},
min_adjust_step: {
type: 'integer',
required: false,
minimum: 1
},
start_time: {
type: 'string',
required: false
},
recurrence: {
type: 'string',
required: false
}
}
}
}
}
};
const service = {
type: 'object',
properties: {
name: {
type: 'string',
required: true
},
deployment: {
type: 'string',
enum: ['none', 'straight'],
default: 'none'
},
description: {
type: 'string',
required: false
},
shutdown_action: {
type: 'string',
enum: ['terminate', 'terminate-hard', 'shutdown', 'shutdown-hard'],
required: false
},
roles: {
type: 'array',
items: { $ref: '/Role' },
required: true
},
custom_attrs: {
type: 'object',
properties: {},
required: false
},
custom_attrs_values: {
type: 'object',
properties: {},
required: false
},
networks: {
type: 'object',
properties: {},
required: false
},
networks_values: {
type: 'array',
items: {
type: 'object',
properties: {}
},
required: false
},
ready_status_gate: {
type: 'boolean',
required: false
}
}
};
const functionRoutes = {
action,
role,
service
};
module.exports = functionRoutes;

View File

@ -0,0 +1,239 @@
/* Copyright 2002-2019, 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 { Validator } = require('jsonschema');
const { action } = require('./schemas');
const { conectionOneFlow } = require('./functions');
const { httpMethod } = require('../../../../utils/constants/defaults');
const { from: fromData } = require('../../../../utils/constants/defaults');
const { httpResponse } = require('../../../../utils/server');
const {
methodNotAllowed,
internalServerError
} = require('../../../../utils/constants/http-codes');
const { parsePostData, returnSchemaError } = require('./functions');
const { GET, POST, DELETE } = httpMethod;
const serviceAll = (req, res, next, connect, zone, user) => {
if (req && res && user && next) {
conectionOneFlow(res, next, GET, user, '/service');
} else {
next();
}
};
const service = (req, res, next, connect, zone, user) => {
if (req && res && user && next) {
if (
req &&
fromData &&
fromData.resource &&
req[fromData.resource] &&
req[fromData.resource].method
) {
conectionOneFlow(
res,
next,
GET,
user,
`/service/{0}`,
req[fromData.resource].method
);
} else {
res.locals.httpCode = httpResponse(
methodNotAllowed,
'',
'invalid id service'
);
next();
}
} else {
next();
}
};
const serviceDelete = (req, res, next, connect, zone, user) => {
if (req && res && user && next) {
if (
req &&
fromData &&
fromData.resource &&
req[fromData.resource] &&
req[fromData.resource].method
) {
conectionOneFlow(res, next, DELETE, user, `/service/{0}`, [
req[fromData.resource].method
]);
} else {
res.locals.httpCode = httpResponse(
methodNotAllowed,
'',
'invalid id service'
);
next();
}
} else {
next();
}
};
// PROBAR oneflow-server.rb
const serviceAddAction = (req, res, next, connect, zone, user) => {
if (req && res && user && next) {
if (
req &&
fromData &&
fromData.resource &&
fromData.postBody &&
req[fromData.resource] &&
req[fromData.postBody] &&
req[fromData.resource].method
) {
const postAction = parsePostData(req[fromData.postBody]);
const v = new Validator();
const valSchema = v.validate(postAction, action);
if (valSchema.valid) {
conectionOneFlow(
res,
next,
POST,
user,
`/service/{0}/action`,
req[fromData.resource].method,
postAction
);
} else {
res.locals.httpCode = httpResponse(
internalServerError,
'',
`invalid schema ${returnSchemaError(valSchema.errors)}`
);
next();
}
} else {
res.locals.httpCode = httpResponse(
methodNotAllowed,
'',
'invalid action or id'
);
next();
}
} else {
next();
}
};
// PROBAR
const serviceAddScale = (req, res, next, connect, zone, user) => {
if (req && res && user && next) {
if (
req &&
fromData &&
fromData.resource &&
fromData.postBody &&
req[fromData.resource] &&
req[fromData.postBody] &&
req[fromData.resource].method
) {
const postAction = parsePostData(req[fromData.postBody]);
const v = new Validator();
const valSchema = v.validate(postAction, action);
if (valSchema.valid) {
conectionOneFlow(
res,
next,
POST,
user,
`/service/{0}/action`,
req[fromData.resource].method,
postAction
);
} else {
res.locals.httpCode = httpResponse(
internalServerError,
'',
`invalid schema ${returnSchemaError(valSchema.errors)}`
);
next();
}
} else {
res.locals.httpCode = httpResponse(
methodNotAllowed,
'',
'invalid action or id'
);
next();
}
} else {
next();
}
};
// PROBAR oneflow-server.rb
const serviceAddRoleAction = (req, res, next, connect, zone, user) => {
if (req && res && user && next) {
if (
req &&
fromData &&
fromData.resource &&
fromData.postBody &&
req[fromData.resource] &&
req[fromData.postBody] &&
req[fromData.resource].method &&
req[fromData.resource].id
) {
const postAction = parsePostData(req[fromData.postBody]);
const v = new Validator();
const valSchema = v.validate(postAction, action);
if (valSchema.valid) {
conectionOneFlow(
res,
next,
POST,
user,
`/service/{0}/role/{1}`,
[req[fromData.resource].method, req[fromData.resource].id],
postAction
);
} else {
res.locals.httpCode = httpResponse(
internalServerError,
'',
`invalid schema ${returnSchemaError(valSchema.errors)}`
);
next();
}
} else {
res.locals.httpCode = httpResponse(
methodNotAllowed,
'',
'invalid action or id'
);
next();
}
} else {
next();
}
};
const serviceApi = {
serviceAll,
service,
serviceDelete,
serviceAddAction,
serviceAddScale,
serviceAddRoleAction
};
module.exports = serviceApi;

View File

@ -0,0 +1,226 @@
/* Copyright 2002-2019, 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 { Validator } = require('jsonschema');
const { conectionOneFlow } = require('./functions');
const { role, service, action } = require('./schemas');
const { httpMethod } = require('../../../../utils/constants/defaults');
const { from: fromData } = require('../../../../utils/constants/defaults');
const { httpResponse } = require('../../../../utils/server');
const {
methodNotAllowed,
internalServerError
} = require('../../../../utils/constants/http-codes');
const { parsePostData, returnSchemaError } = require('./functions');
const { GET, POST, DELETE, PUT } = httpMethod;
const serviceTemplateAll = (req, res, next, connect, zone, user) => {
if (req && res && user && next) {
conectionOneFlow(res, next, GET, user, '/service_template');
} else {
next();
}
};
const serviceTemplate = (req, res, next, connect, zone, user) => {
if (req && res && user && next) {
if (
req &&
fromData &&
fromData.resource &&
req[fromData.resource] &&
req[fromData.resource].method
) {
conectionOneFlow(res, next, GET, user, `/service_template/{0}`, [
req[fromData.resource].method
]);
} else {
res.locals.httpCode = httpResponse(
methodNotAllowed,
'',
'invalid id service template'
);
next();
}
} else {
next();
}
};
const serviceTemplateDelete = (req, res, next, connect, zone, user) => {
if (req && res && user && next) {
if (
req &&
fromData &&
fromData.resource &&
req[fromData.resource] &&
req[fromData.resource].method
) {
conectionOneFlow(res, next, DELETE, user, `/service_template/{0}`, [
req[fromData.resource].method
]);
} else {
res.locals.httpCode = httpResponse(
methodNotAllowed,
'',
'invalid id service'
);
next();
}
} else {
next();
}
};
const serviceTemplateCreate = (req, res, next, connect, zone, user) => {
if (req && res && user && next) {
if (req && fromData && fromData.postBody && req[fromData.postBody]) {
const postService = parsePostData(req[fromData.postBody]);
const v = new Validator();
v.addSchema(role, '/Role');
const valSchema = v.validate(postService, service);
if (valSchema.valid) {
conectionOneFlow(
res,
next,
POST,
user,
`/service_template`,
'',
postService
);
} else {
res.locals.httpCode = httpResponse(
internalServerError,
'',
`invalid schema ${returnSchemaError(valSchema.errors)}`
);
next();
}
} else {
res.locals.httpCode = httpResponse(
methodNotAllowed,
'',
'invalid service json'
);
next();
}
} else {
next();
}
};
// PROBAR
const serviceTemplateUpdate = (req, res, next, connect, zone, user) => {
if (req && res && user && next) {
if (
req &&
fromData &&
fromData.postBody &&
fromData.resource &&
req[fromData.postBody] &&
req[fromData.resource] &&
req[fromData.resource].method
) {
const postService = parsePostData(req[fromData.postBody]);
const v = new Validator();
v.addSchema(role, '/Role');
const valSchema = v.validate(postService, service);
if (valSchema.valid) {
conectionOneFlow(
res,
next,
PUT,
user,
`/service_template/{0}`,
[req[fromData.resource].method],
postService
);
} else {
res.locals.httpCode = httpResponse(
internalServerError,
'',
`invalid schema ${returnSchemaError(valSchema.errors)}`
);
next();
}
} else {
res.locals.httpCode = httpResponse(
methodNotAllowed,
'',
'invalid service json or id'
);
next();
}
} else {
next();
}
};
// PROBAR oneflow-server.rb
const serviceTemplateAction = (req, res, next, connect, zone, user) => {
if (req && res && user && next) {
if (
req &&
fromData &&
fromData.postBody &&
fromData.resource &&
req[fromData.postBody] &&
req[fromData.resource] &&
req[fromData.resource].method
) {
const postAction = parsePostData(req[fromData.postBody]);
const v = new Validator();
const valSchema = v.validate(postAction, action);
if (valSchema.valid) {
conectionOneFlow(
res,
next,
POST,
user,
`/service_template/{0}/action`,
[req[fromData.resource].method],
postAction
);
} else {
res.locals.httpCode = httpResponse(
internalServerError,
'',
`invalid schema ${returnSchemaError(valSchema.errors)}`
);
next();
}
} else {
res.locals.httpCode = httpResponse(
methodNotAllowed,
'',
'invalid action or id'
);
next();
}
} else {
next();
}
};
const serviceTemplateApi = {
serviceTemplateAll,
serviceTemplate,
serviceTemplateDelete,
serviceTemplateCreate,
serviceTemplateUpdate,
serviceTemplateAction
};
module.exports = serviceTemplateApi;

View File

@ -14,16 +14,15 @@
/* -------------------------------------------------------------------------- */
const express = require('express');
const { Map } = require('immutable');
const { defaults, httpCodes, params } = require('../../utils/constants');
const { getConfig } = require('../../utils/yml');
const {
opennebulaConnect,
checkRouteFunction,
commandXML,
checkOpennebulaCommand,
validateRouteFunction,
responseOpennebula
responseOpennebula,
httpResponse
} = require('../../utils');
const {
@ -77,7 +76,7 @@ router.all(
(req, res, next) => {
const { internalServerError, ok, methodNotAllowed } = httpCodes;
const { method: httpMethod } = req;
res.locals.httpCode = Map(internalServerError).toObject();
res.locals.httpCode = httpResponse(internalServerError);
const zone = getDataZone();
if (zone) {
const { RPC } = zone;
@ -87,7 +86,7 @@ router.all(
) => opennebulaConnect(user, pass, RPC);
const { resource } = req.params;
const routeFunction = checkRouteFunction(resource);
res.locals.httpCode = Map(methodNotAllowed).toObject();
res.locals.httpCode = httpResponse(methodNotAllowed);
const dataSources = {
[fromData.resource]: getParamsState(),
[fromData.query]: getQueriesState(),
@ -104,7 +103,8 @@ router.all(
res,
next,
connectOpennebula,
getIdUserOpennebula()
getIdUserOpennebula(),
`${getUserOpennebula()}:${getPassOpennebula()}`
);
} else {
next();
@ -118,12 +118,10 @@ router.all(
);
const getOpennebulaMethod = checkOpennebulaCommand(command, httpMethod);
if (getOpennebulaMethod) {
const response = val => {
res.locals.httpCode = Map(ok).toObject();
res.locals.httpCode.data = val || {};
const response = (val = {}) => {
res.locals.httpCode = httpResponse(ok, val || val === 0 ? val : {});
if (typeof val === 'string') {
res.locals.httpCode.data = {};
res.locals.httpCode.message = val;
res.locals.httpCode = httpResponse(ok, undefined, val);
}
next();
};
@ -138,11 +136,8 @@ router.all(
getPassOpennebula(),
RPC
);
connect(
command,
getOpennebulaMethod(dataSources),
(err, value) =>
responseOpennebula(updaterResponse, err, value, response, next)
connect(command, getOpennebulaMethod(dataSources), (err, value) =>
responseOpennebula(updaterResponse, err, value, response, next)
);
} else {
next();

View File

@ -59,8 +59,16 @@ const validateResource = (req, res, next) => {
idUserOpennebula = session.iss;
userOpennebula = session.aud;
passOpennebula = session.jti;
next();
return;
// deberia estar condicionado por la variable de entorno dev
if (
global &&
global.users &&
global.users[userOpennebula] &&
global.users[userOpennebula] === passOpennebula
) {
next();
return;
}
}
status = unauthorized;
}

View File

@ -159,7 +159,7 @@ module.exports = (
},
group: {
from: postBody,
default: ''
default: 0
}
}
},

View File

@ -14,7 +14,8 @@
/* -------------------------------------------------------------------------- */
const default2FAOpennebulaVar = 'TWO_FACTOR_AUTH_SECRET';
const defaultIp = 'http://127.0.0.1';
const defaultIp = '127.0.0.1';
const protocol = 'http';
const defaults = {
httpMethod: {
GET: 'GET',
@ -31,10 +32,15 @@ const defaults = {
{
ID: 0,
NAME: 'OpenNebula',
RPC: `${defaultIp}:2633/RPC2`,
RPC: `${protocol}://${defaultIp}:2633/RPC2`,
VNC: ''
}
],
defaultOneFlowServer: {
PROTOCOL: protocol,
HOST: defaultIp,
PORT: 2474
},
defaultConfigFile: `${__dirname}/../config.yml`,
defaultTypeLog: 'prod',
defaultWebpackMode: 'development',

View File

@ -34,6 +34,10 @@ const httpCodes = {
id: 503,
message: 'Service Unavailable'
},
accepted: {
id: 202,
message: 'Accepted'
},
ok: {
id: 200,
message: 'OK'

View File

@ -27,6 +27,18 @@ const messageTerminal = ({ color, type, error, message }) => {
console.log(consoleColor, typeConsole, errorConsole);
};
module.exports = {
messageTerminal
const addPrintf = (string = '', args = '') => {
let rtn = string;
if (string && args) {
const replacers = Array.isArray(args) ? args : [args];
rtn = string.replace(/{(\d+)}/g, (match, number) =>
typeof replacers[number] !== 'undefined' ? replacers[number] : match
);
}
return rtn;
};
module.exports = {
messageTerminal,
addPrintf
};

View File

@ -18,7 +18,8 @@ const params = require('./constants/params');
const { defaultTypeLog } = require('./constants/defaults');
const functionRoutes = require('../routes/api');
const { validateAuth } = require('./jwt');
const { messageTerminal } = require('./general');
const { httpResponse } = require('./server');
const { messageTerminal, addPrintf } = require('./general');
const { addWsServer } = require('./ws-zeromq');
const { getConfig } = require('./yml');
@ -125,6 +126,7 @@ module.exports = {
includeJSbyHTML,
includeCSSbyHTML,
messageTerminal,
addPrintf,
getRouteForOpennebulaCommand,
getMethodForOpennebulaCommand,
commandXML,
@ -132,5 +134,6 @@ module.exports = {
checkOpennebulaCommand,
validateRouteFunction,
responseOpennebula,
getConfig
getConfig,
httpResponse
};

View File

@ -19,6 +19,7 @@ const rpc = require('xmlrpc');
const xml2js = require('xml2js');
const { Map } = require('immutable');
const { sprintf } = require('sprintf-js');
const speakeasy = require('speakeasy');
const httpCodes = require('./constants/http-codes');
const commandsParams = require('./constants/commands');
const {
@ -263,13 +264,30 @@ const generateNewTemplate = (
wrapper = 'SUNSTONE=[%1$s]'
) => {
const positions = Object.entries({ ...current, ...addPositions })
.filter(position => position && !removedPositions.includes(position))
.filter(position => {
let element = position;
if (Array.isArray(position)) {
element = position[0];
}
return element && !removedPositions.includes(element);
})
.map(([position, value]) => `${position}=${value}`)
.join(', ');
return sprintf(wrapper, positions);
};
const check2Fa = (secret = '', token = '') => {
let rtn = false;
if (secret && token) {
rtn = speakeasy.totp.verify({
secret,
encoding: 'base32',
token
});
}
return rtn;
};
module.exports = {
opennebulaConnect,
responseOpennebula,
@ -280,5 +298,6 @@ module.exports = {
checkPositionInDataSource,
checkOpennebulaCommand,
paramsDefaultByCommandOpennebula,
generateNewTemplate
generateNewTemplate,
check2Fa
};

View File

@ -0,0 +1,35 @@
/* Copyright 2002-2019, 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 { Map } = require('immutable');
const { internalServerError } = require('./constants/http-codes');
const httpResponse = (response, data, message) => {
let rtn = Map(internalServerError).toObject();
rtn.data = data;
if (response) {
rtn = Map(response).toObject();
}
if (data || data === 0) {
rtn.data = data;
}
if (message) {
rtn.message = message;
}
return rtn;
};
module.exports = {
httpResponse
};

View File

@ -15,14 +15,13 @@
const atob = require('atob');
const { server: Server } = require('websocket');
const { socket } = require('zeromq');
const { socket: socketZeroMQ } = require('zeromq');
const xml2js = require('xml2js');
const { messageTerminal } = require('./general');
const { getConfig } = require('./yml');
const { validateAuth } = require('./jwt');
const { unauthorized } = require('./constants/http-codes');
// const { unauthorized } = require('./constants/http-codes');
// user config
const appConfig = getConfig();
@ -32,92 +31,74 @@ const zeromqPort = appConfig.ZEROPORT || 2101;
const zeromqHost = appConfig.ZEROHOST || '127.0.0.1';
const addWsServer = appServer => {
if (
appServer &&
appServer.constructor &&
appServer.constructor.name &&
appServer.constructor.name === 'Server'
) {
// create the server
const wsServer = new Server({
httpServer: appServer
});
// connect to zeromq
const zeromqSock = socketZeroMQ('sub');
const address = `${zeromqType}://${zeromqHost}:${zeromqPort}`;
try {
zeromqSock.connect(address);
zeromqSock.subscribe('');
// connect to zeromq
const zeromqSock = socket('sub');
const address = `${zeromqType}://${zeromqHost}:${zeromqPort}`;
try {
zeromqSock.connect(address);
let clients = [];
wsServer.on('request', request => {
appServer
.use((socketServer, next) => {
if (
request &&
request.resourceURL &&
request.resourceURL.query &&
request.resourceURL.query.token &&
socketServer.handshake.query &&
socketServer.handshake.query.token &&
validateAuth({
headers: { authorization: request.resourceURL.query.token }
headers: { authorization: socketServer.handshake.query.token }
})
) {
const clientConnection = request.accept(null, request.origin);
clients.push(clientConnection);
zeromqSock.subscribe('');
zeromqSock.on('message', (...args) => {
const mssgs = [];
// broadcast
clients.forEach(client => {
Array.prototype.slice.call(args).forEach(arg => {
mssgs.push(arg.toString());
});
if (mssgs[0] && mssgs[1]) {
xml2js.parseString(
atob(mssgs[1]),
{
explicitArray: false,
trim: true,
normalize: true,
includeWhiteChars: true,
strict: false
},
(error, result) => {
if (error) {
const configErrorParser = {
color: 'red',
type: error,
message: 'Error parser: %s'
};
messageTerminal(configErrorParser);
return;
}
client.send(
JSON.stringify({ command: mssgs[0], data: result })
);
}
);
}
});
});
next();
} else {
const { id, message } = unauthorized;
request.reject(id, message);
next(new Error('Authentication error'));
}
});
})
.on('connection', socketServer => {
zeromqSock.on('message', (...args) => {
const mssgs = [];
Array.prototype.slice.call(args).forEach(arg => {
mssgs.push(arg.toString());
});
if (mssgs[0] && mssgs[1]) {
xml2js.parseString(
atob(mssgs[1]),
{
explicitArray: false,
trim: true,
normalize: true,
includeWhiteChars: true,
strict: false
},
(error, result) => {
if (error) {
const configErrorParser = {
color: 'red',
type: error,
message: 'Error parser: %s'
};
messageTerminal(configErrorParser);
} else {
socketServer.emit('zeroMQ', {
command: mssgs[0],
data: result
});
}
}
);
}
});
wsServer.on('close', request => {
// clear connection to broadcast
clients = clients.filter(client => client !== request);
/* socketServer.on('disconnect', () => {
console.log('Client disconnected');
clearInterval(interval);
}); */
});
} catch (error) {
const configErrorZeroMQ = {
color: 'red',
type: error,
message: '%s'
};
messageTerminal(configErrorZeroMQ);
}
} catch (error) {
const configErrorZeroMQ = {
color: 'red',
type: error,
message: '%s'
};
messageTerminal(configErrorZeroMQ);
}
};

View File

@ -236,6 +236,8 @@ delete '/service/:id' do
end
post '/service/:id/action' do
#require 'pry-byebug'
#binding.pry
action = JSON.parse(request.body.read)['action']
opts = action['params']
@ -458,6 +460,8 @@ post '/service_template' do
s_template = OpenNebula::ServiceTemplate.new(xml, @client)
begin
# require 'pry-byebug'
# binding.pry
rc = s_template.allocate(request.body.read)
rescue Validator::ParseException, JSON::ParserError => e
return internal_error(e.message, VALIDATION_EC)