Updates for API usage after testing against local Dynamo.

This commit is contained in:
mike12345567 2020-10-08 17:34:41 +01:00
parent d2ae589151
commit b400a06027
11 changed files with 127 additions and 33 deletions

View File

@ -0,0 +1,55 @@
// THIS will create API Keys and App Ids input in a local Dynamo instance if it is running
const dynamoClient = require("../src/db/dynamoClient")
if (process.argv[2] == null || process.argv[3] == null) {
console.error(
"Inputs incorrect format, was expecting: node createApiKeyAndAppId.js <API_KEY> <APP_ID>"
)
process.exit(-1)
}
const FAKE_STRING = "fakestring"
// set fake credentials for local dynamo to actually work
process.env.AWS_ACCESS_KEY_ID = "KEY_ID"
process.env.AWS_SECRET_ACCESS_KEY = "SECRET_KEY"
dynamoClient.init("http://localhost:8333")
async function run() {
await dynamoClient.apiKeyTable.put({
item: {
pk: process.argv[2],
accountId: FAKE_STRING,
trackingId: FAKE_STRING,
quotaReset: Date.now() + 2592000000,
usageQuota: {
automationRuns: 0,
records: 0,
storage: 0,
users: 0,
views: 0,
},
usageLimits: {
automationRuns: 10,
records: 10,
storage: 1000,
users: 10,
views: 10,
},
},
})
await dynamoClient.apiKeyTable.put({
item: {
pk: process.argv[3],
apiKey: process.argv[2],
},
})
}
run()
.then(() => {
console.log("Records should have been created.")
})
.catch(err => {
console.error("Cannot create records - " + err)
})

View File

