diff --git a/packages/auth/src/constants.js b/packages/auth/src/constants.js index 363274eda5..28b9ced49b 100644 --- a/packages/auth/src/constants.js +++ b/packages/auth/src/constants.js @@ -16,6 +16,7 @@ exports.Headers = { APP_ID: "x-budibase-app-id", TYPE: "x-budibase-type", TENANT_ID: "x-budibase-tenant-id", + TOKEN: "x-budibase-token", } exports.GlobalRoles = { diff --git a/packages/auth/src/middleware/authenticated.js b/packages/auth/src/middleware/authenticated.js index f0fb6e21c5..87bd4d35ce 100644 --- a/packages/auth/src/middleware/authenticated.js +++ b/packages/auth/src/middleware/authenticated.js @@ -1,5 +1,5 @@ const { Cookies, Headers } = require("../constants") -const { getCookie, clearCookie } = require("../utils") +const { getCookie, clearCookie, openJwt } = require("../utils") const { getUser } = require("../cache/user") const { getSession, updateSessionTTL } = require("../security/sessions") const { buildMatcherRegex, matches } = require("./matchers") @@ -35,8 +35,9 @@ module.exports = ( publicEndpoint = true } try { - // check the actual user is authenticated first - const authCookie = getCookie(ctx, Cookies.Auth) + // check the actual user is authenticated first, try header or cookie + const headerToken = ctx.request.headers[Headers.TOKEN] + const authCookie = getCookie(ctx, Cookies.Auth) || openJwt(headerToken) let authenticated = false, user = null, internal = false diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index b8fa7b9588..8c00f2a8b8 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -63,6 +63,17 @@ exports.getAppId = ctx => { return appId } +/** + * opens the contents of the specified encrypted JWT. + * @return {object} the contents of the token. + */ +exports.openJwt = token => { + if (!token) { + return token + } + return jwt.verify(token, options.secretOrKey) +} + /** * Get a cookie from context, and decrypt if necessary. * @param {object} ctx The request which is to be manipulated. @@ -75,7 +86,7 @@ exports.getCookie = (ctx, name) => { return cookie } - return jwt.verify(cookie, options.secretOrKey) + return exports.openJwt(cookie) } /** diff --git a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/RestExtraConfigForm.svelte b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/RestExtraConfigForm.svelte index 4a104a1987..2c8b699849 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/RestExtraConfigForm.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/rest/RestExtraConfigForm.svelte @@ -60,9 +60,9 @@ Variables enabled you to store and reuse values in queries. Static variables - use constant values while dynamic values can be bound to the response headers - or body of a queryVariables enable you to store and re-use values in queries, with the choice + of a static value such as a token using static variables, or a value from a + query response using dynamic variables. Static @@ -78,7 +78,7 @@ Dynamic Dynamic variables are evaluated when a dependant query is executed. The value - is cached for 24 hours and will re-evaluate if the dependendent query fails. + is cached for a period of time and will be refreshed if a query fails. diff --git a/packages/builder/src/helpers/data/utils.js b/packages/builder/src/helpers/data/utils.js index d41745050f..9ef6e7d40c 100644 --- a/packages/builder/src/helpers/data/utils.js +++ b/packages/builder/src/helpers/data/utils.js @@ -121,10 +121,13 @@ export function flipHeaderState(headersActivity) { } // convert dynamic variables list to simple key/val object -export function variablesToObject(datasource) { +export function getDynamicVariables(datasource, queryId) { const variablesList = datasource?.config?.dynamicVariables if (variablesList && variablesList.length > 0) { - return variablesList.reduce( + const filtered = queryId + ? variablesList.filter(variable => variable.queryId === queryId) + : variablesList + return filtered.reduce( (acc, next) => ({ ...acc, [next.name]: next.value }), {} ) @@ -133,10 +136,10 @@ export function variablesToObject(datasource) { } // convert dynamic variables object back to a list, enrich with query id -export function rebuildVariables(queryId, variables) { - let vars = [] +export function rebuildVariables(datasource, queryId, variables) { + let newVariables = [] if (variables) { - vars = Object.entries(variables).map(entry => { + newVariables = Object.entries(variables).map(entry => { return { name: entry[0], value: entry[1], @@ -144,7 +147,15 @@ export function rebuildVariables(queryId, variables) { } }) } - return vars + let existing = datasource?.config?.dynamicVariables || [] + // filter out any by same name + existing = existing.filter( + variable => + !newVariables.find( + newVar => newVar.name.toLowerCase() === variable.name.toLowerCase() + ) + ) + return [...existing, ...newVariables] } export function shouldShowVariables(dynamicVariables, variablesReadOnly) { @@ -173,7 +184,7 @@ export default { keyValueToQueryParameters, queryParametersToKeyValue, schemaToFields, - variablesToObject, + getDynamicVariables, rebuildVariables, shouldShowVariables, buildAuthConfigs, diff --git a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_components/DynamicVariableModal.svelte b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_components/DynamicVariableModal.svelte index 1fdbc98483..61d0a1993c 100644 --- a/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_components/DynamicVariableModal.svelte +++ b/packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_components/DynamicVariableModal.svelte @@ -1,10 +1,11 @@ @@ -438,9 +439,10 @@ {#if showVariablesTab} - {"Create dynamic variables to use body and headers results in other queries"} + + Create dynamic variables based on response body or headers + from other queries. + { extra: Joi.object().optional(), datasourceId: Joi.string().required(), transformer: Joi.string().optional(), - parameters: Joi.object({}).required().unknown(true) + parameters: Joi.object({}).required().unknown(true), + queryId: Joi.string().optional(), })) } diff --git a/packages/server/src/threads/query.js b/packages/server/src/threads/query.js index b86c1a49fd..fb82d2b12f 100644 --- a/packages/server/src/threads/query.js +++ b/packages/server/src/threads/query.js @@ -13,7 +13,14 @@ class QueryRunner { this.fields = input.fields this.parameters = input.parameters this.transformer = input.transformer + this.queryId = input.queryId this.noRecursiveQuery = flags.noRecursiveQuery + this.cachedVariables = [] + // allows the response from a query to be stored throughout this + // execution so that if it needs to be re-used for another variable + // it can be + this.queryResponse = {} + this.hasRerun = false } async execute() { @@ -43,6 +50,19 @@ class QueryRunner { rows = runner.execute() } + // if the request fails we retry once, invalidating the cached value + if ( + info && + info.code >= 400 && + this.cachedVariables.length > 0 && + !this.hasRerun + ) { + this.hasRerun = true + // invalidate the cache value + await threadUtils.invalidateDynamicVariables(this.cachedVariables) + return this.execute() + } + // needs to an array for next step if (!Array.isArray(rows)) { rows = [rows] @@ -86,8 +106,14 @@ class QueryRunner { name = variable.name let value = await threadUtils.checkCacheForDynamicVariable(queryId, name) if (!value) { - value = await this.runAnotherQuery(queryId, parameters) + value = this.queryResponse[queryId] + ? this.queryResponse[queryId] + : await this.runAnotherQuery(queryId, parameters) + // store incase this query is to be called again + this.queryResponse[queryId] = value await threadUtils.storeDynamicVariable(queryId, name, value) + } else { + this.cachedVariables.push({ queryId, name }) } return value } @@ -108,6 +134,10 @@ class QueryRunner { // need to see if this uses any variables const stringFields = JSON.stringify(fields) const foundVars = dynamicVars.filter(variable => { + // don't allow a query to use its own dynamic variable (loop) + if (variable.queryId === this.queryId) { + return false + } // look for {{ variable }} but allow spaces between handlebars const regex = new RegExp(`{{[ ]*${variable.name}[ ]*}}`) return regex.test(stringFields) @@ -120,6 +150,8 @@ class QueryRunner { data: responses[i].rows, info: responses[i].extra, }) + // make sure its known that this uses dynamic variables in case it fails + this.hasDynamicVariables = true } } return parameters diff --git a/packages/server/src/threads/utils.js b/packages/server/src/threads/utils.js index 34ae4f0477..fee1e19b67 100644 --- a/packages/server/src/threads/utils.js +++ b/packages/server/src/threads/utils.js @@ -38,6 +38,16 @@ exports.checkCacheForDynamicVariable = async (queryId, variable) => { return cache.get(makeVariableKey(queryId, variable)) } +exports.invalidateDynamicVariables = async cachedVars => { + let promises = [] + for (let variable of cachedVars) { + promises.push( + client.delete(makeVariableKey(variable.queryId, variable.name)) + ) + } + await Promise.all(promises) +} + exports.storeDynamicVariable = async (queryId, variable, value) => { const cache = await getClient() await cache.store( diff --git a/packages/worker/src/api/controllers/global/auth.js b/packages/worker/src/api/controllers/global/auth.js index cd7d8abcee..93a15a6514 100644 --- a/packages/worker/src/api/controllers/global/auth.js +++ b/packages/worker/src/api/controllers/global/auth.js @@ -12,7 +12,7 @@ const { hash, platformLogout, } = authPkg.utils -const { Cookies } = authPkg.constants +const { Cookies, Headers } = authPkg.constants const { passport } = authPkg.auth const { checkResetPasswordCode } = require("../../../utilities/redis") const { @@ -60,7 +60,10 @@ async function authInternal(ctx, user, err = null, info = null) { return ctx.throw(403, info ? info : "Unauthorized") } + // set a cookie for browser access setCookie(ctx, user.token, Cookies.Auth, { sign: false }) + // set the token in a header as well for APIs + ctx.set(Headers.TOKEN, user.token) // get rid of any app cookies on login // have to check test because this breaks cypress if (!env.isTest()) {