From 84ed1089ce1514d5d6a3a436323ff2bd34bd0e08 Mon Sep 17 00:00:00 2001
From: "mati108@gmail.com" <34651529+matbenitez@users.noreply.github.com>
Date: Wed, 13 Feb 2019 19:43:20 -0300
Subject: [PATCH] Adding integration with key vault
---
client/package-lock.json | 2 +-
client/package.json | 2 +-
client/src/GraphQL/Calls/index.js | 4 +-
client/src/GraphQL/Calls/keys.js | 15 ++++
client/src/GraphQL/Fragments/clone.js | 1 +
client/src/GraphQL/models.js | 1 -
client/src/index.js | 8 +-
client/src/views/Clone/Add/index.js | 82 ++++++++++++++++---
server/README.md | 29 +++++++
server/src/errors/index.js | 1 -
server/src/graphQLCalls/authorizeClone.js | 28 +++++++
server/src/graphQLCalls/teamQuery.js | 4 +-
server/src/kubernetes/createClone.js | 5 ++
server/src/models/clone.js | 1 +
server/src/schema/cloneDetails.js | 10 +--
.../src/schema/mutations/AddCloneMutation.js | 61 ++++++++------
.../schema/mutations/RemoveCloneMutation.js | 3 +
.../mutations/UpdateExecutionSummary.js | 4 +-
server/src/schema/queries/GetClones.js | 3 +-
server/src/schema/types/CloneType.js | 3 +-
server/src/schema/types/input/Clone.js | 3 +-
21 files changed, 209 insertions(+), 61 deletions(-)
create mode 100644 client/src/GraphQL/Calls/keys.js
create mode 100644 server/src/graphQLCalls/authorizeClone.js
diff --git a/client/package-lock.json b/client/package-lock.json
index 2f4b4a7ba..7ada4ee5a 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "@superalgos/operations-client",
- "version": "0.0.2",
+ "version": "0.0.13",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/client/package.json b/client/package.json
index 43a89253f..40b5a1d21 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "@superalgos/operations-client",
- "version": "0.0.13",
+ "version": "0.0.14",
"description": "Superalgos Operations client-side web app",
"main": "src/index.js",
"dependencies": {
diff --git a/client/src/GraphQL/Calls/index.js b/client/src/GraphQL/Calls/index.js
index f410adf89..7f1d17294 100644
--- a/client/src/GraphQL/Calls/index.js
+++ b/client/src/GraphQL/Calls/index.js
@@ -1,7 +1,9 @@
import clones from './clones';
import teams from './teams';
+import keys from './keys';
export {
clones,
- teams
+ teams,
+ keys
};
diff --git a/client/src/GraphQL/Calls/keys.js b/client/src/GraphQL/Calls/keys.js
new file mode 100644
index 000000000..97bd6b26f
--- /dev/null
+++ b/client/src/GraphQL/Calls/keys.js
@@ -0,0 +1,15 @@
+import gql from 'graphql-tag';
+
+export const GET_ALL_KEYS = gql`
+ query {
+ keyVault_AvailableKeys {
+ id
+ key
+ description
+ }
+ }
+`
+
+export default {
+ GET_ALL_KEYS
+};
diff --git a/client/src/GraphQL/Fragments/clone.js b/client/src/GraphQL/Fragments/clone.js
index 42022c0bd..6993b966b 100644
--- a/client/src/GraphQL/Fragments/clone.js
+++ b/client/src/GraphQL/Fragments/clone.js
@@ -30,5 +30,6 @@ export const clone = gql`
botAvatar
teamAvatar
processName
+ keyId
}
`;
diff --git a/client/src/GraphQL/models.js b/client/src/GraphQL/models.js
index c7a5df51b..82f6c331e 100644
--- a/client/src/GraphQL/models.js
+++ b/client/src/GraphQL/models.js
@@ -3,7 +3,6 @@ const availableMonths = ["January", "February", "March", "April", "May", "June",
const tradingStartModes ={
live: "Live",
- competition: "Competition",
backtest: "Backtest"
}
diff --git a/client/src/index.js b/client/src/index.js
index 970c534aa..fbec00c0c 100644
--- a/client/src/index.js
+++ b/client/src/index.js
@@ -12,10 +12,10 @@ import Home from './views/Home'
const App = () => (
-
-
-
-
+
+
+
+
)
diff --git a/client/src/views/Clone/Add/index.js b/client/src/views/Clone/Add/index.js
index 3db9ccfdd..15bda776f 100644
--- a/client/src/views/Clone/Add/index.js
+++ b/client/src/views/Clone/Add/index.js
@@ -1,6 +1,6 @@
import React, { Component } from 'react'
import { graphql, compose, Query } from 'react-apollo'
-import { clones, teams } from '../../../GraphQL/Calls'
+import { clones, teams, keys } from '../../../GraphQL/Calls'
import { DateTime } from 'luxon';
import TopBar from '../../BannerTopBar'
@@ -45,6 +45,7 @@ class AddClone extends Component {
runAsTeam: false,
teams: [],
teamId: '',
+ keyId: '',
// Indicator Bot
startYear: 2019,
@@ -60,7 +61,8 @@ class AddClone extends Component {
isNewCloneConfirmationOpen: false,
serverResponse: '',
serverError: false,
- processNameError: false
+ processNameError: false,
+ keyIdError: false
}
}
@@ -230,6 +232,60 @@ class AddClone extends Component {
}
+ { this.state.mode == "live" &&
+
+
+ Select one of your available Exchange API Keys to be used by this clone.
+
+
+
+ {({ loading, error, data }) => {
+ if (loading) return (
+ this.setState({keyId:e.target.value})}
+ onBlur={(e)=>this.setState({keyIdError:false})}
+ error={this.state.keyIdError}
+ fullWidth
+ >
+
+
+ );
+ if (error) return `Error! ${error.message}`;
+ const list = data.keyVault_AvailableKeys.map((key) => (
+
+ ));
+ return (
+ this.setState({keyId:e.target.value})}
+ onBlur={(e)=>this.setState({keyIdError:false})}
+ error={this.state.keyIdError}
+ fullWidth
+ >
+ { list }
+
+ );
+ }}
+
+
+
+
+
+ }
+
If Run as Team is selected, the clone will be taken from the
team respository instead of your own team member folder.
@@ -399,7 +455,8 @@ class AddClone extends Component {
-
@@ -431,7 +488,7 @@ class AddClone extends Component {
let serverResponse = await this.createCloneOnServer()
error = serverResponse.errors || !isDefined(serverResponse.data.operations_AddClone)
if(error){
- this.state.serverResponse = serverResponse.errors[0].message
+ this.state.serverResponse = serverResponse.errors[0].message || serverResponse.errors[0]
this.state.serverError = true
}else{
this.state.serverResponse = "The new clone was sucessfully created."
@@ -453,15 +510,16 @@ class AddClone extends Component {
}
}
- if(this.state.mode === "backtest"){
- variables.clone.beginDatetime = this.state.beginDatetime.valueOf() / 1000|0
- variables.clone.endDatetime = this.state.endDatetime.valueOf() / 1000|0
- variables.clone.waitTime = this.state.waitTime
- }
-
if(this.state.selectedBot.kind === "TRADER"){
variables.clone.runAsTeam = this.state.runAsTeam
variables.clone.processName = 'Trading-Process'
+ if(this.state.mode === "backtest"){
+ variables.clone.beginDatetime = this.state.beginDatetime.valueOf() / 1000|0
+ variables.clone.endDatetime = this.state.endDatetime.valueOf() / 1000|0
+ variables.clone.waitTime = this.state.waitTime
+ } else {
+ variables.clone.keyId = this.state.keyId
+ }
} else {
variables.clone.processName = this.state.processName
variables.clone.startYear = this.state.startYear
@@ -508,6 +566,7 @@ class AddClone extends Component {
processName: '',
teams: [],
teamId: '',
+ keyId: '',
//Error handlers
nameError: false,
@@ -517,7 +576,8 @@ class AddClone extends Component {
isNewCloneConfirmationOpen: false,
serverResponse: '',
serverError: false,
- processNameError: false
+ processNameError: false,
+ keyIdError: false
})
};
diff --git a/server/README.md b/server/README.md
index bb4cbecf6..6607a6cfd 100644
--- a/server/README.md
+++ b/server/README.md
@@ -2,3 +2,32 @@
## AA Operations Server
Server side functions support the business rules for the module.
+
+## Available Methods
+
+# Active clones List
+- List active personal clones
+- List active team clones (future release)
+
+# History clone List
+- List history personal clones
+- List history team clones (future release)
+
+# Get clone by id
+- Get existing clones corresponding to the clone ids passed by parameter and returns:
+ - Clone id
+ - Team id
+ - Bot id
+
+# Add new clones:
+ Receive a list of clones, create each of them and return the newly created clone.
+ A clone can be created:
+ - As a personal clone (run from the personal user folder)
+ - As a team clone (on competitions, run from the team folder)
+ Who can create each type:
+ - Personal clones are managed by the user who creates it, not visible for any other user.
+ - Team clones are managed by any ADMIN type team member (on the team member role attribute),
+ and all team admins will see it listed.
+
+ - Events Module:
+ At enroll time, the clone is created and the authId is sent
diff --git a/server/src/errors/index.js b/server/src/errors/index.js
index 63b27a61a..74e7c36ff 100644
--- a/server/src/errors/index.js
+++ b/server/src/errors/index.js
@@ -53,6 +53,5 @@ export class KubernateError extends Error {
message = 'OperationsModule Kubernate Error. ' + this.message
constructor(message){
super(message)
- logger.error(this.stack)
}
}
diff --git a/server/src/graphQLCalls/authorizeClone.js b/server/src/graphQLCalls/authorizeClone.js
new file mode 100644
index 000000000..da12c9879
--- /dev/null
+++ b/server/src/graphQLCalls/authorizeClone.js
@@ -0,0 +1,28 @@
+import logger from '../config/logger'
+import axios from 'axios'
+
+const authorizeClon = (authorization, keyId, cloneId, releaseClon) => {
+ logger.debug('authorizeClon -> Entering function.')
+
+ return axios({
+ url: process.env.GATEWAY_ENDPOINT,
+ method: 'post',
+ data: {
+ query: `
+ mutation keyVault_AuthorizeClone($keyId: String!, $cloneId: String!, $releaseClon: Boolean!){
+ keyVault_AuthorizeClone(keyId: $keyId, cloneId: $cloneId, releaseClon:$releaseClon)
+ }
+ `,
+ variables: {
+ keyId: keyId,
+ cloneId: cloneId,
+ releaseClon: releaseClon
+ }
+ },
+ headers: {
+ authorization: authorization
+ }
+ })
+}
+
+export default authorizeClon
diff --git a/server/src/graphQLCalls/teamQuery.js b/server/src/graphQLCalls/teamQuery.js
index c42c1722a..d5c3e32c4 100644
--- a/server/src/graphQLCalls/teamQuery.js
+++ b/server/src/graphQLCalls/teamQuery.js
@@ -1,8 +1,8 @@
import logger from '../config/logger'
import axios from 'axios'
-const teamQuery = async(authorization, teamId) => {
- logger.debug('GraphQL retrieving team information. %s', teamId)
+const teamQuery = (authorization, teamId) => {
+ logger.debug('teamQuery -> Entering function')
return axios({
url: process.env.GATEWAY_ENDPOINT,
diff --git a/server/src/kubernetes/createClone.js b/server/src/kubernetes/createClone.js
index e4db79b8e..78e03c181 100644
--- a/server/src/kubernetes/createClone.js
+++ b/server/src/kubernetes/createClone.js
@@ -104,6 +104,11 @@ const createClone = async (clone) => {
"name": "WAIT_TIME",
"value": clone.waitTime.toString()
})
+ } else{
+ env.push({
+ "name": "KEY_ID",
+ "value": clone.keyId
+ })
}
} else if(clone.botType === INDICATOR || clone.botType === EXTRACTION ){
if(clone.mode !== NO_TIME){
diff --git a/server/src/models/clone.js b/server/src/models/clone.js
index e69df8262..d1071b4ec 100644
--- a/server/src/models/clone.js
+++ b/server/src/models/clone.js
@@ -23,6 +23,7 @@ const cloneSchema = new Schema({
type: String,
required: true,
},
+ keyId: String,
mode: {
type: String,
enum: CloneModeEnum,
diff --git a/server/src/schema/cloneDetails.js b/server/src/schema/cloneDetails.js
index 5ac7ba13b..22c10d69d 100644
--- a/server/src/schema/cloneDetails.js
+++ b/server/src/schema/cloneDetails.js
@@ -2,8 +2,8 @@ import logger from '../config/logger'
import { isDefined } from '../config/utils'
import { AutorizationError } from '../errors'
-const cloneDetails = async(authId, team, clone) => {
- logger.debug('Retrieving clone details.')
+const cloneDetails = (authId, team, clone) => {
+ logger.debug('cloneDetails -> Entering function.')
if(!(authId === (process.env.AUTH_CLIENT_ID + '@clients') || isAuthorized(team, authId))){
logger.debug('User %s is not authorized to manage team %s.', authId, team.slug )
@@ -22,12 +22,12 @@ const cloneDetails = async(authId, team, clone) => {
clone.teamAvatar = team.profile.avatar
clone.userLoggedIn = team.members[0].member.alias //TODO change by context info if possible
- logger.debug('Retrieving clone details complete.')
+ logger.debug('cloneDetails -> Retrieving clone details complete.')
return clone
};
function getSelectedBot(team, selectedBotId){
- logger.debug('Getting selected bot.')
+ logger.debug('cloneDetails -> Getting selected bot.')
if(isDefined(team)){
for (var i = 0; i < team.fb.length; i++) {
if(team.fb[i].id === selectedBotId){
@@ -38,7 +38,7 @@ function getSelectedBot(team, selectedBotId){
}
function isAuthorized(team, authId){
- logger.debug('Checking if the user is authorized by the teams module.')
+ logger.debug('cloneDetails -> Checking if the user is authorized by the teams module.')
let authorized = false
if(isDefined(team)){
for (var i = 0; i < team.members.length; i++) {
diff --git a/server/src/schema/mutations/AddCloneMutation.js b/server/src/schema/mutations/AddCloneMutation.js
index a0f9db02e..072c4f8df 100644
--- a/server/src/schema/mutations/AddCloneMutation.js
+++ b/server/src/schema/mutations/AddCloneMutation.js
@@ -1,23 +1,26 @@
-import { CloneInputType } from '../types/input';
-import { CloneType } from '../types';
-import { Clone } from '../../models';
-import { CloneModeEnum } from '../../enums/CloneMode';
-import { BotTypesEnum } from '../../enums/BotTypes';
+import { CloneInputType } from '../types/input'
+import { CloneType } from '../types'
+import { Clone } from '../../models'
+import { CloneModeEnum } from '../../enums/CloneMode'
+import { BotTypesEnum } from '../../enums/BotTypes'
import teamQuery from '../../graphQLCalls/teamQuery'
+import authorizeClone from '../../graphQLCalls/authorizeClone'
import cloneDetails from '../cloneDetails'
import {
AuthentificationError,
WrongArgumentsError,
- OperationsError
+ OperationsError,
+ AutorizationError
} from '../../errors'
import logger from '../../config/logger'
import createKubernetesClone from '../../kubernetes/createClone'
import { isDefined } from '../../config/utils'
+import { LIVE } from '../../enums/CloneMode'
const args = {
- clone: {type: CloneInputType }
+ clone: { type: CloneInputType }
}
const resolve = async(parent, { clone }, context) => {
@@ -27,40 +30,44 @@ const resolve = async(parent, { clone }, context) => {
throw new AuthentificationError()
}
- if(!Object.values(CloneModeEnum).includes(clone.mode)){
+ if (!Object.values(CloneModeEnum).includes(clone.mode)) {
throw new WrongArgumentsError('The mode selected is not valid.')
}
- if(!Object.values(BotTypesEnum).includes(clone.botType)){
+ if (!Object.values(BotTypesEnum).includes(clone.botType)) {
throw new WrongArgumentsError('The bot type selected is not valid.')
}
- try{
+ if (clone.mode === LIVE && !isDefined(clone.keyId)) {
+ throw new WrongArgumentsError('The key was not provided to run the clone in Live mode.')
+ }
+
+ try {
let team = await teamQuery(context.authorization, clone.teamId)
clone = await cloneDetails(context.userId, team.data.data.teams_TeamById, clone)
- clone.createDatetime = new Date().valueOf() / 1000|0
+ clone.createDatetime = new Date().valueOf() / 1000 | 0
clone.active = true
- logger.debug('addClone -> Creating a new clone.')
let newClone = new Clone(clone)
newClone.authId = context.userId
clone.id = newClone._id
- await createKubernetesClone(clone)
- return new Promise((resolve, reject) => {
- newClone.save((err) => {
- if (err){
- logger.error('addClone -> Error: %s', err.stack)
- reject('There has been an error storing the clone.')
- }
- else {
- //TODO transaction on database
- logger.debug('addClone -> Save clone sucessful.')
- resolve(newClone)
- }
- })
- })
- } catch(err) {
+ logger.debug('addClone -> Creating the clone on the Database.')
+ let savedClone = await newClone.save()
+
+ if (clone.mode === LIVE) {
+ logger.debug('addClone -> Authorizing clone to use the key.')
+ let response = await authorizeClone(context.authorization, clone.keyId, clone.id, false)
+ logger.debug('addClone -> Authorizing clone to use the key.')
+ if (response.data.data.keyVault_AuthorizeClone === clone.keyId) {
+ await createKubernetesClone(clone)
+ } else {
+ throw new AutorizationError()
+ }
+ }
+ logger.debug('addClone -> Clone created sucessfully.')
+ return savedClone
+ } catch (err) {
logger.error('addClone -> Error: %s', err.stack)
throw new OperationsError('There has been an error creating the clone.')
}
diff --git a/server/src/schema/mutations/RemoveCloneMutation.js b/server/src/schema/mutations/RemoveCloneMutation.js
index 2efa137c0..32f33c3c3 100644
--- a/server/src/schema/mutations/RemoveCloneMutation.js
+++ b/server/src/schema/mutations/RemoveCloneMutation.js
@@ -40,6 +40,9 @@ const resolve = async (parent, { id }, context) => {
let team = await teamQuery(context.authorization, clone.teamId)
clone = await cloneDetails(context.userId, team.data.data.teams_TeamById, clone)
+ logger.debug('removeClone -> Release the clone key.')
+ await authorizeClone(context.authorization, clone.keyId, clone.id, true)
+
logger.debug('removeClone -> Removing Clone from Kubernates.')
await removeKuberneteClone(clone)
diff --git a/server/src/schema/mutations/UpdateExecutionSummary.js b/server/src/schema/mutations/UpdateExecutionSummary.js
index 1c889198d..4e46d5f91 100644
--- a/server/src/schema/mutations/UpdateExecutionSummary.js
+++ b/server/src/schema/mutations/UpdateExecutionSummary.js
@@ -13,6 +13,7 @@ import {
import { Clone } from '../../models'
import logger from '../../config/logger'
import teamQuery from '../../graphQLCalls/teamQuery'
+import authorizeClone from '../../graphQLCalls/authorizeClone'
import cloneDetails from '../cloneDetails'
import removeKuberneteClone from '../../kubernetes/removeClone'
import { isDefined } from '../../config/utils'
@@ -55,9 +56,6 @@ const resolve = async (parent,
let team = await teamQuery(context.authorization, clone.teamId)
clone = await cloneDetails(context.userId, team.data.data.teams_TeamById, clone)
- logger.debug('UpdateExecutionSummary -> Removing Clone from Kubernates.')
- await removeKuberneteClone(clone)
-
const query = { _id: id }
const options = { new: true }
const update = {
diff --git a/server/src/schema/queries/GetClones.js b/server/src/schema/queries/GetClones.js
index 7125edc7b..020b02474 100644
--- a/server/src/schema/queries/GetClones.js
+++ b/server/src/schema/queries/GetClones.js
@@ -18,7 +18,7 @@ const args = {
}
const resolve = async(parent, { cloneIdList }, context) => {
- logger.debug('Get Clones -> Entering Fuction. %j', cloneIdList)
+ logger.debug('Get Clones -> Entering Fuction.')
try{
let clones = []
for (var i = 0; i < cloneIdList.length; i++) {
@@ -44,7 +44,6 @@ const resolve = async(parent, { cloneIdList }, context) => {
id: cloneIdList[i]
})
}
-
}
return clones
diff --git a/server/src/schema/types/CloneType.js b/server/src/schema/types/CloneType.js
index dfcd10a68..e06819625 100644
--- a/server/src/schema/types/CloneType.js
+++ b/server/src/schema/types/CloneType.js
@@ -37,7 +37,8 @@ const CloneType = new GraphQLObjectType({
teamName: {type: GraphQLString},
botName: {type: GraphQLString},
botAvatar: {type: GraphQLString},
- teamAvatar: {type: GraphQLString}
+ teamAvatar: {type: GraphQLString},
+ keyId: {type: GraphQLString}
})
})
export default CloneType
diff --git a/server/src/schema/types/input/Clone.js b/server/src/schema/types/input/Clone.js
index a4a6d95ec..8b40dd2ce 100644
--- a/server/src/schema/types/input/Clone.js
+++ b/server/src/schema/types/input/Clone.js
@@ -23,7 +23,8 @@ const Type = new GraphQLInputObjectType({
month: { type: GraphQLInt },
interval: { type: GraphQLInt },
processName: { type: new GraphQLNonNull(GraphQLString) },
- botType: { type: new GraphQLNonNull(GraphQLString) }
+ botType: { type: new GraphQLNonNull(GraphQLString) },
+ keyId: { type: GraphQLString }
})
});