diff --git a/.github/workflows/deploy-preprod.yml b/.github/workflows/deploy-preprod.yml index 803dd6af52..a1eee2c465 100644 --- a/.github/workflows/deploy-preprod.yml +++ b/.github/workflows/deploy-preprod.yml @@ -1,6 +1,10 @@ name: "deploy-preprod" on: workflow_dispatch: + inputs: + version: + description: Budibase release version. For example - 1.0.0 + required: false workflow_call: jobs: @@ -8,10 +12,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: 'Get Previous tag' - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" + - name: Get the latest budibase release version + id: version + run: | + if [ -z "${{ github.event.inputs.version }}" ]; then + release_version=$(cat lerna.json | jq -r '.version') + else + release_version=${{ github.event.inputs.version }} + fi + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: @@ -26,7 +36,6 @@ jobs: -o values.preprod.yaml \ -L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/budibase-preprod/values.yaml wc -l values.preprod.yaml - - name: Deploy to Preprod Environment uses: budibase/helm@v1.8.0 with: @@ -37,7 +46,7 @@ jobs: helm: helm3 values: | globals: - appVersion: ${{ steps.previoustag.outputs.tag }} + appVersion: v${{ env.RELEASE_VERSION }} ingress: enabled: true nginx: true @@ -52,5 +61,5 @@ jobs: uses: tsickert/discord-webhook@v4.0.0 with: webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }} - content: "Preprod Deployment Complete: ${{ steps.previoustag.outputs.tag }} deployed to Budibase Pre-prod." - embed-title: ${{ steps.previoustag.outputs.tag }} \ No newline at end of file + content: "Preprod Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Pre-prod." + embed-title: ${{ env.RELEASE_VERSION }} diff --git a/.github/workflows/release-master.yml b/.github/workflows/release-master.yml index 3ae265fa21..20a48f5802 100644 --- a/.github/workflows/release-master.yml +++ b/.github/workflows/release-master.yml @@ -91,9 +91,11 @@ jobs: uses: azure/setup-helm@v1 id: helm-install - - name: 'Get Previous tag' - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" + - name: Get the latest budibase release version + id: version + run: | + release_version=$(cat lerna.json | jq -r '.version') + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV # due to helm repo index issue: https://github.com/helm/helm/issues/7363 # we need to create new package in a different dir, merge the index and move the package back @@ -116,8 +118,6 @@ jobs: git add -A git commit -m "Helm Release: ${{ env.RELEASE_VERSION }}" git push - env: - RELEASE_VERSION: ${{ steps.previoustag.outputs.tag }} deploy-to-legacy-preprod-env: needs: [release-images] @@ -130,13 +130,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: 'Get Previous tag' - id: previoustag - uses: "WyriHaximus/github-action-get-previous-tag@v1" + + - name: Get the latest budibase release version + id: version + run: | + release_version=$(cat lerna.json | jq -r '.version') + echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV - uses: passeidireto/trigger-external-workflow-action@main env: - PAYLOAD_VERSION: ${{ steps.previoustag.outputs.tag }} + PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }} with: repository: budibase/budibase-deploys event: budicloud-preprod-deploy diff --git a/charts/budibase/templates/app-service-deployment.yaml b/charts/budibase/templates/app-service-deployment.yaml index 6b0a0338d6..41b0dc48c9 100644 --- a/charts/budibase/templates/app-service-deployment.yaml +++ b/charts/budibase/templates/app-service-deployment.yaml @@ -62,16 +62,22 @@ spec: {{ end }} - name: ENABLE_ANALYTICS value: {{ .Values.globals.enableAnalytics | quote }} + - name: API_ENCRYPTION_KEY + value: {{ .Values.globals.apiEncryptionKey | quote }} - name: INTERNAL_API_KEY valueFrom: secretKeyRef: name: {{ template "budibase.fullname" . }} key: internalApiKey + - name: INTERNAL_API_KEY_FALLBACK + value: {{ .Values.globals.internalApiKeyFallback | quote }} - name: JWT_SECRET valueFrom: secretKeyRef: name: {{ template "budibase.fullname" . }} key: jwtSecret + - name: JWT_SECRET_FALLBACK + value: {{ .Values.globals.jwtSecretFallback | quote }} {{ if .Values.services.objectStore.region }} - name: AWS_REGION value: {{ .Values.services.objectStore.region }} @@ -125,9 +131,9 @@ spec: - name: SELF_HOSTED value: {{ .Values.globals.selfHosted | quote }} - name: SENTRY_DSN - value: {{ .Values.globals.sentryDSN }} + value: {{ .Values.globals.sentryDSN | quote }} - name: POSTHOG_TOKEN - value: {{ .Values.globals.posthogToken }} + value: {{ .Values.globals.posthogToken | quote }} - name: WORKER_URL value: http://worker-service:{{ .Values.services.worker.port }} - name: PLATFORM_URL @@ -198,8 +204,6 @@ spec: - name: GLOBAL_AGENT_NO_PROXY value: {{ .Values.globals.globalAgentNoProxy | quote }} {{ end }} - - name: CDN_URL - value: {{ .Values.globals.cdnUrl }} {{ if .Values.services.tlsRejectUnauthorized }} - name: NODE_TLS_REJECT_UNAUTHORIZED value: {{ .Values.services.tlsRejectUnauthorized }} diff --git a/charts/budibase/templates/worker-service-deployment.yaml b/charts/budibase/templates/worker-service-deployment.yaml index f4305fbb00..7886d55b28 100644 --- a/charts/budibase/templates/worker-service-deployment.yaml +++ b/charts/budibase/templates/worker-service-deployment.yaml @@ -62,16 +62,22 @@ spec: {{ else }} value: http://{{ .Release.Name }}-svc-couchdb:{{ .Values.services.couchdb.port }} {{ end }} + - name: API_ENCRYPTION_KEY + value: {{ .Values.globals.apiEncryptionKey | quote }} - name: INTERNAL_API_KEY valueFrom: secretKeyRef: name: {{ template "budibase.fullname" . }} key: internalApiKey + - name: INTERNAL_API_KEY_FALLBACK + value: {{ .Values.globals.internalApiKeyFallback | quote }} - name: JWT_SECRET valueFrom: secretKeyRef: name: {{ template "budibase.fullname" . }} key: jwtSecret + - name: JWT_SECRET_FALLBACK + value: {{ .Values.globals.jwtSecretFallback | quote }} {{ if .Values.services.objectStore.region }} - name: AWS_REGION value: {{ .Values.services.objectStore.region }} @@ -188,8 +194,6 @@ spec: - name: GLOBAL_AGENT_NO_PROXY value: {{ .Values.globals.globalAgentNoProxy | quote }} {{ end }} - - name: CDN_URL - value: {{ .Values.globals.cdnUrl }} {{ if .Values.services.tlsRejectUnauthorized }} - name: NODE_TLS_REJECT_UNAUTHORIZED value: {{ .Values.services.tlsRejectUnauthorized }} diff --git a/charts/budibase/values.yaml b/charts/budibase/values.yaml index 536af8560f..ed4ff014a9 100644 --- a/charts/budibase/values.yaml +++ b/charts/budibase/values.yaml @@ -96,9 +96,13 @@ globals: createSecrets: true # creates an internal API key, JWT secrets and redis password for you # if createSecrets is set to false, you can hard-code your secrets here + apiEncryptionKey: "" internalApiKey: "" jwtSecret: "" cdnUrl: "" + # fallback values used during live rotation + internalApiKeyFallback: "" + jwtSecretFallback: "" smtp: enabled: false diff --git a/hosting/.env b/hosting/.env index 07b506a6b2..c2b6d55eef 100644 --- a/hosting/.env +++ b/hosting/.env @@ -3,6 +3,7 @@ MAIN_PORT=10000 # This section contains all secrets pertaining to the system # These should be updated +API_ENCRYPTION_KEY=testsecret JWT_SECRET=testsecret MINIO_ACCESS_KEY=budibase MINIO_SECRET_KEY=budibase diff --git a/hosting/docker-compose.yaml b/hosting/docker-compose.yaml index d36937910f..bad34a20ea 100644 --- a/hosting/docker-compose.yaml +++ b/hosting/docker-compose.yaml @@ -17,6 +17,7 @@ services: INTERNAL_API_KEY: ${INTERNAL_API_KEY} BUDIBASE_ENVIRONMENT: ${BUDIBASE_ENVIRONMENT} PORT: 4002 + API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY} JWT_SECRET: ${JWT_SECRET} LOG_LEVEL: info SENTRY_DSN: https://a34ae347621946bf8acded18e5b7d4b8@o420233.ingest.sentry.io/5338131 @@ -40,6 +41,7 @@ services: SELF_HOSTED: 1 PORT: 4003 CLUSTER_PORT: ${MAIN_PORT} + API_ENCRYPTION_KEY: ${API_ENCRYPTION_KEY} JWT_SECRET: ${JWT_SECRET} MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY} MINIO_SECRET_KEY: ${MINIO_SECRET_KEY} diff --git a/hosting/hosting.properties b/hosting/hosting.properties index c5638a266f..6c1d9e5dbd 100644 --- a/hosting/hosting.properties +++ b/hosting/hosting.properties @@ -3,6 +3,7 @@ MAIN_PORT=10000 # This section contains all secrets pertaining to the system # These should be updated +API_ENCRYPTION_KEY=testsecret JWT_SECRET=testsecret MINIO_ACCESS_KEY=budibase MINIO_SECRET_KEY=budibase diff --git a/lerna.json b/lerna.json index 6f26813981..b47cca4c23 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.4.12-alpha.5", + "version": "2.4.26", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 72225bca7c..c27893051d 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.4.12-alpha.5", + "version": "2.4.26", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -24,7 +24,7 @@ "dependencies": { "@budibase/nano": "10.1.2", "@budibase/pouchdb-replication-stream": "1.2.10", - "@budibase/types": "2.4.12-alpha.5", + "@budibase/types": "^2.4.26", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-cloudfront-sign": "2.2.0", diff --git a/packages/backend-core/src/auth/auth.ts b/packages/backend-core/src/auth/auth.ts index 7e6fe4bcee..26c7cd4e26 100644 --- a/packages/backend-core/src/auth/auth.ts +++ b/packages/backend-core/src/auth/auth.ts @@ -1,6 +1,5 @@ const _passport = require("koa-passport") const LocalStrategy = require("passport-local").Strategy -const JwtStrategy = require("passport-jwt").Strategy import { getGlobalDB } from "../context" import { Cookie } from "../constants" import { getSessionsForUser, invalidateSessions } from "../security/sessions" @@ -8,7 +7,6 @@ import { authenticated, csrf, google, - jwt as jwtPassport, local, oidc, tenancy, @@ -21,14 +19,11 @@ import { OIDCInnerConfig, PlatformLogoutOpts, SSOProviderType, - User, } from "@budibase/types" -import { logAlert } from "../logging" import * as events from "../events" import * as configs from "../configs" import { clearCookie, getCookie } from "../utils" import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" -import env from "../environment" const refresh = require("passport-oauth2-refresh") export { @@ -51,25 +46,6 @@ export const jwt = require("jsonwebtoken") // Strategies _passport.use(new LocalStrategy(local.options, local.authenticate)) -if (jwtPassport.options.secretOrKey) { - _passport.use(new JwtStrategy(jwtPassport.options, jwtPassport.authenticate)) -} else if (!env.DISABLE_JWT_WARNING) { - logAlert("No JWT Secret supplied, cannot configure JWT strategy") -} - -_passport.serializeUser((user: User, done: any) => done(null, user)) - -_passport.deserializeUser(async (user: User, done: any) => { - const db = getGlobalDB() - - try { - const dbUser = await db.get(user._id) - return done(null, dbUser) - } catch (err) { - console.error(`User not found`, err) - return done(null, false, { message: "User not found" }) - } -}) async function refreshOIDCAccessToken( chosenConfig: OIDCInnerConfig, diff --git a/packages/backend-core/src/environment.ts b/packages/backend-core/src/environment.ts index 8dc2cce487..f1c96c7fec 100644 --- a/packages/backend-core/src/environment.ts +++ b/packages/backend-core/src/environment.ts @@ -30,6 +30,12 @@ const DefaultBucketName = { const selfHosted = !!parseInt(process.env.SELF_HOSTED || "") +function getAPIEncryptionKey() { + return process.env.API_ENCRYPTION_KEY + ? process.env.API_ENCRYPTION_KEY + : process.env.JWT_SECRET // fallback to the JWT_SECRET used historically +} + const environment = { isTest, isJest, @@ -39,7 +45,9 @@ const environment = { }, JS_BCRYPT: process.env.JS_BCRYPT, JWT_SECRET: process.env.JWT_SECRET, + JWT_SECRET_FALLBACK: process.env.JWT_SECRET_FALLBACK, ENCRYPTION_KEY: process.env.ENCRYPTION_KEY, + API_ENCRYPTION_KEY: getAPIEncryptionKey(), COUCH_DB_URL: process.env.COUCH_DB_URL || "http://localhost:4005", COUCH_DB_USERNAME: process.env.COUCH_DB_USER, COUCH_DB_PASSWORD: process.env.COUCH_DB_PASSWORD, @@ -55,6 +63,7 @@ const environment = { MINIO_URL: process.env.MINIO_URL, MINIO_ENABLED: process.env.MINIO_ENABLED || 1, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, + INTERNAL_API_KEY_FALLBACK: process.env.INTERNAL_API_KEY_FALLBACK, MULTI_TENANCY: process.env.MULTI_TENANCY, ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL || "https://account.budibase.app", diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index 0708581570..5e546b4c1c 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -1,5 +1,10 @@ import { Cookie, Header } from "../constants" -import { getCookie, clearCookie, openJwt } from "../utils" +import { + getCookie, + clearCookie, + openJwt, + isValidInternalAPIKey, +} from "../utils" import { getUser } from "../cache/user" import { getSession, updateSessionTTL } from "../security/sessions" import { buildMatcherRegex, matches } from "./matchers" @@ -35,7 +40,9 @@ function finalise(ctx: any, opts: FinaliseOpts = {}) { } async function checkApiKey(apiKey: string, populateUser?: Function) { - if (apiKey === env.INTERNAL_API_KEY) { + // check both the primary and the fallback internal api keys + // this allows for rotation + if (isValidInternalAPIKey(apiKey)) { return { valid: true } } const decrypted = decrypt(apiKey) diff --git a/packages/backend-core/src/middleware/index.ts b/packages/backend-core/src/middleware/index.ts index addeac6a1a..dce07168d4 100644 --- a/packages/backend-core/src/middleware/index.ts +++ b/packages/backend-core/src/middleware/index.ts @@ -1,4 +1,3 @@ -export * as jwt from "./passport/jwt" export * as local from "./passport/local" export * as google from "./passport/sso/google" export * as oidc from "./passport/sso/oidc" diff --git a/packages/backend-core/src/middleware/internalApi.ts b/packages/backend-core/src/middleware/internalApi.ts index fff761928b..dc73cd6b66 100644 --- a/packages/backend-core/src/middleware/internalApi.ts +++ b/packages/backend-core/src/middleware/internalApi.ts @@ -1,13 +1,21 @@ -import env from "../environment" import { Header } from "../constants" import { BBContext } from "@budibase/types" +import { isValidInternalAPIKey } from "../utils" /** * API Key only endpoint. */ export default async (ctx: BBContext, next: any) => { const apiKey = ctx.request.headers[Header.API_KEY] - if (apiKey !== env.INTERNAL_API_KEY) { + if (!apiKey) { + ctx.throw(403, "Unauthorized") + } + + if (Array.isArray(apiKey)) { + ctx.throw(403, "Unauthorized") + } + + if (!isValidInternalAPIKey(apiKey)) { ctx.throw(403, "Unauthorized") } diff --git a/packages/backend-core/src/middleware/passport/jwt.ts b/packages/backend-core/src/middleware/passport/jwt.ts deleted file mode 100644 index 95dc8f2656..0000000000 --- a/packages/backend-core/src/middleware/passport/jwt.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Cookie } from "../../constants" -import env from "../../environment" -import { authError } from "./utils" -import { BBContext } from "@budibase/types" - -export const options = { - secretOrKey: env.JWT_SECRET, - jwtFromRequest: function (ctx: BBContext) { - return ctx.cookies.get(Cookie.Auth) - }, -} - -export async function authenticate(jwt: Function, done: Function) { - try { - return done(null, jwt) - } catch (err) { - return authError(done, "JWT invalid", err) - } -} diff --git a/packages/backend-core/src/security/encryption.ts b/packages/backend-core/src/security/encryption.ts index d0707cb850..d2c8f18f73 100644 --- a/packages/backend-core/src/security/encryption.ts +++ b/packages/backend-core/src/security/encryption.ts @@ -8,7 +8,7 @@ const RANDOM_BYTES = 16 const STRETCH_LENGTH = 32 export enum SecretOption { - JWT = "jwt", + API = "api", ENCRYPTION = "encryption", } @@ -19,10 +19,10 @@ function getSecret(secretOption: SecretOption): string { secret = env.ENCRYPTION_KEY secretName = "ENCRYPTION_KEY" break - case SecretOption.JWT: + case SecretOption.API: default: - secret = env.JWT_SECRET - secretName = "JWT_SECRET" + secret = env.API_ENCRYPTION_KEY + secretName = "API_ENCRYPTION_KEY" break } if (!secret) { @@ -37,7 +37,7 @@ function stretchString(string: string, salt: Buffer) { export function encrypt( input: string, - secretOption: SecretOption = SecretOption.JWT + secretOption: SecretOption = SecretOption.API ) { const salt = crypto.randomBytes(RANDOM_BYTES) const stretched = stretchString(getSecret(secretOption), salt) @@ -50,7 +50,7 @@ export function encrypt( export function decrypt( input: string, - secretOption: SecretOption = SecretOption.JWT + secretOption: SecretOption = SecretOption.API ) { const [salt, encrypted] = input.split(SEPARATOR) const saltBuffer = Buffer.from(salt, "hex") diff --git a/packages/backend-core/src/utils/utils.ts b/packages/backend-core/src/utils/utils.ts index 3efd40ca80..7c222a9831 100644 --- a/packages/backend-core/src/utils/utils.ts +++ b/packages/backend-core/src/utils/utils.ts @@ -1,5 +1,4 @@ import { getAllApps, queryGlobalView } from "../db" -import { options } from "../middleware/passport/jwt" import { Header, MAX_VALID_DATE, @@ -133,7 +132,30 @@ export function openJwt(token: string) { if (!token) { return token } - return jwt.verify(token, options.secretOrKey) + try { + return jwt.verify(token, env.JWT_SECRET) + } catch (e) { + if (env.JWT_SECRET_FALLBACK) { + // fallback to enable rotation + return jwt.verify(token, env.JWT_SECRET_FALLBACK) + } else { + throw e + } + } +} + +export function isValidInternalAPIKey(apiKey: string) { + if (env.INTERNAL_API_KEY && env.INTERNAL_API_KEY === apiKey) { + return true + } + // fallback to enable rotation + if ( + env.INTERNAL_API_KEY_FALLBACK && + env.INTERNAL_API_KEY_FALLBACK === apiKey + ) { + return true + } + return false } /** @@ -165,7 +187,7 @@ export function setCookie( opts = { sign: true } ) { if (value && opts && opts.sign) { - value = jwt.sign(value, options.secretOrKey) + value = jwt.sign(value, env.JWT_SECRET) } const config: SetOption = { diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 111a0570a2..6f37501ca6 100644 --- a/packages/bbui/package.json +++ b/packages/bbui/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/bbui", "description": "A UI solution used in the different Budibase projects.", - "version": "2.4.12-alpha.5", + "version": "2.4.26", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,8 +38,8 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "1.2.1", - "@budibase/shared-core": "2.4.12-alpha.5", - "@budibase/string-templates": "2.4.12-alpha.5", + "@budibase/shared-core": "^2.4.26", + "@budibase/string-templates": "^2.4.26", "@spectrum-css/accordion": "3.0.24", "@spectrum-css/actionbutton": "1.0.1", "@spectrum-css/actiongroup": "1.0.1", diff --git a/packages/builder/package.json b/packages/builder/package.json index 9a76a22f0f..1b910f2def 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.4.12-alpha.5", + "version": "2.4.26", "license": "GPL-3.0", "private": true, "scripts": { @@ -58,11 +58,11 @@ } }, "dependencies": { - "@budibase/bbui": "2.4.12-alpha.5", - "@budibase/client": "2.4.12-alpha.5", - "@budibase/frontend-core": "2.4.12-alpha.5", - "@budibase/shared-core": "2.4.12-alpha.5", - "@budibase/string-templates": "2.4.12-alpha.5", + "@budibase/bbui": "^2.4.26", + "@budibase/client": "^2.4.26", + "@budibase/frontend-core": "^2.4.26", + "@budibase/shared-core": "^2.4.26", + "@budibase/string-templates": "^2.4.26", "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte index 53d50d57a3..3bc1a1cdd9 100644 --- a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte +++ b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte @@ -74,6 +74,14 @@ } return capitalise(name) } + + function getDisplayError(error, configKey) { + return error?.replace( + new RegExp(`${configKey}`, "i"), + getDisplayName(configKey) + ) + } + function getFieldGroupKeys(fieldGroup) { return Object.entries(schema[fieldGroup].fields || {}) .filter(el => filter(el)) @@ -147,7 +155,7 @@ type={schema[configKey].type} on:change bind:value={config[configKey]} - error={$validation.errors[configKey]} + error={getDisplayError($validation.errors[configKey], configKey)} /> {:else if schema[configKey].type === "fieldGroup"} @@ -180,7 +188,7 @@ type={configKey === "port" ? "string" : schema[configKey].type} on:change bind:value={config[configKey]} - error={$validation.errors[configKey]} + error={getDisplayError($validation.errors[configKey], configKey)} environmentVariablesEnabled={$licensing.environmentVariablesEnabled} {handleUpgradePanel} /> diff --git a/packages/cli/package.json b/packages/cli/package.json index b6178b93d8..146e76f19e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.4.12-alpha.5", + "version": "2.4.26", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "dist/index.js", "bin": { @@ -29,9 +29,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.4.12-alpha.5", - "@budibase/string-templates": "2.4.12-alpha.5", - "@budibase/types": "2.4.12-alpha.5", + "@budibase/backend-core": "^2.4.26", + "@budibase/string-templates": "^2.4.26", + "@budibase/types": "^2.4.26", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/cli/src/hosting/makeFiles.ts b/packages/cli/src/hosting/makeFiles.ts index 9574e107a6..b11a07c107 100644 --- a/packages/cli/src/hosting/makeFiles.ts +++ b/packages/cli/src/hosting/makeFiles.ts @@ -13,6 +13,7 @@ export const ENV_PATH = path.resolve("./.env") function getSecrets(opts = { single: false }) { const secrets = [ + "API_ENCRYPTION_KEY", "JWT_SECRET", "MINIO_ACCESS_KEY", "MINIO_SECRET_KEY", diff --git a/packages/cli/src/hosting/update.ts b/packages/cli/src/hosting/update.ts index 161cc04ae1..ca0ecce615 100644 --- a/packages/cli/src/hosting/update.ts +++ b/packages/cli/src/hosting/update.ts @@ -4,6 +4,8 @@ import { downloadDockerCompose, handleError, getServices, + getServiceImage, + setServiceImage, } from "./utils" import { confirmation } from "../questions" import compose from "docker-compose" @@ -23,7 +25,11 @@ export async function update() { !isSingle && (await confirmation("Do you wish to update you docker-compose.yaml?")) ) { + // get current MinIO image + const image = await getServiceImage("minio") await downloadDockerCompose() + // replace MinIO image + setServiceImage("minio", image) } await handleError(async () => { const status = await compose.ps() diff --git a/packages/cli/src/hosting/utils.ts b/packages/cli/src/hosting/utils.ts index 9e5bd367ed..93e31b8aea 100644 --- a/packages/cli/src/hosting/utils.ts +++ b/packages/cli/src/hosting/utils.ts @@ -9,10 +9,44 @@ const ERROR_FILE = "docker-error.log" const COMPOSE_URL = "https://raw.githubusercontent.com/Budibase/budibase/master/hosting/docker-compose.yaml" -export async function downloadDockerCompose() { - const fileName = COMPOSE_URL.split("/").slice(-1)[0] +function composeFilename() { + return COMPOSE_URL.split("/").slice(-1)[0] +} + +export function getServiceImage(service: string) { + const filename = composeFilename() try { - await downloadFile(COMPOSE_URL, `./${fileName}`) + const { services } = getServices(filename) + const serviceKey = Object.keys(services).find(name => + name.includes(service) + ) + if (serviceKey) { + return services[serviceKey].image + } else { + return null + } + } catch (err) { + return null + } +} + +export function setServiceImage(service: string, image: string) { + const filename = composeFilename() + if (!fs.existsSync(filename)) { + throw new Error( + `File ${filename} not found, cannot update ${service} image.` + ) + } + const current = getServiceImage(service)! + let contents = fs.readFileSync(filename, "utf8") + contents = contents.replace(`image: ${current}`, `image: ${image}`) + fs.writeFileSync(filename, contents) +} + +export async function downloadDockerCompose() { + const filename = composeFilename() + try { + await downloadFile(COMPOSE_URL, `./${filename}`) } catch (err) { console.error(error(`Failed to retrieve compose file - ${err}`)) } @@ -49,6 +83,9 @@ export async function handleError(func: Function) { } export function getServices(path: string) { + if (!fs.existsSync(path)) { + throw new Error(`No yaml found at path: ${path}`) + } const dockerYaml = fs.readFileSync(path, "utf8") const parsedYaml = yaml.parse(dockerYaml) return { yaml: parsedYaml, services: parsedYaml.services } diff --git a/packages/client/manifest.json b/packages/client/manifest.json index e24fa3a68a..2579cdedaa 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -17,10 +17,7 @@ "description": "This component is specific only to layouts", "icon": "Sandbox", "hasChildren": true, - "styles": [ - "padding", - "background" - ], + "styles": ["padding", "background"], "settings": [ { "type": "text", @@ -36,23 +33,14 @@ "type": "select", "label": "Navigation", "key": "navigation", - "options": [ - "Top", - "Left", - "None" - ], + "options": ["Top", "Left", "None"], "defaultValue": "Top" }, { "type": "select", "label": "Width", "key": "width", - "options": [ - "Small", - "Medium", - "Large", - "Max" - ], + "options": ["Small", "Medium", "Large", "Max"], "defaultValue": "Large" }, { @@ -89,13 +77,7 @@ "width": 400, "height": 200 }, - "styles": [ - "padding", - "size", - "background", - "border", - "shadow" - ], + "styles": ["padding", "size", "background", "border", "shadow"], "settings": [ { "type": "select", @@ -255,9 +237,7 @@ "description": "Add a section to your application", "icon": "ColumnTwoB", "hasChildren": true, - "illegalChildren": [ - "section" - ], + "illegalChildren": ["section"], "showEmptyState": false, "size": { "width": 400, @@ -376,9 +356,7 @@ "name": "Divider", "description": "A basic divider", "icon": "Separator", - "illegalChildren": [ - "section" - ], + "illegalChildren": ["section"], "size": { "width": 400, "height": 10 @@ -415,9 +393,7 @@ "name": "Repeater", "description": "A configurable data list that attaches to your backend tables.", "icon": "JourneyData", - "illegalChildren": [ - "section" - ], + "illegalChildren": ["section"], "hasChildren": true, "size": { "width": 400, @@ -574,9 +550,7 @@ "name": "Stacked List", "icon": "TaskList", "description": "A basic card component that can contain content and actions.", - "illegalChildren": [ - "section" - ], + "illegalChildren": ["section"], "settings": [ { "type": "text", @@ -606,9 +580,7 @@ "name": "Vertical Card", "description": "A basic card component that can contain content and actions.", "icon": "ViewColumn", - "illegalChildren": [ - "section" - ], + "illegalChildren": ["section"], "settings": [ { "type": "text", @@ -652,24 +624,14 @@ "type": "select", "label": "Image Height", "key": "imageHeight", - "options": [ - "auto", - "12rem", - "16rem", - "20rem", - "24rem" - ], + "options": ["auto", "12rem", "16rem", "20rem", "24rem"], "defaultValue": "auto" }, { "type": "select", "label": "Card Width", "key": "cardWidth", - "options": [ - "16rem", - "20rem", - "24rem" - ], + "options": ["16rem", "20rem", "24rem"], "defaultValue": "20rem" } ] @@ -678,9 +640,7 @@ "name": "Paragraph", "description": "A component for displaying paragraph text.", "icon": "TextParagraph", - "illegalChildren": [ - "section" - ], + "illegalChildren": ["section"], "editable": true, "size": { "width": 400, @@ -803,9 +763,7 @@ "name": "Headline", "icon": "TextBold", "description": "A component for displaying heading text", - "illegalChildren": [ - "section" - ], + "illegalChildren": ["section"], "editable": true, "size": { "width": 400, @@ -982,9 +940,7 @@ "name": "Image", "description": "A basic component for displaying images", "icon": "Image", - "styles": [ - "size" - ], + "styles": ["size"], "size": { "width": 400, "height": 300 @@ -1002,9 +958,8 @@ "name": "Background Image", "description": "A background image", "icon": "Images", - "styles": [ - "size" - ], + "hasChildren": true, + "styles": ["size"], "size": { "width": 400, "height": 300 @@ -1162,9 +1117,7 @@ "name": "Nav Bar", "description": "A component for handling the navigation within your app.", "icon": "BreadcrumbNavigation", - "illegalChildren": [ - "section" - ], + "illegalChildren": ["section"], "hasChildren": true, "settings": [ { @@ -1365,25 +1318,14 @@ "type": "select", "label": "Image Width", "key": "imageWidth", - "options": [ - "auto", - "8rem", - "12rem", - "16rem" - ], + "options": ["auto", "8rem", "12rem", "16rem"], "defaultValue": "8rem" }, { "type": "select", "label": "Image Height", "key": "imageHeight", - "options": [ - "auto", - "8rem", - "12rem", - "16rem", - "auto" - ], + "options": ["auto", "8rem", "12rem", "16rem", "auto"], "defaultValue": "auto" } ] @@ -1424,9 +1366,7 @@ "name": "Embed", "icon": "Code", "description": "Embed content from 3rd party sources", - "styles": [ - "size" - ], + "styles": ["size"], "size": { "width": 400, "height": 100 @@ -1478,11 +1418,7 @@ "type": "select", "label": "Format", "key": "yAxisUnits", - "options": [ - "Default", - "Thousands", - "Millions" - ], + "options": ["Default", "Thousands", "Millions"], "defaultValue": "Default" }, { @@ -1640,11 +1576,7 @@ "type": "select", "label": "Format", "key": "yAxisUnits", - "options": [ - "Default", - "Thousands", - "Millions" - ], + "options": ["Default", "Thousands", "Millions"], "defaultValue": "Default" }, { @@ -1736,11 +1668,7 @@ "type": "select", "label": "Curve", "key": "curve", - "options": [ - "Smooth", - "Straight", - "Stepline" - ], + "options": ["Smooth", "Straight", "Stepline"], "defaultValue": "Smooth" }, { @@ -1801,11 +1729,7 @@ "type": "select", "label": "Format", "key": "yAxisUnits", - "options": [ - "Default", - "Thousands", - "Millions" - ], + "options": ["Default", "Thousands", "Millions"], "defaultValue": "Default" }, { @@ -1897,11 +1821,7 @@ "type": "select", "label": "Curve", "key": "curve", - "options": [ - "Smooth", - "Straight", - "Stepline" - ], + "options": ["Smooth", "Straight", "Stepline"], "defaultValue": "Smooth" }, { @@ -2253,11 +2173,7 @@ "type": "select", "label": "Format", "key": "yAxisUnits", - "options": [ - "Default", - "Thousands", - "Millions" - ], + "options": ["Default", "Thousands", "Millions"], "defaultValue": "Default" }, { @@ -2293,19 +2209,14 @@ "name": "Form", "icon": "Form", "hasChildren": true, - "illegalChildren": [ - "section", - "form" - ], + "illegalChildren": ["section", "form"], "actions": [ "ValidateForm", "ClearForm", "ChangeFormStep", "UpdateFieldValue" ], - "styles": [ - "size" - ], + "styles": ["size"], "size": { "width": 400, "height": 400 @@ -2315,10 +2226,7 @@ "type": "select", "label": "Type", "key": "actionType", - "options": [ - "Create", - "Update" - ], + "options": ["Create", "Update"], "defaultValue": "Create" }, { @@ -2388,14 +2296,8 @@ "name": "Form Step", "icon": "AssetsAdded", "hasChildren": true, - "illegalChildren": [ - "section", - "form", - "form step" - ], - "styles": [ - "size" - ], + "illegalChildren": ["section", "form", "form step"], + "styles": ["size"], "size": { "width": 400, "height": 400 @@ -2413,12 +2315,8 @@ "fieldgroup": { "name": "Field Group", "icon": "Group", - "illegalChildren": [ - "section" - ], - "styles": [ - "size" - ], + "illegalChildren": ["section"], + "styles": ["size"], "hasChildren": true, "size": { "width": 400, @@ -2451,9 +2349,7 @@ "skeleton": false, "name": "Text Field", "icon": "Text", - "styles": [ - "size" - ], + "styles": ["size"], "editable": true, "size": { "width": 400, @@ -2543,9 +2439,7 @@ "skeleton": false, "name": "Number Field", "icon": "123", - "styles": [ - "size" - ], + "styles": ["size"], "editable": true, "size": { "width": 400, @@ -2601,9 +2495,7 @@ "skeleton": false, "name": "Password Field", "icon": "LockClosed", - "styles": [ - "size" - ], + "styles": ["size"], "editable": true, "size": { "width": 400, @@ -2659,9 +2551,7 @@ "skeleton": false, "name": "Options Picker", "icon": "Menu", - "styles": [ - "size" - ], + "styles": ["size"], "editable": true, "size": { "width": 400, @@ -2828,9 +2718,7 @@ "skeleton": false, "name": "Multi-select Picker", "icon": "ViewList", - "styles": [ - "size" - ], + "styles": ["size"], "editable": true, "size": { "width": 400, @@ -3070,9 +2958,7 @@ "skeleton": false, "name": "Long Form Field", "icon": "TextAlignLeft", - "styles": [ - "size" - ], + "styles": ["size"], "editable": true, "size": { "width": 400, @@ -3150,9 +3036,7 @@ "skeleton": false, "name": "Date Picker", "icon": "Date", - "styles": [ - "size" - ], + "styles": ["size"], "editable": true, "size": { "width": 400, @@ -3232,9 +3116,7 @@ "skeleton": false, "name": "Barcode/QR Scanner", "icon": "Camera", - "styles": [ - "size" - ], + "styles": ["size"], "size": { "width": 400, "height": 50 @@ -3283,9 +3165,7 @@ "embeddedmap": { "name": "Embedded Map", "icon": "Location", - "styles": [ - "size" - ], + "styles": ["size"], "draggable": false, "size": { "width": 400, @@ -3398,9 +3278,7 @@ "skeleton": false, "name": "Attachment", "icon": "Attach", - "styles": [ - "size" - ], + "styles": ["size"], "editable": true, "size": { "width": 400, @@ -3463,9 +3341,7 @@ "skeleton": false, "name": "Relationship Picker", "icon": "TaskList", - "styles": [ - "size" - ], + "styles": ["size"], "editable": true, "size": { "width": 400, @@ -3527,9 +3403,7 @@ "skeleton": false, "name": "JSON Field", "icon": "Brackets", - "styles": [ - "size" - ], + "styles": ["size"], "editable": true, "size": { "width": 400, @@ -3579,9 +3453,7 @@ "s3upload": { "name": "S3 File Upload", "icon": "UploadToCloud", - "styles": [ - "size" - ], + "styles": ["size"], "editable": true, "size": { "width": 400, @@ -3642,13 +3514,9 @@ "dataprovider": { "name": "Data Provider", "icon": "Data", - "illegalChildren": [ - "section" - ], + "illegalChildren": ["section"], "hasChildren": true, - "actions": [ - "RefreshDatasource" - ], + "actions": ["RefreshDatasource"], "size": { "width": 400, "height": 100 @@ -3674,10 +3542,7 @@ "type": "select", "label": "Sort Order", "key": "sortOrder", - "options": [ - "Ascending", - "Descending" - ], + "options": ["Ascending", "Descending"], "defaultValue": "Ascending" }, { @@ -3729,9 +3594,7 @@ "skeleton": false, "name": "Table", "icon": "Table", - "illegalChildren": [ - "section" - ], + "illegalChildren": ["section"], "hasChildren": true, "showEmptyState": false, "size": { @@ -3815,9 +3678,7 @@ "daterangepicker": { "name": "Date Range", "icon": "Calendar", - "styles": [ - "size" - ], + "styles": ["size"], "hasChildren": false, "size": { "width": 200, @@ -3856,9 +3717,7 @@ "spectrumcard": { "name": "Card", "icon": "PersonalizationField", - "styles": [ - "size" - ], + "styles": ["size"], "size": { "width": 300, "height": 120 @@ -4031,10 +3890,7 @@ "type": "select", "label": "Sort Order", "key": "sortOrder", - "options": [ - "Ascending", - "Descending" - ], + "options": ["Ascending", "Descending"], "defaultValue": "Ascending" }, { @@ -4213,11 +4069,7 @@ "type": "select", "label": "Format", "key": "yAxisUnits", - "options": [ - "Default", - "Thousands", - "Millions" - ], + "options": ["Default", "Thousands", "Millions"], "defaultValue": "Default" }, { @@ -4271,11 +4123,7 @@ "type": "select", "label": "Format", "key": "yAxisUnits", - "options": [ - "Default", - "Thousands", - "Millions" - ], + "options": ["Default", "Thousands", "Millions"], "defaultValue": "Default" }, { @@ -4292,11 +4140,7 @@ "type": "select", "label": "Curve", "key": "curve", - "options": [ - "Smooth", - "Straight", - "Stepline" - ], + "options": ["Smooth", "Straight", "Stepline"], "defaultValue": "Smooth" } ] @@ -4328,11 +4172,7 @@ "type": "select", "label": "Format", "key": "yAxisUnits", - "options": [ - "Default", - "Thousands", - "Millions" - ], + "options": ["Default", "Thousands", "Millions"], "defaultValue": "Default" }, { @@ -4349,11 +4189,7 @@ "type": "select", "label": "Curve", "key": "curve", - "options": [ - "Smooth", - "Straight", - "Stepline" - ], + "options": ["Smooth", "Straight", "Stepline"], "defaultValue": "Smooth" }, { @@ -4418,11 +4254,7 @@ "type": "select", "label": "Format", "key": "yAxisUnits", - "options": [ - "Default", - "Thousands", - "Millions" - ], + "options": ["Default", "Thousands", "Millions"], "defaultValue": "Default" }, { @@ -4443,9 +4275,7 @@ "block": true, "name": "Table block", "icon": "Table", - "styles": [ - "size" - ], + "styles": ["size"], "size": { "width": 600, "height": 400 @@ -4483,10 +4313,7 @@ "type": "select", "label": "Sort Order", "key": "sortOrder", - "options": [ - "Ascending", - "Descending" - ], + "options": ["Ascending", "Descending"], "defaultValue": "Ascending" }, { @@ -4638,9 +4465,7 @@ "block": true, "name": "Cards block", "icon": "PersonalizationField", - "styles": [ - "size" - ], + "styles": ["size"], "size": { "width": 600, "height": 400 @@ -4679,10 +4504,7 @@ "type": "select", "label": "Sort Order", "key": "sortOrder", - "options": [ - "Ascending", - "Descending" - ], + "options": ["Ascending", "Descending"], "defaultValue": "Descending" }, { @@ -4816,9 +4638,7 @@ "block": true, "name": "Repeater block", "icon": "ViewList", - "illegalChildren": [ - "section" - ], + "illegalChildren": ["section"], "hasChildren": true, "size": { "width": 400, @@ -4846,10 +4666,7 @@ "type": "select", "label": "Sort Order", "key": "sortOrder", - "options": [ - "Ascending", - "Descending" - ], + "options": ["Ascending", "Descending"], "defaultValue": "Descending" }, { @@ -5044,9 +4861,7 @@ "markdownviewer": { "name": "Markdown Viewer", "icon": "Preview", - "styles": [ - "size" - ], + "styles": ["size"], "size": { "width": 400, "height": 100 @@ -5063,9 +4878,7 @@ "formblock": { "name": "Form Block", "icon": "Form", - "styles": [ - "size" - ], + "styles": ["size"], "block": true, "info": "Form blocks are only compatible with internal or SQL tables", "size": { @@ -5077,11 +4890,7 @@ "type": "select", "label": "Type", "key": "actionType", - "options": [ - "Create", - "Update", - "View" - ], + "options": ["Create", "Update", "View"], "defaultValue": "Create" }, { @@ -5215,10 +5024,7 @@ "name": "Side Panel", "icon": "RailRight", "hasChildren": true, - "illegalChildren": [ - "section", - "sidepanel" - ], + "illegalChildren": ["section", "sidepanel"], "showEmptyState": false, "draggable": false, "info": "Side panels are hidden by default. They will only be revealed when triggered by the 'Open Side Panel' action." @@ -5307,4 +5113,4 @@ "suffix": "repeater" } } -} \ No newline at end of file +} diff --git a/packages/client/package.json b/packages/client/package.json index a31cda3f16..ea62d102fc 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.4.12-alpha.5", + "version": "2.4.26", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,11 +19,11 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.4.12-alpha.5", - "@budibase/frontend-core": "2.4.12-alpha.5", - "@budibase/shared-core": "2.4.12-alpha.5", - "@budibase/string-templates": "2.4.12-alpha.5", - "@budibase/types": "2.4.12-alpha.5", + "@budibase/bbui": "^2.4.26", + "@budibase/frontend-core": "^2.4.26", + "@budibase/shared-core": "^2.4.26", + "@budibase/string-templates": "^2.4.26", + "@budibase/types": "^2.4.26", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/client/src/components/app/BackgroundImage.svelte b/packages/client/src/components/app/BackgroundImage.svelte index 909e0fd3fd..df6459c417 100644 --- a/packages/client/src/components/app/BackgroundImage.svelte +++ b/packages/client/src/components/app/BackgroundImage.svelte @@ -21,7 +21,9 @@ {#if url}
-
+
+ +
{:else if $builderStore.inBuilder}
{ - ctx.config = { - jwtSecret: env.JWT_SECRET, - useAppRootPath: true, - } - await next() - }) // re-direct before any middlewares occur .redirect("/", "/builder") .use( diff --git a/packages/server/src/environment.ts b/packages/server/src/environment.ts index 256d8d10c3..058e8bdff8 100644 --- a/packages/server/src/environment.ts +++ b/packages/server/src/environment.ts @@ -39,17 +39,14 @@ let inThread = false const environment = { // important - prefer app port to generic port PORT: process.env.APP_PORT || process.env.PORT, - JWT_SECRET: process.env.JWT_SECRET, COUCH_DB_URL: process.env.COUCH_DB_URL, MINIO_URL: process.env.MINIO_URL, WORKER_URL: process.env.WORKER_URL, AWS_REGION: process.env.AWS_REGION, MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, - CDN_URL: process.env.CDN_URL || "https://cdn.budi.live", REDIS_URL: process.env.REDIS_URL, REDIS_PASSWORD: process.env.REDIS_PASSWORD, - INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, HTTP_MIGRATIONS: process.env.HTTP_MIGRATIONS, API_REQ_LIMIT_PER_SEC: process.env.API_REQ_LIMIT_PER_SEC, GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, diff --git a/packages/server/src/integrations/oracle.ts b/packages/server/src/integrations/oracle.ts index abf9bcbbe3..65e0829905 100644 --- a/packages/server/src/integrations/oracle.ts +++ b/packages/server/src/integrations/oracle.ts @@ -67,6 +67,7 @@ const SCHEMA: Integration = { database: { type: DatasourceFieldType.STRING, required: true, + display: "Service Name", }, user: { type: DatasourceFieldType.STRING, diff --git a/packages/server/src/integrations/s3.ts b/packages/server/src/integrations/s3.ts index b965b177d0..ad3bb09109 100644 --- a/packages/server/src/integrations/s3.ts +++ b/packages/server/src/integrations/s3.ts @@ -107,6 +107,7 @@ const SCHEMA: Integration = { readCsv: { displayName: "Read CSV", type: QueryType.FIELDS, + readable: true, fields: { bucket: { type: DatasourceFieldType.STRING, diff --git a/packages/server/src/tests/utilities/TestConfiguration.ts b/packages/server/src/tests/utilities/TestConfiguration.ts index 9aab76ba4d..cf0585efd1 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.ts +++ b/packages/server/src/tests/utilities/TestConfiguration.ts @@ -205,7 +205,6 @@ class TestConfiguration { request.appId = appId // fake cookies, we don't need them request.cookies = { set: () => {}, get: () => {} } - request.config = { jwtSecret: env.JWT_SECRET } request.user = { appId, tenantId: this.getTenantId() } request.query = {} request.request = { @@ -332,8 +331,8 @@ class TestConfiguration { roleId: roleId, appId, } - const authToken = auth.jwt.sign(authObj, env.JWT_SECRET) - const appToken = auth.jwt.sign(app, env.JWT_SECRET) + const authToken = auth.jwt.sign(authObj, coreEnv.JWT_SECRET) + const appToken = auth.jwt.sign(app, coreEnv.JWT_SECRET) // returning necessary request headers await cache.user.invalidateUser(userId) @@ -361,8 +360,8 @@ class TestConfiguration { roleId: roles.BUILTIN_ROLE_IDS.ADMIN, appId: this.appId, } - const authToken = auth.jwt.sign(authObj, env.JWT_SECRET) - const appToken = auth.jwt.sign(app, env.JWT_SECRET) + const authToken = auth.jwt.sign(authObj, coreEnv.JWT_SECRET) + const appToken = auth.jwt.sign(app, coreEnv.JWT_SECRET) const headers: any = { Accept: "application/json", Cookie: [ diff --git a/packages/server/src/utilities/workerRequests.ts b/packages/server/src/utilities/workerRequests.ts index e318b12f82..82e1aac428 100644 --- a/packages/server/src/utilities/workerRequests.ts +++ b/packages/server/src/utilities/workerRequests.ts @@ -6,6 +6,7 @@ import { constants, tenancy, logging, + env as coreEnv, } from "@budibase/backend-core" import { updateAppRole } from "./global" import { BBContext, User } from "@budibase/types" @@ -15,7 +16,7 @@ export function request(ctx?: BBContext, request?: any) { request.headers = {} } if (!ctx) { - request.headers[constants.Header.API_KEY] = env.INTERNAL_API_KEY + request.headers[constants.Header.API_KEY] = coreEnv.INTERNAL_API_KEY if (tenancy.isTenantIdSet()) { request.headers[constants.Header.TENANT_ID] = tenancy.getTenantId() } diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 882d194bb1..d09ac3c7c9 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1278,14 +1278,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.4.12-alpha.5": - version "2.4.12-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.4.12-alpha.5.tgz#6fc37b439e05f0806909954c5c9f01f37e99f4d8" - integrity sha512-TVXjKXT/67ZWK3L6Rs1eJ1+8li4o3+zxOisVuSzgAHTepm6tbF9GLNWIVlzMoLGh5k9M9GHjCkhRKmxozMrBYw== +"@budibase/backend-core@2.4.26": + version "2.4.26" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.4.26.tgz#ae9679f20e86ce1706d6d549aed78a342365a4b4" + integrity sha512-9QYJbAT9WPiOckBIR6a/CoqqbUiP9vlmc/Iy5TR5Yj2wy1JnWsf09ReTuL3CsHmh+8bCJlUHZZC4m6PUMg7+ow== dependencies: "@budibase/nano" "10.1.2" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.4.12-alpha.5" + "@budibase/types" "^2.4.26" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -1417,14 +1417,14 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.4.12-alpha.5": - version "2.4.12-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.4.12-alpha.5.tgz#ebdaf6fe987a35c9dee00a36bbcf5acb738015de" - integrity sha512-j749G4I9NHnEE+0AlFckFjBa3Hkx8M93Raw5s+C7YxaPpChws2HfN/7fCSgY33aeCCGqB0SpwCKAm48BSwbwwQ== +"@budibase/pro@2.4.26": + version "2.4.26" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.4.26.tgz#37ca2b94f5dfc28ee4ff0ffa088e29112de5b66f" + integrity sha512-PXpsj5DFnUaSlp3AHZRZa/N4CD02HPpvVFv35/FUGkeGwGJ5AihhmzxlD54U9Q9X3Ln8miejYTFoWvEnV5Ei8w== dependencies: - "@budibase/backend-core" "2.4.12-alpha.5" + "@budibase/backend-core" "2.4.26" "@budibase/string-templates" "2.3.20" - "@budibase/types" "2.4.12-alpha.5" + "@budibase/types" "2.4.26" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1463,10 +1463,10 @@ lodash "^4.17.20" vm2 "^3.9.4" -"@budibase/types@2.4.12-alpha.5": - version "2.4.12-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.4.12-alpha.5.tgz#3727ddef178aebb354e43de0efe03a329b37b91f" - integrity sha512-ddtKzLjNcqdQjwYv1lNRo1t5XHdxiHRsFl+xMFsMwpB/8IY8LDw7zvkoC58sFYPUvOP4c1cBA0Wne9YNxM5IiA== +"@budibase/types@2.4.26", "@budibase/types@^2.4.26": + version "2.4.26" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.4.26.tgz#c4efd9286e736feee56d623c21a9f6fd7c922b94" + integrity sha512-q2QfDXJAopmHNq6Y25udmVJoEtnoskZEtaMy5d7/hX4jePJX3QnBd9sjgnAoOeSC3NOuXDjmvcRGtqXz6ao/Ag== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/shared-core/package.json b/packages/shared-core/package.json index 3b8c1924a2..1ed7d4243e 100644 --- a/packages/shared-core/package.json +++ b/packages/shared-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/shared-core", - "version": "2.4.12-alpha.5", + "version": "2.4.26", "description": "Shared data utils", "main": "dist/cjs/src/index.js", "types": "dist/mjs/src/index.d.ts", @@ -20,7 +20,7 @@ "dev:builder": "yarn prebuild && concurrently \"tsc -p tsconfig.build.json --watch\" \"tsc -p tsconfig-cjs.build.json --watch\"" }, "dependencies": { - "@budibase/types": "2.4.12-alpha.5" + "@budibase/types": "^2.4.26" }, "devDependencies": { "concurrently": "^7.6.0", diff --git a/packages/shared-core/src/filters.ts b/packages/shared-core/src/filters.ts index 2486ca1050..34186cf554 100644 --- a/packages/shared-core/src/filters.ts +++ b/packages/shared-core/src/filters.ts @@ -62,7 +62,7 @@ export const getValidOperatorsForType = ( // Only allow equal/not equal for _id in SQL tables if (field === "_id" && externalTable) { - ops = [Op.Equals, Op.NotEquals] + ops = [Op.Equals, Op.NotEquals, Op.In] } return ops diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index cd67f276b9..998715179f 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.4.12-alpha.5", + "version": "2.4.26", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/types/package.json b/packages/types/package.json index d00f98b647..56f4e56dd0 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.4.12-alpha.5", + "version": "2.4.26", "description": "Budibase types", "main": "dist/cjs/index.js", "types": "dist/mjs/index.d.ts", diff --git a/packages/worker/package.json b/packages/worker/package.json index e366f7fc49..7f4797bd1b 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.4.12-alpha.5", + "version": "2.4.26", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.4.12-alpha.5", - "@budibase/pro": "2.4.12-alpha.5", - "@budibase/string-templates": "2.4.12-alpha.5", - "@budibase/types": "2.4.12-alpha.5", + "@budibase/backend-core": "^2.4.26", + "@budibase/pro": "2.4.26", + "@budibase/string-templates": "^2.4.26", + "@budibase/types": "^2.4.26", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", diff --git a/packages/worker/src/api/controllers/global/configs.ts b/packages/worker/src/api/controllers/global/configs.ts index eced642155..ac502e5054 100644 --- a/packages/worker/src/api/controllers/global/configs.ts +++ b/packages/worker/src/api/controllers/global/configs.ts @@ -295,7 +295,7 @@ export async function publicSettings( // google const googleConfig = await configs.getGoogleConfig() - const preActivated = googleConfig?.activated == null + const preActivated = googleConfig && googleConfig.activated == null const google = preActivated || !!googleConfig?.activated const _googleCallbackUrl = await googleCallbackUrl(googleConfig) diff --git a/packages/worker/src/api/routes/global/tests/configs.spec.ts b/packages/worker/src/api/routes/global/tests/configs.spec.ts index 892fe8a67b..1abe575b02 100644 --- a/packages/worker/src/api/routes/global/tests/configs.spec.ts +++ b/packages/worker/src/api/routes/global/tests/configs.spec.ts @@ -288,7 +288,7 @@ describe("configs", () => { company: "Budibase", logoUrl: "", analyticsEnabled: false, - google: true, + google: false, googleCallbackUrl: `http://localhost:10000/api/global/auth/${config.tenantId}/google/callback`, isSSOEnforced: false, oidc: false, diff --git a/packages/worker/src/environment.ts b/packages/worker/src/environment.ts index 3b5960f6f5..812af6aacd 100644 --- a/packages/worker/src/environment.ts +++ b/packages/worker/src/environment.ts @@ -30,10 +30,8 @@ const environment = { // auth MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, - JWT_SECRET: process.env.JWT_SECRET, SALT_ROUNDS: process.env.SALT_ROUNDS, REDIS_PASSWORD: process.env.REDIS_PASSWORD, - INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, // urls MINIO_URL: process.env.MINIO_URL, @@ -42,7 +40,6 @@ const environment = { ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL, PLATFORM_URL: process.env.PLATFORM_URL, APPS_URL: process.env.APPS_URL, - CDN_URL: process.env.CDN_URL || "https://tenants.cdn.budi.live", // ports // prefer worker port to generic port PORT: process.env.WORKER_PORT || process.env.PORT, diff --git a/packages/worker/src/middleware/cloudRestricted.ts b/packages/worker/src/middleware/cloudRestricted.ts index 5440629de3..f9ab86e2e9 100644 --- a/packages/worker/src/middleware/cloudRestricted.ts +++ b/packages/worker/src/middleware/cloudRestricted.ts @@ -1,5 +1,5 @@ import env from "../environment" -import { constants } from "@budibase/backend-core" +import { constants, utils } from "@budibase/backend-core" import { BBContext } from "@budibase/types" /** @@ -9,7 +9,15 @@ import { BBContext } from "@budibase/types" export default async (ctx: BBContext, next: any) => { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { const apiKey = ctx.request.headers[constants.Header.API_KEY] - if (apiKey !== env.INTERNAL_API_KEY) { + if (!apiKey) { + ctx.throw(403, "Unauthorized") + } + + if (Array.isArray(apiKey)) { + ctx.throw(403, "Unauthorized") + } + + if (!utils.isValidInternalAPIKey(apiKey)) { ctx.throw(403, "Unauthorized") } } diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index 98830c576d..2e716426d5 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -5,10 +5,10 @@ import { sessions, events, HTTPError, + env as coreEnv, } from "@budibase/backend-core" import { PlatformLogoutOpts, User } from "@budibase/types" import jwt from "jsonwebtoken" -import env from "../../environment" import * as userSdk from "../users" import * as emails from "../../utilities/email" import * as redis from "../../utilities/redis" @@ -26,7 +26,7 @@ export async function loginUser(user: User) { sessionId, tenantId, }, - env.JWT_SECRET! + coreEnv.JWT_SECRET! ) return token } diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index 3004d0aed4..e5ed9e8141 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -74,7 +74,6 @@ class TestConfiguration { const request: any = {} // fake cookies, we don't need them request.cookies = { set: () => {}, get: () => {} } - request.config = { jwtSecret: env.JWT_SECRET } request.user = { tenantId: this.getTenantId() } request.query = {} request.request = { @@ -180,7 +179,7 @@ class TestConfiguration { sessionId: "sessionid", tenantId: user.tenantId, } - const authCookie = auth.jwt.sign(authToken, env.JWT_SECRET) + const authCookie = auth.jwt.sign(authToken, coreEnv.JWT_SECRET) return { Accept: "application/json", ...this.cookieHeader([`${constants.Cookie.Auth}=${authCookie}`]), @@ -197,7 +196,7 @@ class TestConfiguration { } internalAPIHeaders() { - return { [constants.Header.API_KEY]: env.INTERNAL_API_KEY } + return { [constants.Header.API_KEY]: coreEnv.INTERNAL_API_KEY } } adminOnlyResponse = () => { @@ -277,7 +276,7 @@ class TestConfiguration { // CONFIGS - OIDC getOIDConfigCookie(configId: string) { - const token = auth.jwt.sign(configId, env.JWT_SECRET) + const token = auth.jwt.sign(configId, coreEnv.JWT_SECRET) return this.cookieHeader([[`${constants.Cookie.OIDC_CONFIG}=${token}`]]) } diff --git a/packages/worker/src/utilities/appService.ts b/packages/worker/src/utilities/appService.ts index 478e986fe8..8f411d58fa 100644 --- a/packages/worker/src/utilities/appService.ts +++ b/packages/worker/src/utilities/appService.ts @@ -1,5 +1,10 @@ import fetch from "node-fetch" -import { constants, tenancy, logging } from "@budibase/backend-core" +import { + constants, + tenancy, + logging, + env as coreEnv, +} from "@budibase/backend-core" import { checkSlashesInUrl } from "../utilities" import env from "../environment" import { SyncUserRequest, User } from "@budibase/types" @@ -9,7 +14,7 @@ async function makeAppRequest(url: string, method: string, body: any) { return } const request: any = { headers: {} } - request.headers[constants.Header.API_KEY] = env.INTERNAL_API_KEY + request.headers[constants.Header.API_KEY] = coreEnv.INTERNAL_API_KEY if (tenancy.isTenantIdSet()) { request.headers[constants.Header.TENANT_ID] = tenancy.getTenantId() } diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index 5d369bcd45..7855e71965 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -475,14 +475,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.4.12-alpha.5": - version "2.4.12-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.4.12-alpha.5.tgz#6fc37b439e05f0806909954c5c9f01f37e99f4d8" - integrity sha512-TVXjKXT/67ZWK3L6Rs1eJ1+8li4o3+zxOisVuSzgAHTepm6tbF9GLNWIVlzMoLGh5k9M9GHjCkhRKmxozMrBYw== +"@budibase/backend-core@2.4.26": + version "2.4.26" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.4.26.tgz#ae9679f20e86ce1706d6d549aed78a342365a4b4" + integrity sha512-9QYJbAT9WPiOckBIR6a/CoqqbUiP9vlmc/Iy5TR5Yj2wy1JnWsf09ReTuL3CsHmh+8bCJlUHZZC4m6PUMg7+ow== dependencies: "@budibase/nano" "10.1.2" "@budibase/pouchdb-replication-stream" "1.2.10" - "@budibase/types" "2.4.12-alpha.5" + "@budibase/types" "^2.4.26" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-cloudfront-sign "2.2.0" @@ -564,14 +564,14 @@ pouchdb-promise "^6.0.4" through2 "^2.0.0" -"@budibase/pro@2.4.12-alpha.5": - version "2.4.12-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.4.12-alpha.5.tgz#ebdaf6fe987a35c9dee00a36bbcf5acb738015de" - integrity sha512-j749G4I9NHnEE+0AlFckFjBa3Hkx8M93Raw5s+C7YxaPpChws2HfN/7fCSgY33aeCCGqB0SpwCKAm48BSwbwwQ== +"@budibase/pro@2.4.26": + version "2.4.26" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.4.26.tgz#37ca2b94f5dfc28ee4ff0ffa088e29112de5b66f" + integrity sha512-PXpsj5DFnUaSlp3AHZRZa/N4CD02HPpvVFv35/FUGkeGwGJ5AihhmzxlD54U9Q9X3Ln8miejYTFoWvEnV5Ei8w== dependencies: - "@budibase/backend-core" "2.4.12-alpha.5" + "@budibase/backend-core" "2.4.26" "@budibase/string-templates" "2.3.20" - "@budibase/types" "2.4.12-alpha.5" + "@budibase/types" "2.4.26" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -592,10 +592,10 @@ lodash "^4.17.20" vm2 "^3.9.4" -"@budibase/types@2.4.12-alpha.5": - version "2.4.12-alpha.5" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.4.12-alpha.5.tgz#3727ddef178aebb354e43de0efe03a329b37b91f" - integrity sha512-ddtKzLjNcqdQjwYv1lNRo1t5XHdxiHRsFl+xMFsMwpB/8IY8LDw7zvkoC58sFYPUvOP4c1cBA0Wne9YNxM5IiA== +"@budibase/types@2.4.26", "@budibase/types@^2.4.26": + version "2.4.26" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.4.26.tgz#c4efd9286e736feee56d623c21a9f6fd7c922b94" + integrity sha512-q2QfDXJAopmHNq6Y25udmVJoEtnoskZEtaMy5d7/hX4jePJX3QnBd9sjgnAoOeSC3NOuXDjmvcRGtqXz6ao/Ag== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" diff --git a/qa-core/.env b/qa-core/.env index 096fb4e157..be24fd2d28 100644 --- a/qa-core/.env +++ b/qa-core/.env @@ -1,6 +1,5 @@ BB_ADMIN_USER_EMAIL=qa@budibase.com BB_ADMIN_USER_PASSWORD=budibase -ENCRYPTED_TEST_PUBLIC_API_KEY=a65722f06bee5caeadc5d7ca2f543a43-d610e627344210c643bb726f COUCH_DB_URL=http://budibase:budibase@localhost:4567 COUCH_DB_USER=budibase COUCH_DB_PASSWORD=budibase diff --git a/qa-core/scripts/jestSetup.js b/qa-core/scripts/jestSetup.js index cd63258f7a..a6f8a6478c 100644 --- a/qa-core/scripts/jestSetup.js +++ b/qa-core/scripts/jestSetup.js @@ -1,11 +1,3 @@ -const env = require("../src/environment") - -env._set("BUDIBASE_SERVER_URL", "http://localhost:4100") -env._set( - "BUDIBASE_PUBLIC_API_KEY", - "a65722f06bee5caeadc5d7ca2f543a43-d610e627344210c643bb726f" -) - // mock all dates to 2020-01-01T00:00:00.000Z // use tk.reset() to use real dates in individual tests const MOCK_DATE = new Date("2020-01-01T00:00:00.000Z") diff --git a/qa-core/src/config/internal-api/TestConfiguration/InternalAPIClient.ts b/qa-core/src/config/internal-api/TestConfiguration/InternalAPIClient.ts index ef47d8a12b..1c79f47609 100644 --- a/qa-core/src/config/internal-api/TestConfiguration/InternalAPIClient.ts +++ b/qa-core/src/config/internal-api/TestConfiguration/InternalAPIClient.ts @@ -49,14 +49,16 @@ class InternalAPIClient { // @ts-ignore const response = await fetch(`https://${process.env.TENANT_ID}.${this.host}${url}`, requestOptions) - if (response.status == 404 || response.status == 500) { + if ( + response.status == 404 || + response.status == 500 || + response.status == 403 + ) { console.error("Error in apiCall") - console.error("Response:") - console.error(response) - console.error("Response body:") - console.error(response.body) - console.error("Request body:") - console.error(requestOptions.body) + console.error("Response:", response) + const json = await response.json() + console.error("Response body:", json) + console.error("Request body:", requestOptions.body) } return response } diff --git a/qa-core/src/config/internal-api/TestConfiguration/accountsAPIClient.ts b/qa-core/src/config/internal-api/TestConfiguration/accountsAPIClient.ts index aff821a7ac..9de03b75b6 100644 --- a/qa-core/src/config/internal-api/TestConfiguration/accountsAPIClient.ts +++ b/qa-core/src/config/internal-api/TestConfiguration/accountsAPIClient.ts @@ -44,12 +44,10 @@ class AccountsAPIClient { const response = await fetch(`${this.host}${url}`, requestOptions) if (response.status == 404 || response.status == 500) { console.error("Error in apiCall") - console.error("Response:") - console.error(response) - console.error("Response body:") - console.error(response.body) - console.error("Request body:") - console.error(requestOptions.body) + console.error("Response:", response) + const json = await response.json() + console.error("Response body:", json) + console.error("Request body:", requestOptions.body) } return response } diff --git a/qa-core/src/config/internal-api/fixtures/accounts.ts b/qa-core/src/config/internal-api/fixtures/accounts.ts index dbeabae928..64b6d51f44 100644 --- a/qa-core/src/config/internal-api/fixtures/accounts.ts +++ b/qa-core/src/config/internal-api/fixtures/accounts.ts @@ -5,7 +5,8 @@ import { Hosting } from "@budibase/types" export const generateAccount = (): Partial => { const randomGuid = generator.guid() - let tenant: string = "a" + randomGuid + //Needs to start with a letter + let tenant: string = "tenant" + randomGuid tenant = tenant.replace(/-/g, "") return { diff --git a/qa-core/src/config/public-api/TestConfiguration/PublicAPIClient.ts b/qa-core/src/config/public-api/TestConfiguration/PublicAPIClient.ts index 3721e31da3..0dfe74bb6a 100644 --- a/qa-core/src/config/public-api/TestConfiguration/PublicAPIClient.ts +++ b/qa-core/src/config/public-api/TestConfiguration/PublicAPIClient.ts @@ -11,20 +11,32 @@ interface ApiOptions { class PublicAPIClient { host: string - apiKey: string + apiKey?: string + tenantName?: string appId?: string + cookie?: string constructor(appId?: string) { - if (!env.BUDIBASE_PUBLIC_API_KEY || !env.BUDIBASE_SERVER_URL) { + if (!env.BUDIBASE_HOST) { throw new Error( "Must set BUDIBASE_PUBLIC_API_KEY and BUDIBASE_SERVER_URL env vars" ) } - this.host = `${env.BUDIBASE_SERVER_URL}/api/public/v1` - this.apiKey = env.BUDIBASE_PUBLIC_API_KEY + this.host = `${env.BUDIBASE_HOST}/api/public/v1` + this.appId = appId } + setTenantName(tenantName: string) { + this.tenantName = tenantName + } + + setApiKey(apiKey: string) { + this.apiKey = apiKey + process.env.BUDIBASE_PUBLIC_API_KEY = apiKey + this.host = `${env.BUDIBASE_HOST}/api/public/v1` + } + apiCall = (method: APIMethod) => async (url = "", options: ApiOptions = {}) => { @@ -32,18 +44,27 @@ class PublicAPIClient { method, body: JSON.stringify(options.body), headers: { - "x-budibase-api-key": this.apiKey, + "x-budibase-api-key": this.apiKey || null, "x-budibase-app-id": this.appId, "Content-Type": "application/json", Accept: "application/json", ...options.headers, + cookie: this.cookie, + redirect: "follow", + follow: 20, }, } + // prettier-ignore // @ts-ignore - const response = await fetch(`${this.host}${url}`, requestOptions) - if (response.status !== 200) { - console.error(response) + const response = await fetch(`https://${process.env.TENANT_ID}.${this.host}${url}`, requestOptions) + + if (response.status == 500 || response.status == 403) { + console.error("Error in apiCall") + console.error("Response:", response) + const json = await response.json() + console.error("Response body:", json) + console.error("Request body:", requestOptions.body) } return response } diff --git a/qa-core/src/config/public-api/TestConfiguration/accounts.ts b/qa-core/src/config/public-api/TestConfiguration/accounts.ts new file mode 100644 index 0000000000..fdf5aedbd0 --- /dev/null +++ b/qa-core/src/config/public-api/TestConfiguration/accounts.ts @@ -0,0 +1,38 @@ +import { Response } from "node-fetch" +import { Account } from "@budibase/types" +import AccountsAPIClient from "./accountsAPIClient" +import { NewAccount } from "../fixtures/types/newAccount" + +export default class AccountsApi { + api: AccountsAPIClient + + constructor(AccountsAPIClient: AccountsAPIClient) { + this.api = AccountsAPIClient + } + + async validateEmail(email: string): Promise { + const response = await this.api.post(`/accounts/validate/email`, { + body: { email }, + }) + expect(response).toHaveStatusCode(200) + return response + } + + async validateTenantId(tenantId: string): Promise { + const response = await this.api.post(`/accounts/validate/tenantId`, { + body: { tenantId }, + }) + expect(response).toHaveStatusCode(200) + return response + } + + async create(body: Partial): Promise<[Response, Account]> { + const headers = { + "no-verify": "1", + } + const response = await this.api.post(`/accounts`, { body, headers }) + const json = await response.json() + expect(response).toHaveStatusCode(201) + return [response, json] + } +} diff --git a/qa-core/src/config/public-api/TestConfiguration/accountsAPIClient.ts b/qa-core/src/config/public-api/TestConfiguration/accountsAPIClient.ts new file mode 100644 index 0000000000..2ea465adda --- /dev/null +++ b/qa-core/src/config/public-api/TestConfiguration/accountsAPIClient.ts @@ -0,0 +1,66 @@ +import env from "../../../environment" +import fetch, { HeadersInit } from "node-fetch" + +type APIMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" + +interface ApiOptions { + method?: APIMethod + body?: object + headers?: HeadersInit | undefined +} + +class AccountsAPIClient { + host: string + appId?: string + cookie?: string + + constructor(appId?: string) { + if (!env.BUDIBASE_ACCOUNTS_URL) { + throw new Error("Must set BUDIBASE_SERVER_URL env var") + } + this.host = `${env.BUDIBASE_ACCOUNTS_URL}/api` + this.appId = appId + } + + apiCall = + (method: APIMethod) => + async (url = "", options: ApiOptions = {}) => { + const requestOptions = { + method, + body: JSON.stringify(options.body), + headers: { + "x-budibase-app-id": this.appId, + "Content-Type": "application/json", + Accept: "application/json", + cookie: this.cookie, + redirect: "follow", + follow: 20, + ...options.headers, + }, + credentials: "include", + } + + // @ts-ignore + const response = await fetch(`${this.host}${url}`, requestOptions) + if ( + response.status == 404 || + response.status == 500 || + response.status == 400 + ) { + console.error("Error in apiCall") + console.error("Response:", response) + const json = await response.json() + console.error("Response body:", json) + console.error("Request body:", requestOptions.body) + } + return response + } + + post = this.apiCall("POST") + get = this.apiCall("GET") + patch = this.apiCall("PATCH") + del = this.apiCall("DELETE") + put = this.apiCall("PUT") +} + +export default AccountsAPIClient diff --git a/qa-core/src/config/public-api/TestConfiguration/applications.ts b/qa-core/src/config/public-api/TestConfiguration/applications.ts index ab25930544..56b0b70795 100644 --- a/qa-core/src/config/public-api/TestConfiguration/applications.ts +++ b/qa-core/src/config/public-api/TestConfiguration/applications.ts @@ -63,4 +63,15 @@ export default class AppApi { const response = await this.api.post(`/applications/${id}/unpublish`) return [response] } + + async createFirstApp() { + const body = { + name: "My first app", + url: "my-first-app", + useTemplate: false, + sampleData: true, + } + const response = await this.api.post("/applications", { body }) + expect(response).toHaveStatusCode(200) + } } diff --git a/qa-core/src/config/public-api/TestConfiguration/auth.ts b/qa-core/src/config/public-api/TestConfiguration/auth.ts new file mode 100644 index 0000000000..3eb4df2245 --- /dev/null +++ b/qa-core/src/config/public-api/TestConfiguration/auth.ts @@ -0,0 +1,48 @@ +import { Response } from "node-fetch" +import AccountsAPIClient from "./accountsAPIClient" +import { ApiKeyResponse } from "../fixtures/types/apiKeyResponse" + +export default class AuthApi { + api: AccountsAPIClient + + constructor(apiClient: AccountsAPIClient) { + this.api = apiClient + } + + async loginAsAdmin(): Promise<[Response, any]> { + const response = await this.api.post(`/auth/login`, { + body: { + username: process.env.BB_ADMIN_USER_EMAIL, + password: process.env.BB_ADMIN_USER_PASSWORD, + }, + }) + const cookie = response.headers.get("set-cookie") + this.api.cookie = cookie as any + return [response, cookie] + } + + async login(email: String, password: String): Promise<[Response, any]> { + const response = await this.api.post(`/global/auth/default/login`, { + body: { + username: email, + password: password, + }, + }) + expect(response).toHaveStatusCode(200) + const cookie = response.headers.get("set-cookie") + this.api.cookie = cookie as any + return [response, cookie] + } + + async logout(): Promise { + return this.api.post(`/global/auth/logout`) + } + + async getApiKey(): Promise { + const response = await this.api.get(`/global/self/api_key`) + const json = await response.json() + expect(response).toHaveStatusCode(200) + expect(json).toHaveProperty("apiKey") + return json + } +} diff --git a/qa-core/src/config/public-api/TestConfiguration/index.ts b/qa-core/src/config/public-api/TestConfiguration/index.ts index 36cc3022b0..e67da27883 100644 --- a/qa-core/src/config/public-api/TestConfiguration/index.ts +++ b/qa-core/src/config/public-api/TestConfiguration/index.ts @@ -3,19 +3,82 @@ import ApplicationApi from "./applications" import TableApi from "./tables" import UserApi from "./users" import RowApi from "./rows" +import AuthApi from "./auth" +import AccountsApiClient from "./accountsAPIClient" +import AccountsApi from "./accounts" +import { generateAccount } from "../fixtures/accounts" +import internalApplicationsApi from "../../internal-api/TestConfiguration/applications" + +import InternalAPIClient from "../../internal-api/TestConfiguration/InternalAPIClient" export default class TestConfiguration { applications: ApplicationApi + auth: AuthApi users: UserApi tables: TableApi rows: RowApi context: T + accounts: AccountsApi + apiClient: PublicAPIClient + accountsApiClient: AccountsApiClient + internalApiClient: InternalAPIClient + internalApplicationsApi: internalApplicationsApi - constructor(apiClient: PublicAPIClient) { + constructor( + apiClient: PublicAPIClient, + accountsApiClient: AccountsApiClient, + internalApiClient: InternalAPIClient + ) { + this.apiClient = apiClient + this.accountsApiClient = accountsApiClient + this.internalApiClient = internalApiClient + + this.auth = new AuthApi(this.internalApiClient) + this.accounts = new AccountsApi(this.accountsApiClient) this.applications = new ApplicationApi(apiClient) this.users = new UserApi(apiClient) this.tables = new TableApi(apiClient) this.rows = new RowApi(apiClient) + this.internalApplicationsApi = new internalApplicationsApi( + internalApiClient + ) + + this.context = {} + } + + async setupAccountAndTenant() { + // This step is required to create a new account and tenant for the tests, its part of + // the support for running tests in multiple environments. + const account = generateAccount() + await this.accounts.validateEmail(account.email) + await this.accounts.validateTenantId(account.tenantId) + process.env.TENANT_ID = account.tenantId + await this.accounts.create(account) + await this.updateApiClients(account.tenantName) + await this.auth.login(account.email, account.password) + const body = { + name: "My first app", + url: "my-first-app", + useTemplate: false, + sampleData: true, + } + await this.internalApplicationsApi.create(body) + } + + // After the account and tenant have been created, we need to get and set the API key for the test + async setApiKey() { + const apiKeyResponse = await this.auth.getApiKey() + this.apiClient.setApiKey(apiKeyResponse.apiKey) + } + async updateApiClients(tenantName: string) { + this.apiClient.setTenantName(tenantName) + this.applications = new ApplicationApi(this.apiClient) + this.rows = new RowApi(this.apiClient) + this.internalApiClient.setTenantName(tenantName) + this.internalApplicationsApi = new internalApplicationsApi( + this.internalApiClient + ) + this.auth = new AuthApi(this.internalApiClient) this.context = {} } diff --git a/qa-core/src/config/public-api/fixtures/accounts.ts b/qa-core/src/config/public-api/fixtures/accounts.ts new file mode 100644 index 0000000000..64b6d51f44 --- /dev/null +++ b/qa-core/src/config/public-api/fixtures/accounts.ts @@ -0,0 +1,22 @@ +import { NewAccount } from "./types/newAccount" + +import generator from "../../generator" +import { Hosting } from "@budibase/types" + +export const generateAccount = (): Partial => { + const randomGuid = generator.guid() + //Needs to start with a letter + let tenant: string = "tenant" + randomGuid + tenant = tenant.replace(/-/g, "") + + return { + email: `qa+${randomGuid}@budibase.com`, + hosting: Hosting.CLOUD, + name: `qa+${randomGuid}@budibase.com`, + password: `${randomGuid}`, + profession: "software_engineer", + size: "10+", + tenantId: `${tenant}`, + tenantName: `${tenant}`, + } +} diff --git a/qa-core/src/config/public-api/fixtures/types/apiKeyResponse.ts b/qa-core/src/config/public-api/fixtures/types/apiKeyResponse.ts new file mode 100644 index 0000000000..4a62d60796 --- /dev/null +++ b/qa-core/src/config/public-api/fixtures/types/apiKeyResponse.ts @@ -0,0 +1,6 @@ +export interface ApiKeyResponse { + apiKey: string + createdAt: string + updatedAt: string + userId: string +} diff --git a/qa-core/src/config/public-api/fixtures/types/newAccount.ts b/qa-core/src/config/public-api/fixtures/types/newAccount.ts new file mode 100644 index 0000000000..e7ad88e697 --- /dev/null +++ b/qa-core/src/config/public-api/fixtures/types/newAccount.ts @@ -0,0 +1,5 @@ +import { Account } from "@budibase/types" + +export interface NewAccount extends Account { + password: string +} diff --git a/qa-core/src/tests/public-api/applications/applications.spec.ts b/qa-core/src/tests/public-api/applications/applications.spec.ts index cf85e6daf2..a5c0ed3691 100644 --- a/qa-core/src/tests/public-api/applications/applications.spec.ts +++ b/qa-core/src/tests/public-api/applications/applications.spec.ts @@ -1,15 +1,25 @@ import TestConfiguration from "../../../config/public-api/TestConfiguration" import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient" +import AccountsAPIClient from "../../../config/public-api/TestConfiguration/accountsAPIClient" import generateApp from "../../../config/public-api/fixtures/applications" import { Application } from "@budibase/server/api/controllers/public/mapping/types" import { db as dbCore } from "@budibase/backend-core" +import InternalAPIClient from "../../../config/internal-api/TestConfiguration/InternalAPIClient" describe("Public API - /applications endpoints", () => { const api = new PublicAPIClient() - const config = new TestConfiguration(api) + const accountsAPI = new AccountsAPIClient() + const internalAPI = new InternalAPIClient() + const config = new TestConfiguration( + api, + accountsAPI, + internalAPI + ) beforeAll(async () => { - await config.beforeAll() + await config.setupAccountAndTenant() + await config.setApiKey() + const [response, app] = await config.applications.seed() config.context = app }) diff --git a/qa-core/src/tests/public-api/tables/rows.spec.ts b/qa-core/src/tests/public-api/tables/rows.spec.ts index 89149159ab..d21b61e41a 100644 --- a/qa-core/src/tests/public-api/tables/rows.spec.ts +++ b/qa-core/src/tests/public-api/tables/rows.spec.ts @@ -2,14 +2,19 @@ import { Row } from "@budibase/server/api/controllers/public/mapping/types" import { generateRow } from "../../../config/public-api/fixtures/tables" import TestConfiguration from "../../../config/public-api/TestConfiguration" import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient" +import AccountsAPIClient from "../../../config/public-api/TestConfiguration/accountsAPIClient" +import InternalAPIClient from "../../../config/internal-api/TestConfiguration/InternalAPIClient" describe("Public API - /rows endpoints", () => { - let api = new PublicAPIClient() - - const config = new TestConfiguration(api) + const api = new PublicAPIClient() + const accountsAPI = new AccountsAPIClient() + const internalAPI = new InternalAPIClient() + const config = new TestConfiguration(api, accountsAPI, internalAPI) beforeAll(async () => { - await config.beforeAll() + await config.setupAccountAndTenant() + await config.setApiKey() + const [aResp, app] = await config.applications.seed() config.tables.api.appId = app._id diff --git a/qa-core/src/tests/public-api/tables/tables.spec.ts b/qa-core/src/tests/public-api/tables/tables.spec.ts index de1ce142ce..fc506d7bb6 100644 --- a/qa-core/src/tests/public-api/tables/tables.spec.ts +++ b/qa-core/src/tests/public-api/tables/tables.spec.ts @@ -2,13 +2,19 @@ import { Table } from "@budibase/server/api/controllers/public/mapping/types" import { generateTable } from "../../../config/public-api/fixtures/tables" import TestConfiguration from "../../../config/public-api/TestConfiguration" import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient" +import AccountsAPIClient from "../../../config/public-api/TestConfiguration/accountsAPIClient" +import InternalAPIClient from "../../../config/internal-api/TestConfiguration/InternalAPIClient" describe("Public API - /tables endpoints", () => { - let api = new PublicAPIClient() - const config = new TestConfiguration(api) + const api = new PublicAPIClient() + const accountsAPI = new AccountsAPIClient() + const internalAPI = new InternalAPIClient() + const config = new TestConfiguration
(api, accountsAPI, internalAPI) beforeAll(async () => { - await config.beforeAll() + await config.setupAccountAndTenant() + await config.setApiKey() + const [appResp, app] = await config.applications.seed() config.tables.api.appId = app._id diff --git a/qa-core/src/tests/public-api/users/users.spec.ts b/qa-core/src/tests/public-api/users/users.spec.ts index 5e68c77c50..597a8ff2dd 100644 --- a/qa-core/src/tests/public-api/users/users.spec.ts +++ b/qa-core/src/tests/public-api/users/users.spec.ts @@ -2,13 +2,18 @@ import TestConfiguration from "../../../config/public-api/TestConfiguration" import PublicAPIClient from "../../../config/public-api/TestConfiguration/PublicAPIClient" import generateUser from "../../../config/public-api/fixtures/users" import { User } from "@budibase/server/api/controllers/public/mapping/types" +import AccountsAPIClient from "../../../config/public-api/TestConfiguration/accountsAPIClient" +import InternalAPIClient from "../../../config/internal-api/TestConfiguration/InternalAPIClient" describe("Public API - /users endpoints", () => { const api = new PublicAPIClient() - const config = new TestConfiguration(api) + const accountsAPI = new AccountsAPIClient() + const internalAPI = new InternalAPIClient() + const config = new TestConfiguration(api, accountsAPI, internalAPI) beforeAll(async () => { - await config.beforeAll() + await config.setupAccountAndTenant() + await config.setApiKey() const [_, user] = await config.users.seed() config.context = user })