@ -21,9 +21,10 @@ function runWorker(job) {
async function updateQuota(automation) { async function updateQuota(automation) {
const appId = automation.appId const appId = automation.appId
const apiKey = await getAPIKey(appId) const apiObj = await getAPIKey(appId)
// this will fail, causing automation to escape if limits reached // this will fail, causing automation to escape if limits reached
await update(apiKey, Properties.AUTOMATION, 1) await update(apiObj.apiKey, Properties.AUTOMATION, 1)
return apiObj.apiKey
} }
/** /**
@ -33,8 +34,8 @@ module.exports.init = function() {
actions.init().then(() => { actions.init().then(() => {
triggers.automationQueue.process(async job => { triggers.automationQueue.process(async job => {
try { try {
if (environment.CLOUD) { if (environment.CLOUD && job.data.automation) {
await updateQuota(job.data.automation) job.data.automation.apiKey = await updateQuota(job.data.automation)
} }
if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") { if (environment.BUDIBASE_ENVIRONMENT === "PRODUCTION") {
await runWorker(job) await runWorker(job)

View File

@ -1,5 +1,7 @@
const recordController = require("../../api/controllers/record") const recordController = require("../../api/controllers/record")
const automationUtils = require("../automationUtils") const automationUtils = require("../automationUtils")
const environment = require("../../environment")
const usage = require("../../utilities/usageQuota")
module.exports.definition = { module.exports.definition = {
name: "Create Row", name: "Create Row",
@ -56,7 +58,7 @@ module.exports.definition = {
}, },
} }
module.exports.run = async function({ inputs, instanceId }) { module.exports.run = async function({ inputs, instanceId, apiKey }) {
// TODO: better logging of when actions are missed due to missing parameters // TODO: better logging of when actions are missed due to missing parameters
if (inputs.record == null || inputs.record.modelId == null) { if (inputs.record == null || inputs.record.modelId == null) {
return return
@ -78,6 +80,9 @@ module.exports.run = async function({ inputs, instanceId }) {
} }
try { try {
if (environment.CLOUD) {
await usage.update(apiKey, usage.Properties.RECORD, 1)
}
await recordController.save(ctx) await recordController.save(ctx)
return { return {
record: inputs.record, record: inputs.record,

View File

@ -1,5 +1,7 @@
const accessLevels = require("../../utilities/accessLevels") const accessLevels = require("../../utilities/accessLevels")
const userController = require("../../api/controllers/user") const userController = require("../../api/controllers/user")
const environment = require("../../environment")
const usage = require("../../utilities/usageQuota")
module.exports.definition = { module.exports.definition = {
description: "Create a new user", description: "Create a new user",
@ -56,7 +58,7 @@ module.exports.definition = {
}, },
} }
module.exports.run = async function({ inputs, instanceId }) { module.exports.run = async function({ inputs, instanceId, apiKey }) {
const { username, password, accessLevelId } = inputs const { username, password, accessLevelId } = inputs
const ctx = { const ctx = {
user: { user: {
@ -68,6 +70,9 @@ module.exports.run = async function({ inputs, instanceId }) {
} }
try { try {
if (environment.CLOUD) {
await usage.update(apiKey, usage.Properties.USER, 1)
}
await userController.create(ctx) await userController.create(ctx)
return { return {
response: ctx.body, response: ctx.body,

View File

@ -1,4 +1,6 @@
const recordController = require("../../api/controllers/record") const recordController = require("../../api/controllers/record")
const environment = require("../../environment")
const usage = require("../../utilities/usageQuota")
module.exports.definition = { module.exports.definition = {
description: "Delete a row from your database", description: "Delete a row from your database",
@ -48,7 +50,7 @@ module.exports.definition = {
}, },
} }
module.exports.run = async function({ inputs, instanceId }) { module.exports.run = async function({ inputs, instanceId, apiKey }) {
// TODO: better logging of when actions are missed due to missing parameters // TODO: better logging of when actions are missed due to missing parameters
if (inputs.id == null || inputs.revision == null) { if (inputs.id == null || inputs.revision == null) {
return return
@ -63,6 +65,9 @@ module.exports.run = async function({ inputs, instanceId }) {
} }
try { try {
if (environment.CLOUD) {
await usage.update(apiKey, usage.Properties.RECORD, -1)
}
await recordController.destroy(ctx) await recordController.destroy(ctx)
return { return {
response: ctx.body, response: ctx.body,

View File

@ -62,6 +62,7 @@ class Orchestrator {
const outputs = await stepFn({ const outputs = await stepFn({
inputs: step.inputs, inputs: step.inputs,
instanceId: this._instanceId, instanceId: this._instanceId,
apiKey: automation.apiKey,
}) })
if (step.stepId === FILTER_STEP_ID && !outputs.success) { if (step.stepId === FILTER_STEP_ID && !outputs.success) {
break break

View File

@ -1,6 +1,8 @@
let _ = require("lodash") let _ = require("lodash")
let environment = require("../environment") let environment = require("../environment")
const AWS_REGION = environment.AWS_REGION ? environment.AWS_REGION : "eu-west-1"
const TableInfo = { const TableInfo = {
API_KEYS: { API_KEYS: {
name: "beta-api-key-table", name: "beta-api-key-table",
@ -49,6 +51,7 @@ class Table {
condition, condition,
names, names,
values, values,
exists,
otherProps, otherProps,
}) { }) {
let params = { let params = {
@ -66,6 +69,13 @@ class Table {
if (this._sort && sort) { if (this._sort && sort) {
params.Key[this._sort] = sort params.Key[this._sort] = sort
} }
if (exists) {
params.ExpressionAttributeNames["#PRIMARY"] = this._primary
if (params.ConditionExpression) {
params.ConditionExpression += " AND "
}
params.ConditionExpression += "attribute_exists(#PRIMARY)"
}
if (otherProps) { if (otherProps) {
params = _.merge(params, otherProps) params = _.merge(params, otherProps)
} }
@ -90,15 +100,17 @@ class Table {
} }
} }
exports.init = () => { exports.init = endpoint => {
if (!environment.CLOUD) {
return
}
let AWS = require("aws-sdk") let AWS = require("aws-sdk")
AWS.config.update({
region: AWS_REGION,
})
let docClientParams = { let docClientParams = {
correctClockSkew: true, correctClockSkew: true,
} }
if (environment.DYNAMO_ENDPOINT) { if (endpoint) {
docClientParams.endpoint = endpoint
} else if (environment.DYNAMO_ENDPOINT) {
docClientParams.endpoint = environment.DYNAMO_ENDPOINT docClientParams.endpoint = environment.DYNAMO_ENDPOINT
} }
docClient = new AWS.DynamoDB.DocumentClient(docClientParams) docClient = new AWS.DynamoDB.DocumentClient(docClientParams)
@ -106,3 +118,11 @@ exports.init = () => {
exports.apiKeyTable = new Table(TableInfo.API_KEYS) exports.apiKeyTable = new Table(TableInfo.API_KEYS)
exports.userTable = new Table(TableInfo.USERS) exports.userTable = new Table(TableInfo.USERS)
if (environment.CLOUD) {
exports.init(`https://dynamodb.${AWS_REGION}.amazonaws.com`)
} else {
process.env.AWS_ACCESS_KEY_ID = "KEY_ID"
process.env.AWS_SECRET_ACCESS_KEY = "SECRET_KEY"
exports.init("http://localhost:8333")
}

View File

@ -16,8 +16,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
} }
if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) { if (ctx.user.accessLevel._id === BUILDER_LEVEL_ID) {
await next() return next()
return
} }
if (permName === BUILDER) { if (permName === BUILDER) {
@ -28,8 +27,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "") const permissionId = ({ name, itemId }) => name + (itemId ? `-${itemId}` : "")
if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) { if (ctx.user.accessLevel._id === ADMIN_LEVEL_ID) {
await next() return next()
return
} }
const thisPermissionId = permissionId({ const thisPermissionId = permissionId({
@ -42,8 +40,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
ctx.user.accessLevel._id === POWERUSER_LEVEL_ID && ctx.user.accessLevel._id === POWERUSER_LEVEL_ID &&
!adminPermissions.map(permissionId).includes(thisPermissionId) !adminPermissions.map(permissionId).includes(thisPermissionId)
) { ) {
await next() return next()
return
} }
if ( if (
@ -51,8 +48,7 @@ module.exports = (permName, getItemId) => async (ctx, next) => {
.map(permissionId) .map(permissionId)
.includes(thisPermissionId) .includes(thisPermissionId)
) { ) {
await next() return next()
return
} }
ctx.throw(403, "Not Authorized") ctx.throw(403, "Not Authorized")

View File

@ -1,5 +1,6 @@
const CouchDB = require("../db") const CouchDB = require("../db")
const usageQuota = require("../utilities/usageQuota") const usageQuota = require("../utilities/usageQuota")
const environment = require("../environment")
// currently only counting new writes and deletes // currently only counting new writes and deletes
const METHOD_MAP = { const METHOD_MAP = {
@ -29,11 +30,9 @@ module.exports = async (ctx, next) => {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
let usage = METHOD_MAP[ctx.req.method] let usage = METHOD_MAP[ctx.req.method]
const property = getProperty(ctx.req.url) const property = getProperty(ctx.req.url)
console.log(ctx.req.url)
if (usage == null || property == null) { if (usage == null || property == null) {
return next() return next()
} }
console.log(`${usage} to ${property}`)
// post request could be a save of a pre-existing entry // post request could be a save of a pre-existing entry
if (ctx.request.body && ctx.request.body._id) { if (ctx.request.body && ctx.request.body._id) {
try { try {
@ -52,8 +51,12 @@ module.exports = async (ctx, next) => {
: [ctx.request.files.file] : [ctx.request.files.file]
usage = files.map(file => file.size).reduce((total, size) => total + size) usage = files.map(file => file.size).reduce((total, size) => total + size)
} }
if (!environment.CLOUD) {
return next()
}
try { try {
await usageQuota.update(ctx.apiKey, property, usage) await usageQuota.update(ctx.apiKey, property, usage)
return next()
} catch (err) { } catch (err) {
ctx.throw(403, err) ctx.throw(403, err)
} }

View File

@ -8,7 +8,9 @@ module.exports = (ctx, appId, instanceId) => {
instanceId, instanceId,
appId, appId,
} }
if (process.env.BUDIBASE_API_KEY) {
builderUser.apiKey = process.env.BUDIBASE_API_KEY
}
const token = jwt.sign(builderUser, ctx.config.jwtSecret, { const token = jwt.sign(builderUser, ctx.config.jwtSecret, {
expiresIn: "30 days", expiresIn: "30 days",
}) })

View File

@ -4,12 +4,12 @@ const { apiKeyTable } = require("../db/dynamoClient")
function buildUpdateParams(key, property, usage) { function buildUpdateParams(key, property, usage) {
return { return {
primary: key, primary: key,
condition: "#quota.#prop + :usage < #limits.model AND #quotaReset < :now", condition: "#quota.#prop < #limits.#prop AND #quotaReset > :now",
expression: "ADD #quota.#prop :usage", expression: "ADD #quota.#prop :usage",
names: { names: {
"#quota": "usageQuota", "#quota": "usageQuota",
"#prop": property, "#prop": property,
"#limits": "limits", "#limits": "usageLimits",
"#quotaReset": "quotaReset", "#quotaReset": "quotaReset",
}, },
values: { values: {
@ -50,21 +50,22 @@ exports.update = async (apiKey, property, usage) => {
try { try {
await apiKeyTable.update(buildUpdateParams(apiKey, property, usage)) await apiKeyTable.update(buildUpdateParams(apiKey, property, usage))
} catch (err) { } catch (err) {
if (err.code !== "ConditionalCheckFailedException") { if (err.code === "ConditionalCheckFailedException") {
// get the API key so we can check it // get the API key so we can check it
let apiKey = await apiKeyTable.get({ primary: apiKey }) const keyObj = await apiKeyTable.get({ primary: apiKey })
// we have infact breached the reset period // we have infact breached the reset period
if (apiKey && apiKey.quotaReset >= Date.now()) { if (keyObj && keyObj.quotaReset <= Date.now()) {
// update the quota reset period and reset the values for all properties // update the quota reset period and reset the values for all properties
apiKey.quotaReset = Date.now() + QUOTA_RESET keyObj.quotaReset = Date.now() + QUOTA_RESET
for (let prop of Object.keys(apiKey.usageQuota)) { for (let prop of Object.keys(keyObj.usageQuota)) {
if (prop === property) { if (prop === property) {
apiKey.usageQuota[prop] = usage > 0 ? usage : 0 keyObj.usageQuota[prop] = usage > 0 ? usage : 0
} else { } else {
apiKey.usageQuota[prop] = 0 keyObj.usageQuota[prop] = 0
} }
} }
await apiKeyTable.put({ item: apiKey }) await apiKeyTable.put({ item: keyObj })
return
} }
throw "Resource limits have been reached" throw "Resource limits have been reached"
} }