diff --git a/hosting/nginx.dev.conf.hbs b/hosting/nginx.dev.conf.hbs index 7bc8100e45..93a07435e5 100644 --- a/hosting/nginx.dev.conf.hbs +++ b/hosting/nginx.dev.conf.hbs @@ -58,12 +58,15 @@ http { } location ~ ^/api/(system|admin|global)/ { - proxy_pass http://worker-service; proxy_read_timeout 120s; proxy_connect_timeout 120s; proxy_send_timeout 120s; proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection ""; + + proxy_pass http://worker-service; } location /api/backups/ { @@ -78,60 +81,78 @@ http { location /api/ { proxy_read_timeout 120s; proxy_connect_timeout 120s; - proxy_send_timeout 120s; - proxy_pass http://app-service; + proxy_send_timeout 120s; proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection ""; + + proxy_pass http://app-service; } location = / { - proxy_pass http://app-service; proxy_read_timeout 120s; proxy_connect_timeout 120s; proxy_send_timeout 120s; proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection ""; + + proxy_pass http://app-service; } location /app_ { - proxy_pass http://app-service; proxy_read_timeout 120s; proxy_connect_timeout 120s; proxy_send_timeout 120s; proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection ""; + + proxy_pass http://app-service; } location /app { - proxy_pass http://app-service; proxy_read_timeout 120s; proxy_connect_timeout 120s; proxy_send_timeout 120s; proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection ""; + + proxy_pass http://app-service; } location /builder { - proxy_pass http://builder; proxy_read_timeout 120s; proxy_connect_timeout 120s; proxy_send_timeout 120s; proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection ""; + + proxy_pass http://builder; rewrite ^/builder(.*)$ /builder/$1 break; } location /builder/ { - proxy_pass http://builder; - proxy_http_version 1.1; + + proxy_set_header Host $host; proxy_set_header Connection $connection_upgrade; proxy_set_header Upgrade $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 120s; proxy_connect_timeout 120s; proxy_send_timeout 120s; + + proxy_pass http://builder; } location /vite/ { diff --git a/hosting/nginx.prod.conf.hbs b/hosting/nginx.prod.conf.hbs index aa79fd92ad..15d7c6823e 100644 --- a/hosting/nginx.prod.conf.hbs +++ b/hosting/nginx.prod.conf.hbs @@ -100,18 +100,25 @@ http { location ~ ^/(builder|app_) { proxy_http_version 1.1; + proxy_set_header Connection $connection_upgrade; proxy_set_header Upgrade $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_pass http://$apps:4002; } location ~ ^/api/(system|admin|global)/ { + proxy_set_header Host $host; + proxy_pass http://$worker:4003; } location /worker/ { + proxy_set_header Host $host; + proxy_pass http://$worker:4003; rewrite ^/worker/(.*)$ /$1 break; } @@ -139,6 +146,7 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; proxy_pass http://$apps:4002; } @@ -158,6 +166,7 @@ http { proxy_set_header Upgrade $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; proxy_pass http://$apps:4002; } diff --git a/lerna.json b/lerna.json index 6bd7f8df89..55f768ac0c 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.1.22-alpha.3", + "version": "2.1.22-alpha.6", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/package.json b/package.json index 355f93985d..af513fc8dd 100644 --- a/package.json +++ b/package.json @@ -75,8 +75,8 @@ "env:multi:disable": "lerna run env:multi:disable", "env:selfhost:enable": "lerna run env:selfhost:enable", "env:selfhost:disable": "lerna run env:selfhost:disable", - "env:localdomain:enable": "lerna run env:localdomain:enable", - "env:localdomain:disable": "lerna run env:localdomain:disable", + "env:localdomain:enable": "./scripts/localdomain.sh enable", + "env:localdomain:disable": "./scripts/localdomain.sh disable", "env:account:enable": "lerna run env:account:enable", "env:account:disable": "lerna run env:account:disable", "mode:self": "yarn env:selfhost:enable && yarn env:multi:disable && yarn env:account:disable", diff --git a/packages/backend-core/__mocks__/node-fetch.ts b/packages/backend-core/__mocks__/node-fetch.ts deleted file mode 100644 index 4c7127ee48..0000000000 --- a/packages/backend-core/__mocks__/node-fetch.ts +++ /dev/null @@ -1 +0,0 @@ -jest.mock("node-fetch", () => jest.fn()) diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json index 8305c16749..e4eee39b90 100644 --- a/packages/backend-core/package.json +++ b/packages/backend-core/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/backend-core", - "version": "2.1.22-alpha.3", + "version": "2.1.22-alpha.6", "description": "Budibase backend core libraries used in server and worker", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", @@ -20,7 +20,7 @@ "test:watch": "jest --watchAll" }, "dependencies": { - "@budibase/types": "2.1.22-alpha.3", + "@budibase/types": "2.1.22-alpha.6", "@shopify/jest-koa-mocks": "5.0.1", "@techpass/passport-openidconnect": "0.3.2", "aws-sdk": "2.1030.0", @@ -57,7 +57,7 @@ "@types/chance": "1.1.3", "@types/ioredis": "4.28.0", "@types/jest": "27.5.1", - "@types/koa": "2.0.52", + "@types/koa": "2.13.4", "@types/lodash": "4.14.180", "@types/node": "14.18.20", "@types/node-fetch": "2.6.1", @@ -69,7 +69,7 @@ "chance": "1.1.3", "ioredis-mock": "5.8.0", "jest": "28.1.1", - "koa": "2.7.0", + "koa": "2.13.4", "nodemon": "2.0.16", "pouchdb-adapter-memory": "7.2.2", "timekeeper": "2.2.0", diff --git a/packages/backend-core/src/context/deprovision.ts b/packages/backend-core/src/context/deprovision.ts index befcd48a15..6f397f60d0 100644 --- a/packages/backend-core/src/context/deprovision.ts +++ b/packages/backend-core/src/context/deprovision.ts @@ -1,8 +1,12 @@ -import { getGlobalUserParams, getAllApps } from "../db/utils" -import { doWithDB, PouchLike } from "../db" +import { + getGlobalUserParams, + getAllApps, + doWithDB, + StaticDatabases, + PouchLike, +} from "../db" import { doWithGlobalDB } from "../tenancy" -import { StaticDatabases } from "../db/constants" -import { User } from "@budibase/types" +import { App, Tenants, User } from "@budibase/types" const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name @@ -10,10 +14,8 @@ const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name async function removeTenantFromInfoDB(tenantId: string) { try { await doWithDB(PLATFORM_INFO_DB, async (infoDb: PouchLike) => { - let tenants = await infoDb.get(TENANT_DOC) - tenants.tenantIds = tenants.tenantIds.filter( - (id: string) => id !== tenantId - ) + const tenants = (await infoDb.get(TENANT_DOC)) as Tenants + tenants.tenantIds = tenants.tenantIds.filter(id => id !== tenantId) await infoDb.put(tenants) }) @@ -23,18 +25,35 @@ async function removeTenantFromInfoDB(tenantId: string) { } } +export async function removeUserFromInfoDB(dbUser: User) { + await doWithDB(PLATFORM_INFO_DB, async (infoDb: PouchLike) => { + const keys = [dbUser._id!, dbUser.email] + const userDocs = await infoDb.allDocs({ + keys, + include_docs: true, + }) + const toDelete = userDocs.rows.map((row: any) => { + return { + ...row.doc, + _deleted: true, + } + }) + await infoDb.bulkDocs(toDelete) + }) +} + async function removeUsersFromInfoDB(tenantId: string) { - return doWithGlobalDB(tenantId, async (db: PouchLike) => { + return doWithGlobalDB(tenantId, async (db: any) => { try { const allUsers = await db.allDocs( getGlobalUserParams(null, { include_docs: true, }) ) - await doWithDB(PLATFORM_INFO_DB, async (infoDb: PouchLike) => { - const allEmails = allUsers.rows.map(row => row.doc.email) + await doWithDB(PLATFORM_INFO_DB, async (infoDb: any) => { + const allEmails = allUsers.rows.map((row: any) => row.doc.email) // get the id docs - let keys = allUsers.rows.map(row => row.id) + let keys = allUsers.rows.map((row: any) => row.id) // and the email docs keys = keys.concat(allEmails) // retrieve the docs and delete them @@ -42,7 +61,7 @@ async function removeUsersFromInfoDB(tenantId: string) { keys, include_docs: true, }) - const toDelete = userDocs.rows.map(row => { + const toDelete = userDocs.rows.map((row: any) => { return { ...row.doc, _deleted: true, @@ -70,7 +89,7 @@ async function removeGlobalDB(tenantId: string) { async function removeTenantApps(tenantId: string) { try { - const apps = await getAllApps({ all: true }) + const apps = (await getAllApps({ all: true })) as App[] const destroyPromises = apps.map(app => doWithDB(app.appId, (db: PouchLike) => db.destroy()) ) @@ -81,23 +100,6 @@ async function removeTenantApps(tenantId: string) { } } -export async function removeUserFromInfoDB(dbUser: User) { - await doWithDB(PLATFORM_INFO_DB, async (infoDb: PouchLike) => { - const keys = [dbUser._id!, dbUser.email] - const userDocs = await infoDb.allDocs({ - keys, - include_docs: true, - }) - const toDelete = userDocs.rows.map(row => { - return { - ...row.doc, - _deleted: true, - } - }) - await infoDb.bulkDocs(toDelete) - }) -} - // can't live in tenancy package due to circular dependency on db/utils export async function deleteTenant(tenantId: string) { await removeTenantFromInfoDB(tenantId) diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 61997901d7..76e011735e 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -16,6 +16,7 @@ import { isDevApp, isDevAppID, getProdAppID } from "./conversions" import { APP_PREFIX } from "./constants" import * as events from "../events" import { PouchLike } from "./couch" +import { App } from "@budibase/types" export * from "./constants" export * from "./conversions" @@ -302,7 +303,12 @@ export async function getAllDbs(opts = { efficient: false }) { * * @return {Promise} returns the app information document stored in each app database. */ -export async function getAllApps({ dev, all, idsOnly, efficient }: any = {}) { +export async function getAllApps({ + dev, + all, + idsOnly, + efficient, +}: any = {}): Promise { let tenantId = getTenantId() if (!env.MULTI_TENANCY && !tenantId) { tenantId = DEFAULT_TENANT_ID @@ -374,18 +380,16 @@ export async function getAllApps({ dev, all, idsOnly, efficient }: any = {}) { * Utility function for getAllApps but filters to production apps only. */ export async function getProdAppIDs() { - return (await getAllApps({ idsOnly: true })).filter( - (id: any) => !isDevAppID(id) - ) + const apps = (await getAllApps({ idsOnly: true })) as string[] + return apps.filter((id: any) => !isDevAppID(id)) } /** * Utility function for the inverse of above. */ export async function getDevAppIDs() { - return (await getAllApps({ idsOnly: true })).filter((id: any) => - isDevAppID(id) - ) + const apps = (await getAllApps({ idsOnly: true })) as string[] + return apps.filter((id: any) => isDevAppID(id)) } export async function dbExists(dbName: any) { diff --git a/packages/backend-core/src/middleware/matchers.js b/packages/backend-core/src/middleware/matchers.ts similarity index 62% rename from packages/backend-core/src/middleware/matchers.js rename to packages/backend-core/src/middleware/matchers.ts index 3d5065c069..efbdec2dbe 100644 --- a/packages/backend-core/src/middleware/matchers.js +++ b/packages/backend-core/src/middleware/matchers.ts @@ -1,27 +1,34 @@ +import { BBContext, EndpointMatcher, RegexMatcher } from "@budibase/types" + const PARAM_REGEX = /\/:(.*?)(\/.*)?$/g -exports.buildMatcherRegex = patterns => { +export const buildMatcherRegex = ( + patterns: EndpointMatcher[] +): RegexMatcher[] => { if (!patterns) { return [] } return patterns.map(pattern => { - const isObj = typeof pattern === "object" && pattern.route - const method = isObj ? pattern.method : "GET" + let route = pattern.route + const method = pattern.method const strict = pattern.strict ? pattern.strict : false - let route = isObj ? pattern.route : pattern + // if there is a param in the route + // use a wildcard pattern const matches = route.match(PARAM_REGEX) if (matches) { for (let match of matches) { - const pattern = "/.*" + (match.endsWith("/") ? "/" : "") + const suffix = match.endsWith("/") ? "/" : "" + const pattern = "/.*" + suffix route = route.replace(match, pattern) } } + return { regex: new RegExp(route), method, strict, route } }) } -exports.matches = (ctx, options) => { +export const matches = (ctx: BBContext, options: RegexMatcher[]) => { return options.find(({ regex, method, strict, route }) => { let urlMatch if (strict) { diff --git a/packages/backend-core/src/middleware/tenancy.js b/packages/backend-core/src/middleware/tenancy.js deleted file mode 100644 index b2a96cd5e2..0000000000 --- a/packages/backend-core/src/middleware/tenancy.js +++ /dev/null @@ -1,52 +0,0 @@ -const { doInTenant, isMultiTenant, DEFAULT_TENANT_ID } = require("../tenancy") -const { buildMatcherRegex, matches } = require("./matchers") -const { Header } = require("../constants") - -const getTenantID = (ctx, opts = { allowQs: false, allowNoTenant: false }) => { - // exit early if not multi-tenant - if (!isMultiTenant()) { - return DEFAULT_TENANT_ID - } - - let tenantId - const allowQs = opts && opts.allowQs - const allowNoTenant = opts && opts.allowNoTenant - const header = ctx.request.headers[Header.TENANT_ID] - const user = ctx.user || {} - if (allowQs) { - const query = ctx.request.query || {} - tenantId = query.tenantId - } - // override query string (if allowed) by user, or header - // URL params cannot be used in a middleware, as they are - // processed later in the chain - tenantId = user.tenantId || header || tenantId - - // Set the tenantId from the subdomain - if (!tenantId) { - tenantId = ctx.subdomains && ctx.subdomains[0] - } - - if (!tenantId && !allowNoTenant) { - ctx.throw(403, "Tenant id not set") - } - - return tenantId -} - -module.exports = ( - allowQueryStringPatterns, - noTenancyPatterns, - opts = { noTenancyRequired: false } -) => { - const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) - const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) - - return async function (ctx, next) { - const allowNoTenant = - opts.noTenancyRequired || !!matches(ctx, noTenancyOptions) - const allowQs = !!matches(ctx, allowQsOptions) - const tenantId = getTenantID(ctx, { allowQs, allowNoTenant }) - return doInTenant(tenantId, next) - } -} diff --git a/packages/backend-core/src/middleware/tenancy.ts b/packages/backend-core/src/middleware/tenancy.ts new file mode 100644 index 0000000000..03dd9d11e6 --- /dev/null +++ b/packages/backend-core/src/middleware/tenancy.ts @@ -0,0 +1,37 @@ +import { doInTenant, getTenantIDFromCtx } from "../tenancy" +import { buildMatcherRegex, matches } from "./matchers" +import { Headers } from "../constants" +import { + BBContext, + EndpointMatcher, + GetTenantIdOptions, + TenantResolutionStrategy, +} from "@budibase/types" + +const tenancy = ( + allowQueryStringPatterns: EndpointMatcher[], + noTenancyPatterns: EndpointMatcher[], + opts = { noTenancyRequired: false } +) => { + const allowQsOptions = buildMatcherRegex(allowQueryStringPatterns) + const noTenancyOptions = buildMatcherRegex(noTenancyPatterns) + + return async function (ctx: BBContext, next: any) { + const allowNoTenant = + opts.noTenancyRequired || !!matches(ctx, noTenancyOptions) + const tenantOpts: GetTenantIdOptions = { + allowNoTenant, + } + + const allowQs = !!matches(ctx, allowQsOptions) + if (!allowQs) { + tenantOpts.excludeStrategies = [TenantResolutionStrategy.QUERY] + } + + const tenantId = getTenantIDFromCtx(ctx, tenantOpts) + ctx.set(Headers.TENANT_ID, tenantId as string) + return doInTenant(tenantId, next) + } +} + +export = tenancy diff --git a/packages/backend-core/src/middleware/tests/matchers.spec.ts b/packages/backend-core/src/middleware/tests/matchers.spec.ts new file mode 100644 index 0000000000..c39bbb6dd3 --- /dev/null +++ b/packages/backend-core/src/middleware/tests/matchers.spec.ts @@ -0,0 +1,134 @@ +import * as matchers from "../matchers" +import { structures } from "../../../tests" + +describe("matchers", () => { + it("matches by path and method", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(true) + }) + + it("wildcards path", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests/id/something/else" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(true) + }) + + it("doesn't wildcard path with strict", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + strict: true, + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests/id/something/else" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(false) + }) + + it("matches with param", () => { + const pattern = [ + { + route: "/api/tests/:testId", + method: "GET", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests/id" + ctx.request.method = "GET" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(true) + }) + + // TODO: Support the below behaviour + // Strict does not work when a param is present + // it("matches with param with strict", () => { + // const pattern = [{ + // route: "/api/tests/:testId", + // method: "GET", + // strict: true + // }] + // const ctx = structures.koa.newContext() + // ctx.request.url = "/api/tests/id" + // ctx.request.method = "GET" + // + // const built = matchers.buildMatcherRegex(pattern) + // + // expect(!!matchers.matches(ctx, built)).toBe(true) + // }) + + it("doesn't match by path", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/unknown" + ctx.request.method = "POST" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(false) + }) + + it("doesn't match by method", () => { + const pattern = [ + { + route: "/api/tests", + method: "POST", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests" + ctx.request.method = "GET" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(false) + }) + + it("matches by path and wildcard method", () => { + const pattern = [ + { + route: "/api/tests", + method: "ALL", + }, + ] + const ctx = structures.koa.newContext() + ctx.request.url = "/api/tests" + ctx.request.method = "GET" + + const built = matchers.buildMatcherRegex(pattern) + + expect(!!matchers.matches(ctx, built)).toBe(true) + }) +}) diff --git a/packages/backend-core/src/migrations/migrations.ts b/packages/backend-core/src/migrations/migrations.ts index 2170c5983a..60c17f4020 100644 --- a/packages/backend-core/src/migrations/migrations.ts +++ b/packages/backend-core/src/migrations/migrations.ts @@ -12,6 +12,7 @@ import { MigrationOptions, MigrationType, MigrationNoOpOptions, + App, } from "@budibase/types" export const getMigrationsDoc = async (db: any) => { @@ -55,14 +56,17 @@ export const runMigration = async ( } // get the db to store the migration in - let dbNames + let dbNames: string[] if (migrationType === MigrationType.GLOBAL) { dbNames = [getGlobalDBName()] } else if (migrationType === MigrationType.APP) { if (options.noOp) { + if (!options.noOp.appId) { + throw new Error("appId is required for noOp app migration") + } dbNames = [options.noOp.appId] } else { - const apps = await getAllApps(migration.appOpts) + const apps = (await getAllApps(migration.appOpts)) as App[] dbNames = apps.map(app => app.appId) } } else if (migrationType === MigrationType.INSTALLATION) { diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts index 55ac66b95c..0bbd423fe5 100644 --- a/packages/backend-core/src/tenancy/tenancy.ts +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -9,7 +9,13 @@ import { isMultiTenant, } from "../context" import env from "../environment" -import { PlatformUser } from "@budibase/types" +import { + BBContext, + PlatformUser, + TenantResolutionStrategy, + GetTenantIdOptions, +} from "@budibase/types" +import { Headers } from "../constants" const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name @@ -151,3 +157,108 @@ export async function getTenantIds() { return (tenants && tenants.tenantIds) || [] }) } + +const ALL_STRATEGIES = Object.values(TenantResolutionStrategy) + +export const getTenantIDFromCtx = ( + ctx: BBContext, + opts: GetTenantIdOptions +): string | null => { + // exit early if not multi-tenant + if (!isMultiTenant()) { + return DEFAULT_TENANT_ID + } + + // opt defaults + if (opts.allowNoTenant === undefined) { + opts.allowNoTenant = false + } + if (!opts.includeStrategies) { + opts.includeStrategies = ALL_STRATEGIES + } + if (!opts.excludeStrategies) { + opts.excludeStrategies = [] + } + + const isAllowed = (strategy: TenantResolutionStrategy) => { + // excluded takes precedence + if (opts.excludeStrategies?.includes(strategy)) { + return false + } + if (opts.includeStrategies?.includes(strategy)) { + return true + } + } + + // always use user first + if (isAllowed(TenantResolutionStrategy.USER)) { + const userTenantId = ctx.user?.tenantId + if (userTenantId) { + return userTenantId + } + } + + // header + if (isAllowed(TenantResolutionStrategy.HEADER)) { + const headerTenantId = ctx.request.headers[Headers.TENANT_ID] + if (headerTenantId) { + return headerTenantId as string + } + } + + // query param + if (isAllowed(TenantResolutionStrategy.QUERY)) { + const queryTenantId = ctx.request.query.tenantId + if (queryTenantId) { + return queryTenantId as string + } + } + + // subdomain + if (isAllowed(TenantResolutionStrategy.SUBDOMAIN)) { + // e.g. budibase.app or local.com:10000 + const platformHost = new URL(env.PLATFORM_URL).host.split(":")[0] + // e.g. tenant.budibase.app or tenant.local.com + const requestHost = ctx.host + // parse the tenant id from the difference + if (requestHost.includes(platformHost)) { + const tenantId = requestHost.substring( + 0, + requestHost.indexOf(`.${platformHost}`) + ) + if (tenantId) { + return tenantId + } + } + } + + // path + if (isAllowed(TenantResolutionStrategy.PATH)) { + // params - have to parse manually due to koa-router not run yet + const match = ctx.matched.find( + (m: any) => !!m.paramNames.find((p: any) => p.name === "tenantId") + ) + + // get the raw path url - without any query params + const ctxUrl = ctx.originalUrl + let url + if (ctxUrl.includes("?")) { + url = ctxUrl.split("?")[0] + } else { + url = ctxUrl + } + + if (match) { + const params = match.params(url, match.captures(url), {}) + if (params.tenantId) { + return params.tenantId + } + } + } + + if (!opts.allowNoTenant) { + ctx.throw(403, "Tenant id not set") + } + + return null +} diff --git a/packages/backend-core/src/utils.js b/packages/backend-core/src/utils.ts similarity index 66% rename from packages/backend-core/src/utils.js rename to packages/backend-core/src/utils.ts index ed8f21d5c2..c04d6196b3 100644 --- a/packages/backend-core/src/utils.js +++ b/packages/backend-core/src/utils.ts @@ -1,38 +1,51 @@ -const { DocumentType, SEPARATOR, ViewName, getAllApps } = require("./db/utils") +import { + DocumentType, + SEPARATOR, + ViewName, + getAllApps, + queryGlobalView, +} from "./db" +import { options } from "./middleware/passport/jwt" +import { Header, Cookie, MAX_VALID_DATE } from "./constants" +import env from "./environment" +import userCache from "./cache/user" +import { getSessionsForUser, invalidateSessions } from "./security/sessions" +import * as events from "./events" +import tenancy from "./tenancy" +import { + App, + BBContext, + PlatformLogoutOpts, + TenantResolutionStrategy, +} from "@budibase/types" +import { SetOption } from "cookies" const jwt = require("jsonwebtoken") -const { options } = require("./middleware/passport/jwt") -const { queryGlobalView } = require("./db/views") -const { Header, Cookie, MAX_VALID_DATE } = require("./constants") -const env = require("./environment") -const userCache = require("./cache/user") -const { - getSessionsForUser, - invalidateSessions, -} = require("./security/sessions") -const events = require("./events") -const tenancy = require("./tenancy") const APP_PREFIX = DocumentType.APP + SEPARATOR const PROD_APP_PREFIX = "/app/" -function confirmAppId(possibleAppId) { +function confirmAppId(possibleAppId: string | undefined) { return possibleAppId && possibleAppId.startsWith(APP_PREFIX) ? possibleAppId : undefined } -async function resolveAppUrl(ctx) { +async function resolveAppUrl(ctx: BBContext) { const appUrl = ctx.path.split("/")[2] let possibleAppUrl = `/${appUrl.toLowerCase()}` - let tenantId = tenancy.getTenantId() - if (!env.SELF_HOSTED && ctx.subdomains.length) { - // always use the tenant id from the url in cloud - tenantId = ctx.subdomains[0] + let tenantId: string | null = tenancy.getTenantId() + if (env.MULTI_TENANCY) { + // always use the tenant id from the subdomain in multi tenancy + // this ensures the logged-in user tenant id doesn't overwrite + // e.g. in the case of viewing a public app while already logged-in to another tenant + tenantId = tenancy.getTenantIDFromCtx(ctx, { + includeStrategies: [TenantResolutionStrategy.SUBDOMAIN], + }) } // search prod apps for a url that matches - const apps = await tenancy.doInTenant(tenantId, () => + const apps: App[] = await tenancy.doInTenant(tenantId, () => getAllApps({ dev: false }) ) const app = apps.filter( @@ -42,7 +55,7 @@ async function resolveAppUrl(ctx) { return app && app.appId ? app.appId : undefined } -exports.isServingApp = ctx => { +export function isServingApp(ctx: BBContext) { // dev app if (ctx.path.startsWith(`/${APP_PREFIX}`)) { return true @@ -59,12 +72,12 @@ exports.isServingApp = ctx => { * @param {object} ctx The main request body to look through. * @returns {string|undefined} If an appId was found it will be returned. */ -exports.getAppIdFromCtx = async ctx => { +export async function getAppIdFromCtx(ctx: BBContext) { // look in headers const options = [ctx.headers[Header.APP_ID]] let appId for (let option of options) { - appId = confirmAppId(option) + appId = confirmAppId(option as string) if (appId) { break } @@ -95,7 +108,7 @@ exports.getAppIdFromCtx = async ctx => { * opens the contents of the specified encrypted JWT. * @return {object} the contents of the token. */ -exports.openJwt = token => { +export function openJwt(token: string) { if (!token) { return token } @@ -107,14 +120,14 @@ exports.openJwt = token => { * @param {object} ctx The request which is to be manipulated. * @param {string} name The name of the cookie to get. */ -exports.getCookie = (ctx, name) => { +export function getCookie(ctx: BBContext, name: string) { const cookie = ctx.cookies.get(name) if (!cookie) { return cookie } - return exports.openJwt(cookie) + return openJwt(cookie) } /** @@ -124,12 +137,17 @@ exports.getCookie = (ctx, name) => { * @param {string|object} value The value of cookie which will be set. * @param {object} opts options like whether to sign. */ -exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => { +export function setCookie( + ctx: BBContext, + value: any, + name = "builder", + opts = { sign: true } +) { if (value && opts && opts.sign) { value = jwt.sign(value, options.secretOrKey) } - const config = { + const config: SetOption = { expires: MAX_VALID_DATE, path: "/", httpOnly: false, @@ -146,8 +164,8 @@ exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => { /** * Utility function, simply calls setCookie with an empty string for value */ -exports.clearCookie = (ctx, name) => { - exports.setCookie(ctx, null, name) +export function clearCookie(ctx: BBContext, name: string) { + setCookie(ctx, null, name) } /** @@ -156,11 +174,11 @@ exports.clearCookie = (ctx, name) => { * @param {object} ctx The koa context object to be tested. * @return {boolean} returns true if the call is from the client lib (a built app rather than the builder). */ -exports.isClient = ctx => { +export function isClient(ctx: BBContext) { return ctx.headers[Header.TYPE] === "client" } -const getBuilders = async () => { +async function getBuilders() { const builders = await queryGlobalView(ViewName.USER_BY_BUILDERS, { include_docs: false, }) @@ -176,7 +194,7 @@ const getBuilders = async () => { } } -exports.getBuildersCount = async () => { +export async function getBuildersCount() { const builders = await getBuilders() return builders.length } @@ -184,10 +202,14 @@ exports.getBuildersCount = async () => { /** * Logs a user out from budibase. Re-used across account portal and builder. */ -exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => { +export async function platformLogout(opts: PlatformLogoutOpts) { + const ctx = opts.ctx + const userId = opts.userId + const keepActiveSession = opts.keepActiveSession + if (!ctx) throw new Error("Koa context must be supplied to logout.") - const currentSession = exports.getCookie(ctx, Cookie.Auth) + const currentSession = getCookie(ctx, Cookie.Auth) let sessions = await getSessionsForUser(userId) if (keepActiveSession) { @@ -196,8 +218,8 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => { ) } else { // clear cookies - exports.clearCookie(ctx, Cookie.Auth) - exports.clearCookie(ctx, Cookie.CurrentApp) + clearCookie(ctx, Cookie.Auth) + clearCookie(ctx, Cookie.CurrentApp) } const sessionIds = sessions.map(({ sessionId }) => sessionId) @@ -206,6 +228,6 @@ exports.platformLogout = async ({ ctx, userId, keepActiveSession }) => { await userCache.invalidateUser(userId) } -exports.timeout = timeMs => { +export function timeout(timeMs: number) { return new Promise(resolve => setTimeout(resolve, timeMs)) } diff --git a/packages/backend-core/tests/jestSetup.ts b/packages/backend-core/tests/jestSetup.ts index 30b645d0e5..7870a721aa 100644 --- a/packages/backend-core/tests/jestSetup.ts +++ b/packages/backend-core/tests/jestSetup.ts @@ -1,6 +1,9 @@ import env from "../src/environment" import { mocks } from "./utilities" +// must explicitly enable fetch mock +mocks.fetch.enable() + // mock all dates to 2020-01-01T00:00:00.000Z // use tk.reset() to use real dates in individual tests import tk from "timekeeper" diff --git a/packages/backend-core/tests/utilities/index.ts b/packages/backend-core/tests/utilities/index.ts index 9a2222b1bc..65578ff013 100644 --- a/packages/backend-core/tests/utilities/index.ts +++ b/packages/backend-core/tests/utilities/index.ts @@ -1,5 +1,6 @@ export * as mocks from "./mocks" export * as structures from "./structures" +export { generator } from "./structures" import * as dbConfig from "./db" dbConfig.init() diff --git a/packages/backend-core/tests/utilities/mocks/accounts.ts b/packages/backend-core/tests/utilities/mocks/accounts.ts index 79436443db..cb4c68b65e 100644 --- a/packages/backend-core/tests/utilities/mocks/accounts.ts +++ b/packages/backend-core/tests/utilities/mocks/accounts.ts @@ -1,7 +1,9 @@ export const getAccount = jest.fn() export const getAccountByTenantId = jest.fn() +export const getStatus = jest.fn() jest.mock("../../../src/cloud/accounts", () => ({ getAccount, getAccountByTenantId, + getStatus, })) diff --git a/packages/backend-core/tests/utilities/mocks/fetch.ts b/packages/backend-core/tests/utilities/mocks/fetch.ts new file mode 100644 index 0000000000..eeb0ccda45 --- /dev/null +++ b/packages/backend-core/tests/utilities/mocks/fetch.ts @@ -0,0 +1,10 @@ +const mockFetch = jest.fn() + +const enable = () => { + jest.mock("node-fetch", () => mockFetch) +} + +export default { + ...mockFetch, + enable, +} diff --git a/packages/backend-core/tests/utilities/mocks/index.ts b/packages/backend-core/tests/utilities/mocks/index.ts index 7031b225ec..e71c739e26 100644 --- a/packages/backend-core/tests/utilities/mocks/index.ts +++ b/packages/backend-core/tests/utilities/mocks/index.ts @@ -2,3 +2,4 @@ import "./posthog" import "./events" export * as accounts from "./accounts" export * as date from "./date" +export { default as fetch } from "./fetch" diff --git a/packages/backend-core/tests/utilities/structures/accounts.ts b/packages/backend-core/tests/utilities/structures/accounts.ts index 5d23962575..f1718aecc0 100644 --- a/packages/backend-core/tests/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/utilities/structures/accounts.ts @@ -1,23 +1,29 @@ import { generator, uuid } from "." -import { AuthType, CloudAccount, Hosting } from "@budibase/types" import * as db from "../../../src/db/utils" +import { Account, AuthType, CloudAccount, Hosting } from "@budibase/types" -export const cloudAccount = (): CloudAccount => { +export const account = (): Account => { return { accountId: uuid(), + tenantId: generator.word(), + email: generator.email(), + tenantName: generator.word(), + hosting: Hosting.SELF, createdAt: Date.now(), verified: true, verificationSent: true, - tier: "", - email: generator.email(), - tenantId: generator.word(), - hosting: Hosting.CLOUD, + tier: "FREE", // DEPRECATED authType: AuthType.PASSWORD, - password: generator.word(), - tenantName: generator.word(), name: generator.name(), size: "10+", profession: "Software Engineer", + } +} + +export const cloudAccount = (): CloudAccount => { + return { + ...account(), + hosting: Hosting.CLOUD, budibaseUserId: db.generateGlobalUserID(), } } diff --git a/packages/backend-core/tests/utilities/structures/common.ts b/packages/backend-core/tests/utilities/structures/common.ts index 51ae220254..05b879f36b 100644 --- a/packages/backend-core/tests/utilities/structures/common.ts +++ b/packages/backend-core/tests/utilities/structures/common.ts @@ -1 +1,7 @@ +import { v4 as uuid } from "uuid" + export { v4 as uuid } from "uuid" + +export const email = () => { + return `${uuid()}@test.com` +} diff --git a/packages/backend-core/tests/utilities/structures/koa.ts b/packages/backend-core/tests/utilities/structures/koa.ts index 6f0f7866e6..a33dca1546 100644 --- a/packages/backend-core/tests/utilities/structures/koa.ts +++ b/packages/backend-core/tests/utilities/structures/koa.ts @@ -1,5 +1,14 @@ -import { createMockContext } from "@shopify/jest-koa-mocks" +import { createMockContext, createMockCookies } from "@shopify/jest-koa-mocks" +import { BBContext } from "@budibase/types" -export const newContext = () => { - return createMockContext() +export const newContext = (): BBContext => { + const ctx = createMockContext() + return { + ...ctx, + cookies: createMockCookies(), + request: { + ...ctx.request, + body: {}, + }, + } } diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock index 42614b4b3b..6ba9f7b5ae 100644 --- a/packages/backend-core/yarn.lock +++ b/packages/backend-core/yarn.lock @@ -1079,7 +1079,7 @@ dependencies: "@types/koa" "*" -"@types/koa@*": +"@types/koa@*", "@types/koa@2.13.4": version "2.13.4" resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.13.4.tgz#10620b3f24a8027ef5cbae88b393d1b31205726b" integrity sha512-dfHYMfU+z/vKtQB7NUrthdAEiSvnLebvBjwHtfFmpZmB7em2N3WVQdHgnFq+xvyVgxW5jKDmjWfLD3lw4g4uTw== @@ -1093,18 +1093,6 @@ "@types/koa-compose" "*" "@types/node" "*" -"@types/koa@2.0.52": - version "2.0.52" - resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.0.52.tgz#7dd11de4189ab339ad66c4ccad153716b14e525f" - integrity sha512-cp/GTOhOYwomlSKqEoG0kaVEVJEzP4ojYmfa7EKaGkmkkRwJ4B/1VBLbQZ49Z+WJNvzXejQB/9GIKqMo9XLgFQ== - dependencies: - "@types/accepts" "*" - "@types/cookies" "*" - "@types/http-assert" "*" - "@types/keygrip" "*" - "@types/koa-compose" "*" - "@types/node" "*" - "@types/lodash@4.14.180": version "4.14.180" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" @@ -1483,11 +1471,6 @@ ansi-styles@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== -any-promise@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" - integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== - anymatch@^3.0.3, anymatch@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" @@ -2078,14 +2061,6 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" -cookies@~0.7.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.3.tgz#7912ce21fbf2e8c2da70cf1c3f351aecf59dadfa" - integrity sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A== - dependencies: - depd "~1.1.2" - keygrip "~1.0.3" - cookies@~0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" @@ -2156,13 +2131,6 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - debuglog@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -2223,7 +2191,7 @@ denque@^1.1.0: resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== -depd@^1.1.0, depd@^1.1.2, depd@~1.1.2: +depd@^1.1.0, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== @@ -2375,11 +2343,6 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -error-inject@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" - integrity sha512-JM8N6PytDbmIYm1IhPWlo8vr3NtfjhDY/1MhD/a5b/aad/USE8a0+NsqE9d5n+GVGmuNkPQWm4bFQWv18d8tMg== - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -3654,11 +3617,6 @@ jws@^3.0.0, jws@^3.1.4, jws@^3.2.2: jwa "^1.4.1" safe-buffer "^5.0.1" -keygrip@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc" - integrity sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g== - keygrip@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" @@ -3678,26 +3636,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -koa-compose@^3.0.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" - integrity sha512-8gen2cvKHIZ35eDEik5WOo8zbVp9t4cP8p4hW4uE55waxolLRexKKrqfCpwhGVppnB40jWeF8bZeTVg99eZgPw== - dependencies: - any-promise "^1.1.0" - koa-compose@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== -koa-convert@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" - integrity sha512-K9XqjmEDStGX09v3oxR7t5uPRy0jqJdvodHa6wxWTHrTfDq0WUNnYTOOUZN6g8OM8oZQXprQASbiIXG2Ez8ehA== - dependencies: - co "^4.6.0" - koa-compose "^3.0.0" - koa-convert@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-2.0.0.tgz#86a0c44d81d40551bae22fee6709904573eea4f5" @@ -3706,11 +3649,6 @@ koa-convert@^2.0.0: co "^4.6.0" koa-compose "^4.1.0" -koa-is-json@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" - integrity sha512-+97CtHAlWDx0ndt0J8y3P12EWLwTLMXIfMnYDev3wOTwH/RpBGMlfn4bDXlMEg1u73K6XRE9BbUp+5ZAYoRYWw== - koa-passport@4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/koa-passport/-/koa-passport-4.1.4.tgz#5f1665c1c2a37ace79af9f970b770885ca30ccfa" @@ -3718,37 +3656,7 @@ koa-passport@4.1.4: dependencies: passport "^0.4.0" -koa@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.7.0.tgz#7e00843506942b9d82c6cc33749f657c6e5e7adf" - integrity sha512-7ojD05s2Q+hFudF8tDLZ1CpCdVZw8JQELWSkcfG9bdtoTDzMmkRF6BQBU7JzIzCCOY3xd3tftiy/loHBUYaY2Q== - dependencies: - accepts "^1.3.5" - cache-content-type "^1.0.0" - content-disposition "~0.5.2" - content-type "^1.0.4" - cookies "~0.7.1" - debug "~3.1.0" - delegates "^1.0.0" - depd "^1.1.2" - destroy "^1.0.4" - error-inject "^1.0.0" - escape-html "^1.0.3" - fresh "~0.5.2" - http-assert "^1.3.0" - http-errors "^1.6.3" - is-generator-function "^1.0.7" - koa-compose "^4.1.0" - koa-convert "^1.2.0" - koa-is-json "^1.0.0" - on-finished "^2.3.0" - only "~0.0.2" - parseurl "^1.3.2" - statuses "^1.5.0" - type-is "^1.6.16" - vary "^1.1.2" - -koa@^2.13.4: +koa@2.13.4, koa@^2.13.4: version "2.13.4" resolved "https://registry.yarnpkg.com/koa/-/koa-2.13.4.tgz#ee5b0cb39e0b8069c38d115139c774833d32462e" integrity sha512-43zkIKubNbnrULWlHdN5h1g3SEKXOEzoAlRsHOTFpnlDu8JlAOZSMJBLULusuXRequboiwJcj5vtYXKB3k7+2g== @@ -4131,11 +4039,6 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" diff --git a/packages/bbui/package.json b/packages/bbui/package.json index 767ccea8b0..169d51a6ca 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.1.22-alpha.3", + "version": "2.1.22-alpha.6", "license": "MPL-2.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", @@ -38,7 +38,7 @@ ], "dependencies": { "@adobe/spectrum-css-workflow-icons": "^1.2.1", - "@budibase/string-templates": "2.1.22-alpha.3", + "@budibase/string-templates": "2.1.22-alpha.6", "@spectrum-css/actionbutton": "^1.0.1", "@spectrum-css/actiongroup": "^1.0.1", "@spectrum-css/avatar": "^3.0.2", diff --git a/packages/builder/cypress/integration/createApp.spec.js b/packages/builder/cypress/integration/createApp.spec.js index 179741e21a..d37b0806c4 100644 --- a/packages/builder/cypress/integration/createApp.spec.js +++ b/packages/builder/cypress/integration/createApp.spec.js @@ -10,7 +10,7 @@ filterTests(['smoke', 'all'], () => { }) if (!(Cypress.env("TEST_ENV"))) { - it("should show the new user UI/UX", () => { + it.skip("should show the new user UI/UX", () => { cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`, { timeout: 5000 }) //added /portal/apps/create cy.wait(1000) cy.get(interact.CREATE_APP_BUTTON, { timeout: 10000 }).contains('Start from scratch').should("exist") @@ -83,7 +83,7 @@ filterTests(['smoke', 'all'], () => { }) }) - it("should create the first application from scratch", () => { + it.skip("should create the first application from scratch", () => { const appName = "Cypress Tests" cy.createApp(appName, false) @@ -93,7 +93,7 @@ filterTests(['smoke', 'all'], () => { cy.deleteApp(appName) }) - it("should create the first application from scratch with a default name", () => { + it.skip("should create the first application from scratch with a default name", () => { cy.updateUserInformation("", "") cy.createApp("", false) cy.applicationInAppTable("My app") diff --git a/packages/builder/cypress/integration/createAutomation.spec.js b/packages/builder/cypress/integration/createAutomation.spec.js index b5ff406297..8c16f4bd22 100644 --- a/packages/builder/cypress/integration/createAutomation.spec.js +++ b/packages/builder/cypress/integration/createAutomation.spec.js @@ -12,7 +12,7 @@ filterTests(['smoke', 'all'], () => { cy.createTestTableWithData() cy.wait(2000) cy.contains("Automate").click() - cy.get(interact.ADD_BUTTON_SPECTRUM).click() + cy.get(interact.SPECTRUM_BUTTON_TEMPLATE).contains("Add automation").click({ force: true }) cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.get("input").type("Add Row") cy.contains("Row Created").click({ force: true }) @@ -24,7 +24,7 @@ filterTests(['smoke', 'all'], () => { cy.wait(500) cy.contains("dog").click() // Create action - cy.get('[aria-label="AddCircle"]', { timeout: 2000 }).eq(1).click() + cy.get('[aria-label="AddCircle"]', { timeout: 2000 }).click() cy.get(interact.MODAL_INNER_WRAPPER).within(() => { cy.wait(1000) cy.contains("Create Row").trigger('mouseover').click().click() diff --git a/packages/builder/cypress/integration/createScreen.spec.js b/packages/builder/cypress/integration/createScreen.spec.js index a516e279f4..c4b237279d 100644 --- a/packages/builder/cypress/integration/createScreen.spec.js +++ b/packages/builder/cypress/integration/createScreen.spec.js @@ -9,7 +9,7 @@ filterTests(["smoke", "all"], () => { cy.navigateToFrontend() }) - it("Should successfully create a screen", () => { + it.skip("Should successfully create a screen", () => { cy.createScreen("test") cy.get(interact.BODY).within(() => { cy.contains("/test").should("exist") @@ -23,7 +23,7 @@ filterTests(["smoke", "all"], () => { }) }) - it("should delete all screens then create first screen via button", () => { + it.skip("should delete all screens then create first screen via button", () => { cy.deleteAllScreens() cy.contains("Create first screen").click() diff --git a/packages/builder/cypress/integration/revertApp.spec.js b/packages/builder/cypress/integration/revertApp.spec.js index 0fb58e89e9..2cd806b02c 100644 --- a/packages/builder/cypress/integration/revertApp.spec.js +++ b/packages/builder/cypress/integration/revertApp.spec.js @@ -2,7 +2,7 @@ import filterTests from "../support/filterTests" const interact = require('../support/interact') filterTests(['smoke', 'all'], () => { - context("Revert apps", () => { + xcontext("Revert apps", () => { before(() => { cy.login() cy.createTestApp() diff --git a/packages/builder/package.json b/packages/builder/package.json index 0e339a0363..bcda888beb 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "2.1.22-alpha.3", + "version": "2.1.22-alpha.6", "license": "GPL-3.0", "private": true, "scripts": { @@ -71,10 +71,10 @@ } }, "dependencies": { - "@budibase/bbui": "2.1.22-alpha.3", - "@budibase/client": "2.1.22-alpha.3", - "@budibase/frontend-core": "2.1.22-alpha.3", - "@budibase/string-templates": "2.1.22-alpha.3", + "@budibase/bbui": "2.1.22-alpha.6", + "@budibase/client": "2.1.22-alpha.6", + "@budibase/frontend-core": "2.1.22-alpha.6", + "@budibase/string-templates": "2.1.22-alpha.6", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte index dab0bfdd90..116fdeff28 100644 --- a/packages/builder/src/pages/builder/portal/manage/auth/index.svelte +++ b/packages/builder/src/pages/builder/portal/manage/auth/index.svelte @@ -20,11 +20,12 @@ Toggle, Tag, Tags, + Icon, + Helpers, } from "@budibase/bbui" import { onMount } from "svelte" import { API } from "api" import { organisation, admin } from "stores/portal" - import { Helpers } from "@budibase/bbui" const ConfigTypes = { Google: "google", @@ -40,7 +41,9 @@ // Indicate to user that callback is based on platform url // If there is an existing value, indicate that it may be removed to return to default behaviour - $: googleCallbackTooltip = googleCallbackReadonly + $: googleCallbackTooltip = $admin.cloud + ? null + : googleCallbackReadonly ? "Vist the organisation page to update the platform URL" : "Leave blank to use the default callback URL" @@ -54,6 +57,7 @@ readonly: googleCallbackReadonly, tooltip: googleCallbackTooltip, placeholder: $organisation.googleCallbackUrl, + copyButton: true, }, ], } @@ -66,9 +70,12 @@ { name: "callbackURL", readonly: true, - tooltip: "Vist the organisation page to update the platform URL", + tooltip: $admin.cloud + ? null + : "Vist the organisation page to update the platform URL", label: "Callback URL", placeholder: $organisation.oidcCallbackUrl, + copyButton: true, }, ], } @@ -231,6 +238,11 @@ }, ] + const copyToClipboard = async value => { + await Helpers.copyToClipboard(value) + notifications.success("Copied") + } + onMount(async () => { try { await organisation.init() @@ -336,11 +348,23 @@ {#each GoogleConfigFields.Google as field}
- +
+
+ +
+ {#if field.copyButton} +
copyToClipboard(field.placeholder)} + > + +
+ {/if} +
{/each}
@@ -375,12 +399,23 @@ {#each OIDCConfigFields.Oidc as field}
- +
+
+ +
+ {#if field.copyButton} +
copyToClipboard(field.placeholder)} + > + +
+ {/if} +
{/each} @@ -557,4 +592,16 @@ .provider-title span { flex: 1 1 auto; } + .inputContainer { + display: flex; + flex-direction: row; + } + .input { + flex: 1; + } + .copy { + display: flex; + align-items: center; + margin-left: 10px; + } diff --git a/packages/cli/package.json b/packages/cli/package.json index 066ee70e87..b63744ac38 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "2.1.22-alpha.3", + "version": "2.1.22-alpha.6", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { @@ -26,9 +26,9 @@ "outputPath": "build" }, "dependencies": { - "@budibase/backend-core": "2.1.22-alpha.3", - "@budibase/string-templates": "2.1.22-alpha.3", - "@budibase/types": "2.1.22-alpha.3", + "@budibase/backend-core": "2.1.22-alpha.6", + "@budibase/string-templates": "2.1.22-alpha.6", + "@budibase/types": "2.1.22-alpha.6", "axios": "0.21.2", "chalk": "4.1.0", "cli-progress": "3.11.2", diff --git a/packages/client/package.json b/packages/client/package.json index be965071cd..eaa2eaac67 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "2.1.22-alpha.3", + "version": "2.1.22-alpha.6", "license": "MPL-2.0", "module": "dist/budibase-client.js", "main": "dist/budibase-client.js", @@ -19,9 +19,9 @@ "dev:builder": "rollup -cw" }, "dependencies": { - "@budibase/bbui": "2.1.22-alpha.3", - "@budibase/frontend-core": "2.1.22-alpha.3", - "@budibase/string-templates": "2.1.22-alpha.3", + "@budibase/bbui": "2.1.22-alpha.6", + "@budibase/frontend-core": "2.1.22-alpha.6", + "@budibase/string-templates": "2.1.22-alpha.6", "@spectrum-css/button": "^3.0.3", "@spectrum-css/card": "^3.0.3", "@spectrum-css/divider": "^1.0.3", diff --git a/packages/frontend-core/package.json b/packages/frontend-core/package.json index 8017e5b9d5..21d0e27230 100644 --- a/packages/frontend-core/package.json +++ b/packages/frontend-core/package.json @@ -1,12 +1,12 @@ { "name": "@budibase/frontend-core", - "version": "2.1.22-alpha.3", + "version": "2.1.22-alpha.6", "description": "Budibase frontend core libraries used in builder and client", "author": "Budibase", "license": "MPL-2.0", "svelte": "src/index.js", "dependencies": { - "@budibase/bbui": "2.1.22-alpha.3", + "@budibase/bbui": "2.1.22-alpha.6", "lodash": "^4.17.21", "svelte": "^3.46.2" } diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 75a7c5fc4d..56c609c303 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/sdk", - "version": "2.1.22-alpha.3", + "version": "2.1.22-alpha.6", "description": "Budibase Public API SDK", "author": "Budibase", "license": "MPL-2.0", diff --git a/packages/server/package.json b/packages/server/package.json index 8d16598dea..188811523a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "2.1.22-alpha.3", + "version": "2.1.22-alpha.6", "description": "Budibase Web Server", "main": "src/index.ts", "repository": { @@ -43,11 +43,11 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/swagger-parser": "10.0.3", - "@budibase/backend-core": "2.1.22-alpha.3", - "@budibase/client": "2.1.22-alpha.3", - "@budibase/pro": "2.1.22-alpha.3", - "@budibase/string-templates": "2.1.22-alpha.3", - "@budibase/types": "2.1.22-alpha.3", + "@budibase/backend-core": "2.1.22-alpha.6", + "@budibase/client": "2.1.22-alpha.6", + "@budibase/pro": "2.1.22-alpha.6", + "@budibase/string-templates": "2.1.22-alpha.6", + "@budibase/types": "2.1.22-alpha.6", "@bull-board/api": "3.7.0", "@bull-board/koa": "3.9.4", "@elastic/elasticsearch": "7.10.0", diff --git a/packages/server/scripts/localdomain.js b/packages/server/scripts/localdomain.js index 7dd1c083b4..9317538e9f 100644 --- a/packages/server/scripts/localdomain.js +++ b/packages/server/scripts/localdomain.js @@ -2,6 +2,36 @@ const updateDotEnv = require("update-dotenv") const arg = process.argv.slice(2)[0] +const isEnable = arg === "enable" + +let domain = process.argv.slice(2)[1] +if (!domain) { + domain = "local.com" +} + +const getAccountPortalUrl = () => { + if (isEnable) { + return `http://account.${domain}:10001` + } else { + return `http://localhost:10001` + } +} + +const getBudibaseUrl = () => { + if (isEnable) { + return `http://${domain}:10000` + } else { + return `http://localhost:10000` + } +} + +const getCookieDomain = () => { + if (isEnable) { + return `.${domain}` + } else { + return "" + } +} /** * For testing multi tenancy sub domains locally. @@ -16,9 +46,7 @@ const arg = process.argv.slice(2)[0] * 127.0.0.1 t2.local.com */ updateDotEnv({ - ACCOUNT_PORTAL_URL: - arg === "enable" - ? "http://account.local.com:10001" - : "http://localhost:10001", - COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "", -}).then(() => console.log("Updated worker!")) + ACCOUNT_PORTAL_URL: getAccountPortalUrl(), + COOKIE_DOMAIN: getCookieDomain(), + PLATFORM_URL: getBudibaseUrl(), +}).then(() => console.log("Updated server!")) diff --git a/packages/server/src/migrations/functions/backfill/global.ts b/packages/server/src/migrations/functions/backfill/global.ts index d7be61c130..e3a96b77dc 100644 --- a/packages/server/src/migrations/functions/backfill/global.ts +++ b/packages/server/src/migrations/functions/backfill/global.ts @@ -149,7 +149,7 @@ export const run = async (db: any) => { } try { - const allApps: App[] = await dbUtils.getAllApps({ dev: true }) + const allApps = (await dbUtils.getAllApps({ dev: true })) as App[] totals.apps = allApps.length totals.usage = await quotas.backfill(allApps) diff --git a/packages/server/src/migrations/functions/usageQuotas/syncRows.ts b/packages/server/src/migrations/functions/usageQuotas/syncRows.ts index 0b123d2357..e5c8a1743c 100644 --- a/packages/server/src/migrations/functions/usageQuotas/syncRows.ts +++ b/packages/server/src/migrations/functions/usageQuotas/syncRows.ts @@ -2,11 +2,11 @@ import { getTenantId } from "@budibase/backend-core/tenancy" import { getAllApps } from "@budibase/backend-core/db" import { getUniqueRows } from "../../../utilities/usageQuota/rows" import { quotas } from "@budibase/pro" -import { StaticQuotaName, QuotaUsageType } from "@budibase/types" +import { StaticQuotaName, QuotaUsageType, App } from "@budibase/types" export const run = async () => { // get all rows in all apps - const allApps = await getAllApps({ all: true }) + const allApps = (await getAllApps({ all: true })) as App[] const appIds = allApps ? allApps.map((app: { appId: any }) => app.appId) : [] const { appRows } = await getUniqueRows(appIds) diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index 6a945c226e..2e54c41cf5 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -1273,12 +1273,12 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.1.22-alpha.3": - version "2.1.22-alpha.3" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.1.22-alpha.3.tgz#5cdff781cf6a448677304aa80a9658e9e51c36a5" - integrity sha512-je1mKTb1h9f+tyCAiFNJykg9O8ZM3smVQ3JJf24rXDYvLYN8GG8hxZ5fOJuWoeF/aGu8m+w3v3+EdWuTlQPcKg== +"@budibase/backend-core@2.1.22-alpha.6": + version "2.1.22-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.1.22-alpha.6.tgz#e9886620fcbee6fe0348365cb96ac6484fc5f8fd" + integrity sha512-P3NSgNuQXKmdeT8MLfeCji3ibRSeIIMSOQeNSQBWpaOTA69rpXQk753lHRwUWMpqil/ybsOuE/h1/Y3eTa+/UA== dependencies: - "@budibase/types" "2.1.22-alpha.3" + "@budibase/types" "2.1.22-alpha.6" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-sdk "2.1030.0" @@ -1360,13 +1360,13 @@ svelte-flatpickr "^3.2.3" svelte-portal "^1.0.0" -"@budibase/pro@2.1.22-alpha.3": - version "2.1.22-alpha.3" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.1.22-alpha.3.tgz#8f87ffc1c6c158ad467216d5cfcb7844e9a08363" - integrity sha512-IE0eHPswBycPYzvduZCAp4T6U8t1qTcT1A9jVJ8TeX2jiL22hPOQ+8nVNIaqTGfQvJ7foGiqtDcugQbk3QLGOg== +"@budibase/pro@2.1.22-alpha.6": + version "2.1.22-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.1.22-alpha.6.tgz#e4d8238f1727eab85ac72b3ce2fcaec2a5c18456" + integrity sha512-1CKqQ2HMX+/5p24aHpPlUgxoMjKRZxRoyK5fPD/X35Z0mDj+9Ohny3oqbF1fC20pl/20bmcaE4J+q2ph/pbxdQ== dependencies: - "@budibase/backend-core" "2.1.22-alpha.3" - "@budibase/types" "2.1.22-alpha.3" + "@budibase/backend-core" "2.1.22-alpha.6" + "@budibase/types" "2.1.22-alpha.6" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" @@ -1390,10 +1390,10 @@ svelte-apexcharts "^1.0.2" svelte-flatpickr "^3.1.0" -"@budibase/types@2.1.22-alpha.3": - version "2.1.22-alpha.3" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.1.22-alpha.3.tgz#30cfcb6a58989a94dfba43ec7f9052e4855b6803" - integrity sha512-3WKZ5DVkygUi9H3KJTL8geQf9cjmssM8tsbDEFi8KJHogfszJPia9di/DnN8rd/CXbsx3Zbsbe8LHiAAADz5og== +"@budibase/types@2.1.22-alpha.6": + version "2.1.22-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.1.22-alpha.6.tgz#a280321373c26a5bf1a52ed119de9178292893c0" + integrity sha512-rVrhs9u7OTzlCxgUFqBu5H6jsMHhwH8uduPhT8Eo7J+Wr4J/0Io7WeuAt1egK6t83JiEPxDBH3WLzAH+04hPVA== "@bull-board/api@3.7.0": version "3.7.0" diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 557e446d6d..7655fe4417 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "2.1.22-alpha.3", + "version": "2.1.22-alpha.6", "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 f63fbf3480..2978e8b6c1 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/types", - "version": "2.1.22-alpha.3", + "version": "2.1.22-alpha.6", "description": "Budibase types", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 98ffcdf360..0ebe4ccce8 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -51,3 +51,9 @@ export interface SearchUsersRequest { appId?: string userIds?: string[] } + +export interface CreateAdminUserRequest { + email: string + password: string + tenantId: string +} diff --git a/packages/types/src/documents/platform/index.ts b/packages/types/src/documents/platform/index.ts index 1a7cef91cf..8f57ce85fa 100644 --- a/packages/types/src/documents/platform/index.ts +++ b/packages/types/src/documents/platform/index.ts @@ -1,3 +1,4 @@ export * from "./info" export * from "./users" export * from "./accounts" +export * from "./tenants" diff --git a/packages/types/src/documents/platform/tenants.ts b/packages/types/src/documents/platform/tenants.ts new file mode 100644 index 0000000000..dd994df1d2 --- /dev/null +++ b/packages/types/src/documents/platform/tenants.ts @@ -0,0 +1,5 @@ +import { Document } from "../document" + +export interface Tenants extends Document { + tenantIds: string[] +} diff --git a/packages/types/src/sdk/auth.ts b/packages/types/src/sdk/auth.ts index 6a040abf77..766d18a606 100644 --- a/packages/types/src/sdk/auth.ts +++ b/packages/types/src/sdk/auth.ts @@ -1,3 +1,5 @@ +import { BBContext } from "./koa" + export interface AuthToken { userId: string tenantId: string @@ -25,3 +27,9 @@ export interface SessionKey { export interface ScannedSession { value: Session } + +export interface PlatformLogoutOpts { + ctx: BBContext + userId: string + keepActiveSession?: boolean +} diff --git a/packages/types/src/sdk/index.ts b/packages/types/src/sdk/index.ts index 724b152303..a32c8e2077 100644 --- a/packages/types/src/sdk/index.ts +++ b/packages/types/src/sdk/index.ts @@ -9,3 +9,4 @@ export * from "./koa" export * from "./auth" export * from "./locks" export * from "./db" +export * from "./middleware" diff --git a/packages/types/src/sdk/middleware/index.ts b/packages/types/src/sdk/middleware/index.ts new file mode 100644 index 0000000000..bc4220e329 --- /dev/null +++ b/packages/types/src/sdk/middleware/index.ts @@ -0,0 +1,2 @@ +export * from "./matchers" +export * from "./tenancy" diff --git a/packages/types/src/sdk/middleware/matchers.ts b/packages/types/src/sdk/middleware/matchers.ts new file mode 100644 index 0000000000..fc4ceb323e --- /dev/null +++ b/packages/types/src/sdk/middleware/matchers.ts @@ -0,0 +1,22 @@ +export interface EndpointMatcher { + /** + * The HTTP Path. e.g. /api/things/:thingId + */ + route: string + /** + * The HTTP Verb. e.g. GET, POST, etc. + * ALL is also accepted to cover all verbs. + */ + method: string + /** + * The route must match exactly - not just begins with + */ + strict?: boolean +} + +export interface RegexMatcher { + regex: RegExp + method: string + strict: boolean + route: string +} diff --git a/packages/types/src/sdk/middleware/tenancy.ts b/packages/types/src/sdk/middleware/tenancy.ts new file mode 100644 index 0000000000..8bb362d049 --- /dev/null +++ b/packages/types/src/sdk/middleware/tenancy.ts @@ -0,0 +1,13 @@ +export interface GetTenantIdOptions { + allowNoTenant?: boolean + excludeStrategies?: TenantResolutionStrategy[] + includeStrategies?: TenantResolutionStrategy[] +} + +export enum TenantResolutionStrategy { + USER = "user", + HEADER = "header", + QUERY = "query", + SUBDOMAIN = "subdomain", + PATH = "path", +} diff --git a/packages/worker/__mocks__/node-fetch.ts b/packages/worker/__mocks__/node-fetch.ts deleted file mode 100644 index 4c7127ee48..0000000000 --- a/packages/worker/__mocks__/node-fetch.ts +++ /dev/null @@ -1 +0,0 @@ -jest.mock("node-fetch", () => jest.fn()) diff --git a/packages/worker/__mocks__/oauth.ts b/packages/worker/__mocks__/oauth.ts new file mode 100644 index 0000000000..8e8122a9e0 --- /dev/null +++ b/packages/worker/__mocks__/oauth.ts @@ -0,0 +1,57 @@ +import * as jwt from "jsonwebtoken" + +const mockOAuth2 = { + getOAuthAccessToken: (code: string, p: any, cb: any) => { + const err = null + const accessToken = "access_token" + const refreshToken = "refresh_token" + + const exp = new Date() + exp.setDate(exp.getDate() + 1) + + const iat = new Date() + iat.setDate(iat.getDate() - 1) + + const claims = { + iss: "test", + sub: "sub", + aud: "clientId", + exp: exp.getTime() / 1000, + iat: iat.getTime() / 1000, + email: "oauth@example.com", + } + + const idToken = jwt.sign(claims, "secret") + + const params = { + id_token: idToken, + } + return cb(err, accessToken, refreshToken, params) + }, + _request: ( + method: string, + url: string, + headers: any, + postBody: any, + accessToken: string, + cb: any + ) => { + const err = null + const body = { + sub: "sub", + user_id: "userId", + name: "OAuth", + family_name: "2", + given_name: "OAuth", + middle_name: "", + } + const res = {} + return cb(err, JSON.stringify(body), res) + }, +} + +const oauth = { + OAuth2: jest.fn(() => mockOAuth2), +} + +export = oauth diff --git a/packages/worker/package.json b/packages/worker/package.json index a9a093e753..0ad9fd6758 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "2.1.22-alpha.3", + "version": "2.1.22-alpha.6", "description": "Budibase background service", "main": "src/index.ts", "repository": { @@ -36,10 +36,10 @@ "author": "Budibase", "license": "GPL-3.0", "dependencies": { - "@budibase/backend-core": "2.1.22-alpha.3", - "@budibase/pro": "2.1.22-alpha.3", - "@budibase/string-templates": "2.1.22-alpha.3", - "@budibase/types": "2.1.22-alpha.3", + "@budibase/backend-core": "2.1.22-alpha.6", + "@budibase/pro": "2.1.22-alpha.6", + "@budibase/string-templates": "2.1.22-alpha.6", + "@budibase/types": "2.1.22-alpha.6", "@koa/router": "8.0.8", "@sentry/node": "6.17.7", "@techpass/passport-openidconnect": "0.3.2", @@ -71,9 +71,11 @@ }, "devDependencies": { "@types/jest": "26.0.23", + "@types/jsonwebtoken": "8.5.1", "@types/koa": "2.13.4", "@types/koa__router": "8.0.11", "@types/node": "14.18.20", + "@types/node-fetch": "2.6.1", "@types/pouchdb": "6.4.0", "@types/uuid": "8.3.4", "@typescript-eslint/parser": "5.12.0", diff --git a/packages/worker/scripts/localdomain.js b/packages/worker/scripts/localdomain.js index 4e181628b2..985840b401 100644 --- a/packages/worker/scripts/localdomain.js +++ b/packages/worker/scripts/localdomain.js @@ -2,6 +2,36 @@ const updateDotEnv = require("update-dotenv") const arg = process.argv.slice(2)[0] +const isEnable = arg === "enable" + +let domain = process.argv.slice(2)[1] +if (!domain) { + domain = "local.com" +} + +const getAccountPortalUrl = () => { + if (isEnable) { + return `http://account.${domain}:10001` + } else { + return `http://localhost:10001` + } +} + +const getBudibaseUrl = () => { + if (isEnable) { + return `http://${domain}:10000` + } else { + return `http://localhost:10000` + } +} + +const getCookieDomain = () => { + if (isEnable) { + return `.${domain}` + } else { + return "" + } +} /** * For testing multi tenancy sub domains locally. @@ -16,11 +46,7 @@ const arg = process.argv.slice(2)[0] * 127.0.0.1 t2.local.com */ updateDotEnv({ - ACCOUNT_PORTAL_URL: - arg === "enable" - ? "http://account.local.com:10001" - : "http://localhost:10001", - COOKIE_DOMAIN: arg === "enable" ? ".local.com" : "", - PLATFORM_URL: - arg === "enable" ? "http://local.com:10000" : "http://localhost:10000", + ACCOUNT_PORTAL_URL: getAccountPortalUrl(), + COOKIE_DOMAIN: getCookieDomain(), + PLATFORM_URL: getBudibaseUrl(), }).then(() => console.log("Updated worker!")) diff --git a/packages/worker/src/api/controllers/global/auth.ts b/packages/worker/src/api/controllers/global/auth.ts index 3df77f024d..8d36024634 100644 --- a/packages/worker/src/api/controllers/global/auth.ts +++ b/packages/worker/src/api/controllers/global/auth.ts @@ -1,22 +1,26 @@ -const core = require("@budibase/backend-core") -const { Config, EmailTemplatePurpose } = require("../../../constants") -const { sendEmail, isEmailConfigured } = require("../../../utilities/email") +import core from "@budibase/backend-core" +import { + events, + users as usersCore, + context, + tenancy, +} from "@budibase/backend-core" +import { Config, EmailTemplatePurpose } from "../../../constants" +import { sendEmail, isEmailConfigured } from "../../../utilities/email" +import { checkResetPasswordCode } from "../../../utilities/redis" +import env from "../../../environment" +import sdk from "../../../sdk" +import { User } from "@budibase/types" const { setCookie, getCookie, clearCookie, hash, platformLogout } = core.utils const { Cookie, Header } = core.constants const { passport, ssoCallbackUrl, google, oidc } = core.auth -const { checkResetPasswordCode } = require("../../../utilities/redis") -const { getGlobalDB } = require("@budibase/backend-core/tenancy") -const env = require("../../../environment") -import { events, users as usersCore, context } from "@budibase/backend-core" -import sdk from "../../../sdk" -import { User } from "@budibase/types" export const googleCallbackUrl = async (config: any) => { - return ssoCallbackUrl(getGlobalDB(), config, "google") + return ssoCallbackUrl(tenancy.getGlobalDB(), config, "google") } export const oidcCallbackUrl = async (config: any) => { - return ssoCallbackUrl(getGlobalDB(), config, "oidc") + return ssoCallbackUrl(tenancy.getGlobalDB(), config, "oidc") } async function authInternal(ctx: any, user: any, err = null, info = null) { @@ -106,7 +110,7 @@ export const resetUpdate = async (ctx: any) => { const { resetCode, password } = ctx.request.body try { const { userId } = await checkResetPasswordCode(resetCode) - const db = getGlobalDB() + const db = tenancy.getGlobalDB() const user = await db.get(userId) user.password = await hash(password) await db.put(user) @@ -160,7 +164,7 @@ export const datasourceAuth = async (ctx: any, next: any) => { * On a successful login, you will be redirected to the googleAuth callback route. */ export const googlePreAuth = async (ctx: any, next: any) => { - const db = getGlobalDB() + const db = tenancy.getGlobalDB() const config = await core.db.getScopedConfig(db, { type: Config.GOOGLE, @@ -181,7 +185,7 @@ export const googlePreAuth = async (ctx: any, next: any) => { } export const googleAuth = async (ctx: any, next: any) => { - const db = getGlobalDB() + const db = tenancy.getGlobalDB() const config = await core.db.getScopedConfig(db, { type: Config.GOOGLE, @@ -208,7 +212,7 @@ export const googleAuth = async (ctx: any, next: any) => { } export const oidcStrategyFactory = async (ctx: any, configId: any) => { - const db = getGlobalDB() + const db = tenancy.getGlobalDB() const config = await core.db.getScopedConfig(db, { type: Config.OIDC, group: ctx.query.group, @@ -235,7 +239,7 @@ export const oidcPreAuth = async (ctx: any, next: any) => { setCookie(ctx, configId, Cookie.OIDC_CONFIG) - const db = getGlobalDB() + const db = tenancy.getGlobalDB() const config = await core.db.getScopedConfig(db, { type: Config.OIDC, group: ctx.query.group, diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index ea1df5b45a..7edb1b710a 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -5,6 +5,7 @@ import { BulkUserRequest, BulkUserResponse, CloudAccount, + CreateAdminUserRequest, InviteUserRequest, InviteUsersRequest, SearchUsersRequest, @@ -67,7 +68,8 @@ const parseBooleanParam = (param: any) => { } export const adminUser = async (ctx: any) => { - const { email, password, tenantId } = ctx.request.body + const { email, password, tenantId } = ctx.request + .body as CreateAdminUserRequest await tenancy.doInTenant(tenantId, async () => { // account portal sends a pre-hashed password - honour param to prevent double hashing const hashPassword = parseBooleanParam(ctx.request.query.hashPassword) diff --git a/packages/worker/src/api/controllers/system/environment.js b/packages/worker/src/api/controllers/system/environment.ts similarity index 69% rename from packages/worker/src/api/controllers/system/environment.js rename to packages/worker/src/api/controllers/system/environment.ts index 4edf1ff8d3..8ae0fcda3f 100644 --- a/packages/worker/src/api/controllers/system/environment.js +++ b/packages/worker/src/api/controllers/system/environment.ts @@ -1,6 +1,7 @@ -const env = require("../../../environment") +import { BBContext } from "@budibase/types" +import env from "../../../environment" -exports.fetch = async ctx => { +export const fetch = async (ctx: BBContext) => { ctx.body = { multiTenancy: !!env.MULTI_TENANCY, cloud: !env.SELF_HOSTED, diff --git a/packages/worker/src/api/controllers/system/status.js b/packages/worker/src/api/controllers/system/status.ts similarity index 54% rename from packages/worker/src/api/controllers/system/status.js rename to packages/worker/src/api/controllers/system/status.ts index 9d2bd6ecda..b763a67d4f 100644 --- a/packages/worker/src/api/controllers/system/status.js +++ b/packages/worker/src/api/controllers/system/status.ts @@ -1,7 +1,8 @@ -const accounts = require("@budibase/backend-core/accounts") -const env = require("../../../environment") +import { accounts } from "@budibase/backend-core" +import env from "../../../environment" +import { BBContext } from "@budibase/types" -exports.fetch = async ctx => { +export const fetch = async (ctx: BBContext) => { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { const status = await accounts.getStatus() ctx.body = status diff --git a/packages/worker/src/api/controllers/system/tenants.ts b/packages/worker/src/api/controllers/system/tenants.ts index d6e6261c22..6916049534 100644 --- a/packages/worker/src/api/controllers/system/tenants.ts +++ b/packages/worker/src/api/controllers/system/tenants.ts @@ -1,61 +1,18 @@ -const { StaticDatabases, doWithDB } = require("@budibase/backend-core/db") -const { getTenantId } = require("@budibase/backend-core/tenancy") -const { deleteTenant } = require("@budibase/backend-core/deprovision") +import { BBContext } from "@budibase/types" +import { deprovisioning } from "@budibase/backend-core" import { quotas } from "@budibase/pro" -export const exists = async (ctx: any) => { - const tenantId = ctx.request.params - ctx.body = { - exists: await doWithDB( - StaticDatabases.PLATFORM_INFO.name, - async (db: any) => { - let exists = false - try { - const tenantsDoc = await db.get( - StaticDatabases.PLATFORM_INFO.docs.tenants - ) - if (tenantsDoc) { - exists = tenantsDoc.tenantIds.indexOf(tenantId) !== -1 - } - } catch (err) { - // if error it doesn't exist - } - return exists - } - ), - } -} +const _delete = async (ctx: BBContext) => { + const user = ctx.user! + const tenantId = ctx.params.tenantId -export const fetch = async (ctx: any) => { - ctx.body = await doWithDB( - StaticDatabases.PLATFORM_INFO.name, - async (db: any) => { - let tenants = [] - try { - const tenantsDoc = await db.get( - StaticDatabases.PLATFORM_INFO.docs.tenants - ) - if (tenantsDoc) { - tenants = tenantsDoc.tenantIds - } - } catch (err) { - // if error it doesn't exist - } - return tenants - } - ) -} - -const _delete = async (ctx: any) => { - const tenantId = getTenantId() - - if (ctx.params.tenantId !== tenantId) { - ctx.throw(403, "Unauthorized") + if (tenantId !== user.tenantId) { + ctx.throw(403, "Tenant ID does not match current user") } try { - await deleteTenant(tenantId) await quotas.bustCache() + await deprovisioning.deleteTenant(tenantId) ctx.status = 204 } catch (err) { ctx.log.error(err) diff --git a/packages/worker/src/api/index.ts b/packages/worker/src/api/index.ts index 22ff159dff..9a32792691 100644 --- a/packages/worker/src/api/index.ts +++ b/packages/worker/src/api/index.ts @@ -7,11 +7,12 @@ import { errors, auth, middleware } from "@budibase/backend-core" import { APIError } from "@budibase/types" const PUBLIC_ENDPOINTS = [ - // old deprecated endpoints kept for backwards compat + // deprecated single tenant sso callback { route: "/api/admin/auth/google/callback", method: "GET", }, + // deprecated single tenant sso callback { route: "/api/admin/auth/oidc/callback", method: "GET", @@ -44,17 +45,19 @@ const PUBLIC_ENDPOINTS = [ method: "POST", }, { - route: "api/system/environment", + route: "/api/system/environment", method: "GET", }, { - route: "api/system/status", + route: "/api/system/status", method: "GET", }, + // TODO: This should be an internal api { route: "/api/global/users/tenant/:id", method: "GET", }, + // TODO: This should be an internal api { route: "/api/system/restored", method: "POST", @@ -62,17 +65,37 @@ const PUBLIC_ENDPOINTS = [ ] const NO_TENANCY_ENDPOINTS = [ - ...PUBLIC_ENDPOINTS, + // system endpoints are not specific to any tenant { route: "/api/system", method: "ALL", }, + // tenant is determined in request body + // used for creating the tenant { - route: "/api/global/users/self", + route: "/api/global/users/init", + method: "POST", + }, + // deprecated single tenant sso callback + { + route: "/api/admin/auth/google/callback", method: "GET", }, + // deprecated single tenant sso callback { - route: "/api/global/self", + route: "/api/admin/auth/oidc/callback", + method: "GET", + }, + // tenant is determined from code in redis + { + route: "/api/global/users/invite/accept", + method: "POST", + }, + // global user search - no tenancy + // :id is user id + // TODO: this should really be `/api/system/users/:id` + { + route: "/api/global/users/tenant/:id", method: "GET", }, ] diff --git a/packages/worker/src/api/routes/global/auth.js b/packages/worker/src/api/routes/global/auth.js index 1c292cdc7f..2bf6bb68bf 100644 --- a/packages/worker/src/api/routes/global/auth.js +++ b/packages/worker/src/api/routes/global/auth.js @@ -2,7 +2,6 @@ const Router = require("@koa/router") const authController = require("../../controllers/global/auth") const { joiValidator } = require("@budibase/backend-core/auth") const Joi = require("joi") -const { updateTenantId } = require("@budibase/backend-core/tenancy") const router = new Router() @@ -29,77 +28,61 @@ function buildResetUpdateValidation() { }).required().unknown(false)) } -function updateTenant(ctx, next) { - if (ctx.params) { - updateTenantId(ctx.params.tenantId) - } - return next() -} - router + // PASSWORD .post( "/api/global/auth/:tenantId/login", buildAuthValidation(), - updateTenant, authController.authenticate ) + .post("/api/global/auth/logout", authController.logout) .post( "/api/global/auth/:tenantId/reset", buildResetValidation(), - updateTenant, authController.reset ) .post( "/api/global/auth/:tenantId/reset/update", buildResetUpdateValidation(), - updateTenant, authController.resetUpdate ) - .post("/api/global/auth/logout", authController.logout) + // INIT .post("/api/global/auth/init", authController.setInitInfo) .get("/api/global/auth/init", authController.getInitInfo) - .get( - "/api/global/auth/:tenantId/google", - updateTenant, - authController.googlePreAuth - ) + + // DATASOURCE - MULTI TENANT .get( "/api/global/auth/:tenantId/datasource/:provider", - updateTenant, authController.datasourcePreAuth ) - // single tenancy endpoint - .get("/api/global/auth/google/callback", authController.googleAuth) + .get( + "/api/global/auth/:tenantId/datasource/:provider/callback", + authController.datasourceAuth + ) + + // DATASOURCE - SINGLE TENANT - DEPRECATED .get( "/api/global/auth/datasource/:provider/callback", authController.datasourceAuth ) - // multi-tenancy endpoint - .get( - "/api/global/auth/:tenantId/google/callback", - updateTenant, - authController.googleAuth - ) - .get( - "/api/global/auth/:tenantId/datasource/:provider/callback", - updateTenant, - authController.datasourceAuth - ) + + // GOOGLE - MULTI TENANT + .get("/api/global/auth/:tenantId/google", authController.googlePreAuth) + .get("/api/global/auth/:tenantId/google/callback", authController.googleAuth) + + // GOOGLE - SINGLE TENANT - DEPRECATED + .get("/api/global/auth/google/callback", authController.googleAuth) + .get("/api/admin/auth/google/callback", authController.googleAuth) + + // OIDC - MULTI TENANT .get( "/api/global/auth/:tenantId/oidc/configs/:configId", - updateTenant, authController.oidcPreAuth ) - // single tenancy endpoint + .get("/api/global/auth/:tenantId/oidc/callback", authController.oidcAuth) + + // OIDC - SINGLE TENANT - DEPRECATED .get("/api/global/auth/oidc/callback", authController.oidcAuth) - // multi-tenancy endpoint - .get( - "/api/global/auth/:tenantId/oidc/callback", - updateTenant, - authController.oidcAuth - ) - // deprecated - used by the default system before tenancy - .get("/api/admin/auth/google/callback", authController.googleAuth) .get("/api/admin/auth/oidc/callback", authController.oidcAuth) module.exports = router diff --git a/packages/worker/src/api/routes/global/templates.ts b/packages/worker/src/api/routes/global/templates.ts index 2db9b5009e..40600ce9aa 100644 --- a/packages/worker/src/api/routes/global/templates.ts +++ b/packages/worker/src/api/routes/global/templates.ts @@ -1,6 +1,6 @@ import Router from "@koa/router" import * as controller from "../../controllers/global/templates" -import { TemplatePurpose, TemplateTypes } from "../../../constants" +import { TemplatePurpose, TemplateType } from "../../../constants" import { auth as authCore } from "@budibase/backend-core" import Joi from "joi" const { adminOnly, joiValidator } = authCore @@ -16,7 +16,7 @@ function buildTemplateSaveValidation() { name: Joi.string().allow(null, ""), contents: Joi.string().required(), purpose: Joi.string().required().valid(...Object.values(TemplatePurpose)), - type: Joi.string().required().valid(...Object.values(TemplateTypes)), + type: Joi.string().required().valid(...Object.values(TemplateType)), }).required().unknown(true).optional()) } diff --git a/packages/worker/src/api/routes/global/tests/auth.spec.ts b/packages/worker/src/api/routes/global/tests/auth.spec.ts index 69fa1b223c..0d47857ac1 100644 --- a/packages/worker/src/api/routes/global/tests/auth.spec.ts +++ b/packages/worker/src/api/routes/global/tests/auth.spec.ts @@ -1,11 +1,16 @@ jest.mock("nodemailer") -import { TestConfiguration, mocks, API } from "../../../../tests" +import { TestConfiguration, mocks } from "../../../../tests" const sendMailMock = mocks.email.mock() import { events } from "@budibase/backend-core" +const expectSetAuthCookie = (res: any) => { + expect( + res.get("Set-Cookie").find((c: string) => c.startsWith("budibase:auth")) + ).toBeDefined() +} + describe("/api/global/auth", () => { const config = new TestConfiguration() - const api = new API(config) beforeAll(async () => { await config.beforeAll() @@ -19,90 +24,155 @@ describe("/api/global/auth", () => { jest.clearAllMocks() }) - it("should logout", async () => { - await api.auth.logout() - expect(events.auth.logout).toBeCalledTimes(1) - }) - - it("should be able to generate password reset email", async () => { - const { res, code } = await api.auth.requestPasswordReset(sendMailMock) - const user = await config.getUser("test@test.com") - - expect(res.body).toEqual({ - message: "Please check your email for a reset link.", + describe("password", () => { + describe("POST /api/global/auth/:tenantId/login", () => { + it("should login", () => {}) }) - expect(sendMailMock).toHaveBeenCalled() - expect(code).toBeDefined() - expect(events.user.passwordResetRequested).toBeCalledTimes(1) - expect(events.user.passwordResetRequested).toBeCalledWith(user) + describe("POST /api/global/auth/logout", () => { + it("should logout", async () => { + await config.api.auth.logout() + expect(events.auth.logout).toBeCalledTimes(1) + + // TODO: Verify sessions deleted + }) + }) + + describe("POST /api/global/auth/:tenantId/reset", () => { + it("should generate password reset email", async () => { + const { res, code } = await config.api.auth.requestPasswordReset( + sendMailMock + ) + const user = await config.getUser("test@test.com") + + expect(res.body).toEqual({ + message: "Please check your email for a reset link.", + }) + expect(sendMailMock).toHaveBeenCalled() + + expect(code).toBeDefined() + expect(events.user.passwordResetRequested).toBeCalledTimes(1) + expect(events.user.passwordResetRequested).toBeCalledWith(user) + }) + }) + + describe("POST /api/global/auth/:tenantId/reset/update", () => { + it("should reset password", async () => { + const { code } = await config.api.auth.requestPasswordReset( + sendMailMock + ) + const user = await config.getUser("test@test.com") + delete user.password + + const res = await config.api.auth.updatePassword(code) + + expect(res.body).toEqual({ message: "password reset successfully." }) + expect(events.user.passwordReset).toBeCalledTimes(1) + expect(events.user.passwordReset).toBeCalledWith(user) + + // TODO: Login using new password + }) + }) }) - it("should allow resetting user password with code", async () => { - const { code } = await api.auth.requestPasswordReset(sendMailMock) - const user = await config.getUser("test@test.com") - delete user.password + describe("init", () => { + describe("POST /api/global/auth/init", () => {}) - const res = await api.auth.updatePassword(code) + describe("GET /api/global/auth/init", () => {}) + }) - expect(res.body).toEqual({ message: "password reset successfully." }) - expect(events.user.passwordReset).toBeCalledTimes(1) - expect(events.user.passwordReset).toBeCalledWith(user) + describe("datasource", () => { + // MULTI TENANT + + describe("GET /api/global/auth/:tenantId/datasource/:provider", () => {}) + + describe("GET /api/global/auth/:tenantId/datasource/:provider/callback", () => {}) + + // SINGLE TENANT + + describe("GET /api/global/auth/datasource/:provider/callback", () => {}) + }) + + describe("google", () => { + // MULTI TENANT + + describe("GET /api/global/auth/:tenantId/google", () => {}) + + describe("GET /api/global/auth/:tenantId/google/callback", () => {}) + + // SINGLE TENANT + + describe("GET /api/global/auth/google/callback", () => {}) + + describe("GET /api/admin/auth/google/callback", () => {}) }) describe("oidc", () => { - const auth = require("@budibase/backend-core/auth") - - const passportSpy = jest.spyOn(auth.passport, "authenticate") - let oidcConf - let chosenConfig: any - let configId: string - - // mock the oidc strategy implementation and return value - let strategyFactory = jest.fn() - let mockStrategyReturn = jest.fn() - let mockStrategyConfig = jest.fn() - auth.oidc.fetchStrategyConfig = mockStrategyConfig - - strategyFactory.mockReturnValue(mockStrategyReturn) - auth.oidc.strategyFactory = strategyFactory - beforeEach(async () => { - oidcConf = await config.saveOIDCConfig() - chosenConfig = oidcConf.config.configs[0] - configId = chosenConfig.uuid - mockStrategyConfig.mockReturnValue(chosenConfig) + jest.clearAllMocks() + mockGetWellKnownConfig() + + // see: __mocks__/oauth + // for associated mocking inside passport }) - afterEach(() => { - expect(strategyFactory).toBeCalledWith(chosenConfig, expect.any(Function)) - }) + const generateOidcConfig = async () => { + const oidcConf = await config.saveOIDCConfig() + const chosenConfig = oidcConf.config.configs[0] + return chosenConfig.uuid + } - describe("oidc configs", () => { - it("should load strategy and delegate to passport", async () => { - await api.configs.getOIDCConfig(configId) + const mockGetWellKnownConfig = () => { + mocks.fetch.mockReturnValue({ + ok: true, + json: () => ({ + issuer: "test", + authorization_endpoint: "http://localhost/auth", + token_endpoint: "http://localhost/token", + userinfo_endpoint: "http://localhost/userinfo", + }), + }) + } - expect(passportSpy).toBeCalledWith(mockStrategyReturn, { - scope: ["profile", "email", "offline_access"], - }) - expect(passportSpy.mock.calls.length).toBe(1) + // MULTI TENANT + describe("GET /api/global/auth/:tenantId/oidc/configs/:configId", () => { + it("redirects to auth provider", async () => { + const configId = await generateOidcConfig() + + const res = await config.api.configs.getOIDCConfig(configId) + + expect(res.status).toBe(302) + const location: string = res.get("location") + expect( + location.startsWith( + "http://localhost/auth?response_type=code&client_id=clientId&redirect_uri=http%3A%2F%2Flocalhost%3A10000%2Fapi%2Fglobal%2Fauth%2Fdefault%2Foidc%2Fcallback&scope=openid%20profile%20email%20offline_access" + ) + ).toBe(true) }) }) - describe("oidc callback", () => { - it("should load strategy and delegate to passport", async () => { - await api.configs.OIDCCallback(configId) + describe("GET /api/global/auth/:tenantId/oidc/callback", () => { + it("logs in", async () => { + const configId = await generateOidcConfig() + const preAuthRes = await config.api.configs.getOIDCConfig(configId) - expect(passportSpy).toBeCalledWith( - mockStrategyReturn, - { - successRedirect: "/", - failureRedirect: "/error", - }, - expect.anything() - ) - expect(passportSpy.mock.calls.length).toBe(1) + const res = await config.api.configs.OIDCCallback(configId, preAuthRes) + + expect(events.auth.login).toBeCalledWith("oidc") + expect(events.auth.login).toBeCalledTimes(1) + expect(res.status).toBe(302) + const location: string = res.get("location") + expect(location).toBe("/") + expectSetAuthCookie(res) }) }) + + // SINGLE TENANT + + describe("GET /api/global/auth/oidc/callback", () => {}) + + describe("GET /api/global/auth/oidc/callback", () => {}) + + describe("GET /api/admin/auth/oidc/callback", () => {}) }) }) 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 82e80f4c90..ed457e7bcd 100644 --- a/packages/worker/src/api/routes/global/tests/configs.spec.ts +++ b/packages/worker/src/api/routes/global/tests/configs.spec.ts @@ -1,12 +1,11 @@ // mock the email system jest.mock("nodemailer") -import { TestConfiguration, structures, mocks, API } from "../../../../tests" +import { TestConfiguration, structures, mocks } from "../../../../tests" mocks.email.mock() import { Config, events } from "@budibase/backend-core" describe("configs", () => { const config = new TestConfiguration() - const api = new API(config) beforeAll(async () => { await config.beforeAll() @@ -28,7 +27,7 @@ describe("configs", () => { _rev, } - const res = await api.configs.saveConfig(data) + const res = await config.api.configs.saveConfig(data) return { ...data, @@ -235,7 +234,7 @@ describe("configs", () => { expect(events.org.nameUpdated).toBeCalledTimes(1) expect(events.org.logoUpdated).toBeCalledTimes(1) expect(events.org.platformURLUpdated).toBeCalledTimes(1) - config.modeAccount() + config.modeCloud() }) }) @@ -257,7 +256,7 @@ describe("configs", () => { expect(events.org.nameUpdated).toBeCalledTimes(1) expect(events.org.logoUpdated).toBeCalledTimes(1) expect(events.org.platformURLUpdated).toBeCalledTimes(1) - config.modeAccount() + config.modeCloud() }) }) }) @@ -266,7 +265,7 @@ describe("configs", () => { it("should return the correct checklist status based on the state of the budibase installation", async () => { await config.saveSmtpConfig() - const res = await api.configs.getConfigChecklist() + const res = await config.api.configs.getConfigChecklist() const checklist = res.body expect(checklist.apps.checked).toBeFalsy() diff --git a/packages/worker/src/api/routes/global/tests/email.spec.ts b/packages/worker/src/api/routes/global/tests/email.spec.ts index 608f4094f8..9e65cda3c5 100644 --- a/packages/worker/src/api/routes/global/tests/email.spec.ts +++ b/packages/worker/src/api/routes/global/tests/email.spec.ts @@ -1,11 +1,10 @@ jest.mock("nodemailer") -import { TestConfiguration, mocks, API } from "../../../../tests" +import { TestConfiguration, mocks } from "../../../../tests" const sendMailMock = mocks.email.mock() import { EmailTemplatePurpose } from "../../../../constants" describe("/api/global/email", () => { const config = new TestConfiguration() - const api = new API(config) beforeAll(async () => { await config.beforeAll() @@ -20,7 +19,9 @@ describe("/api/global/email", () => { await config.saveSmtpConfig() await config.saveSettingsConfig() - const res = await api.emails.sendEmail(EmailTemplatePurpose.INVITATION) + const res = await config.api.emails.sendEmail( + EmailTemplatePurpose.INVITATION + ) expect(res.body.message).toBeDefined() expect(sendMailMock).toHaveBeenCalled() diff --git a/packages/worker/src/api/routes/global/tests/license.spec.ts b/packages/worker/src/api/routes/global/tests/license.spec.ts new file mode 100644 index 0000000000..b25b41adb9 --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/license.spec.ts @@ -0,0 +1,31 @@ +import { TestConfiguration } from "../../../../tests" + +// TODO + +describe("/api/global/license", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("POST /api/global/license/activate", () => { + it("activates license", () => {}) + }) + + describe("POST /api/global/license/refresh", () => {}) + + describe("GET /api/global/license/info", () => {}) + + describe("DELETE /api/global/license/info", () => {}) + + describe("GET /api/global/license/usage", () => {}) +}) diff --git a/packages/worker/src/api/routes/global/tests/realEmail.spec.ts b/packages/worker/src/api/routes/global/tests/realEmail.spec.ts index 135367e0d8..1c180be75d 100644 --- a/packages/worker/src/api/routes/global/tests/realEmail.spec.ts +++ b/packages/worker/src/api/routes/global/tests/realEmail.spec.ts @@ -1,4 +1,4 @@ -import { TestConfiguration, API } from "../../../../tests" +import { TestConfiguration } from "../../../../tests" import { EmailTemplatePurpose } from "../../../../constants" const nodemailer = require("nodemailer") const fetch = require("node-fetch") @@ -8,7 +8,6 @@ jest.setTimeout(30000) describe("/api/global/email", () => { const config = new TestConfiguration() - const api = new API(config) beforeAll(async () => { await config.beforeAll() @@ -35,7 +34,7 @@ describe("/api/global/email", () => { await Promise.race([config.saveEtherealSmtpConfig(), timeout()]) await Promise.race([config.saveSettingsConfig(), timeout()]) - const res = await api.emails.sendEmail(purpose).timeout(20000) + const res = await config.api.emails.sendEmail(purpose).timeout(20000) // ethereal hiccup, can't test right now if (res.status >= 300) { return diff --git a/packages/worker/src/api/routes/global/tests/roles.spec.ts b/packages/worker/src/api/routes/global/tests/roles.spec.ts new file mode 100644 index 0000000000..516c3433ab --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/roles.spec.ts @@ -0,0 +1,27 @@ +import { TestConfiguration } from "../../../../tests" + +// TODO + +describe("/api/global/roles", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("GET /api/global/roles", () => { + it("retrieves roles", () => {}) + }) + + describe("GET /api/global/roles/:appId", () => {}) + + describe("DELETE /api/global/roles/:appId", () => {}) +}) diff --git a/packages/worker/src/api/routes/global/tests/self.spec.ts b/packages/worker/src/api/routes/global/tests/self.spec.ts index 5640bab3ce..d253a7f24e 100644 --- a/packages/worker/src/api/routes/global/tests/self.spec.ts +++ b/packages/worker/src/api/routes/global/tests/self.spec.ts @@ -1,10 +1,9 @@ jest.mock("nodemailer") -import { TestConfiguration, API, mocks } from "../../../../tests" +import { TestConfiguration, mocks } from "../../../../tests" import { events } from "@budibase/backend-core" describe("/api/global/self", () => { const config = new TestConfiguration() - const api = new API(config) beforeAll(async () => { await config.beforeAll() @@ -24,7 +23,7 @@ describe("/api/global/self", () => { await config.createSession(user) delete user.password - const res = await api.self.updateSelf(user) + const res = await config.api.self.updateSelf(user) const dbUser = await config.getUser(user.email) user._rev = dbUser._rev @@ -40,7 +39,7 @@ describe("/api/global/self", () => { await config.createSession(user) user.password = "newPassword" - const res = await api.self.updateSelf(user) + const res = await config.api.self.updateSelf(user) const dbUser = await config.getUser(user.email) user._rev = dbUser._rev diff --git a/packages/worker/src/api/routes/global/tests/templates.spec.ts b/packages/worker/src/api/routes/global/tests/templates.spec.ts new file mode 100644 index 0000000000..d1c296643d --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/templates.spec.ts @@ -0,0 +1,35 @@ +import { TestConfiguration } from "../../../../tests" + +// TODO + +describe("/api/global/template", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("GET /api/global/template/definitions", () => { + it("retrieves definitions", () => {}) + }) + + describe("POST /api/global/template", () => {}) + + describe("GET /api/global/template", () => {}) + + describe("GET /api/global/template/:type", () => {}) + + describe("GET /api/global/template/:ownerId", () => {}) + + describe("GET /api/global/template/:id", () => {}) + + describe("DELETE /api/global/template/:id/:rev", () => {}) +}) diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index 218bc60800..3165cba315 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -6,14 +6,12 @@ import { mocks, structures, TENANT_1, - API, } from "../../../../tests" const sendMailMock = mocks.email.mock() import { events, tenancy } from "@budibase/backend-core" describe("/api/global/users", () => { const config = new TestConfiguration() - const api = new API(config) beforeAll(async () => { await config.beforeAll() @@ -30,7 +28,10 @@ describe("/api/global/users", () => { describe("invite", () => { it("should be able to generate an invitation", async () => { const email = structures.users.newEmail() - const { code, res } = await api.users.sendUserInvite(sendMailMock, email) + const { code, res } = await config.api.users.sendUserInvite( + sendMailMock, + email + ) expect(res.body).toEqual({ message: "Invitation has been sent." }) expect(sendMailMock).toHaveBeenCalled() @@ -39,7 +40,7 @@ describe("/api/global/users", () => { }) it("should not be able to generate an invitation for existing user", async () => { - const { code, res } = await api.users.sendUserInvite( + const { code, res } = await config.api.users.sendUserInvite( sendMailMock, config.defaultUser!.email, 400 @@ -53,9 +54,12 @@ describe("/api/global/users", () => { it("should be able to create new user from invite", async () => { const email = structures.users.newEmail() - const { code } = await api.users.sendUserInvite(sendMailMock, email) + const { code } = await config.api.users.sendUserInvite( + sendMailMock, + email + ) - const res = await api.users.acceptInvite(code) + const res = await config.api.users.acceptInvite(code) expect(res.body._id).toBeDefined() const user = await config.getUser(email) @@ -74,7 +78,7 @@ describe("/api/global/users", () => { }) const request = [newUserInvite(), newUserInvite()] - const res = await api.users.sendMultiUserInvite(request) + const res = await config.api.users.sendMultiUserInvite(request) const body = res.body as InviteUsersResponse expect(body.successful.length).toBe(2) @@ -86,7 +90,7 @@ describe("/api/global/users", () => { it("should not be able to generate an invitation for existing user", async () => { const request = [{ email: config.defaultUser!.email, userInfo: {} }] - const res = await api.users.sendMultiUserInvite(request) + const res = await config.api.users.sendMultiUserInvite(request) const body = res.body as InviteUsersResponse expect(body.successful.length).toBe(0) @@ -102,7 +106,7 @@ describe("/api/global/users", () => { const user = await config.createUser() jest.clearAllMocks() - const response = await api.users.bulkCreateUsers([user]) + const response = await config.api.users.bulkCreateUsers([user]) expect(response.created?.successful.length).toBe(0) expect(response.created?.unsuccessful.length).toBe(1) @@ -115,7 +119,7 @@ describe("/api/global/users", () => { jest.resetAllMocks() await tenancy.doInTenant(TENANT_1, async () => { - const response = await api.users.bulkCreateUsers([user]) + const response = await config.api.users.bulkCreateUsers([user]) expect(response.created?.successful.length).toBe(0) expect(response.created?.unsuccessful.length).toBe(1) @@ -126,11 +130,11 @@ describe("/api/global/users", () => { it("should ignore accounts using the same email", async () => { const account = structures.accounts.account() - const resp = await api.accounts.saveMetadata(account) + const resp = await config.api.accounts.saveMetadata(account) const user = structures.users.user({ email: resp.email }) jest.clearAllMocks() - const response = await api.users.bulkCreateUsers([user]) + const response = await config.api.users.bulkCreateUsers([user]) expect(response.created?.successful.length).toBe(0) expect(response.created?.unsuccessful.length).toBe(1) @@ -143,7 +147,11 @@ describe("/api/global/users", () => { const admin = structures.users.adminUser() const user = structures.users.user() - const response = await api.users.bulkCreateUsers([builder, admin, user]) + const response = await config.api.users.bulkCreateUsers([ + builder, + admin, + user, + ]) expect(response.created?.successful.length).toBe(3) expect(response.created?.successful[0].email).toBe(builder.email) @@ -160,7 +168,7 @@ describe("/api/global/users", () => { it("should be able to create a basic user", async () => { const user = structures.users.user() - await api.users.saveUser(user) + await config.api.users.saveUser(user) expect(events.user.created).toBeCalledTimes(1) expect(events.user.updated).not.toBeCalled() @@ -171,7 +179,7 @@ describe("/api/global/users", () => { it("should be able to create an admin user", async () => { const user = structures.users.adminUser() - await api.users.saveUser(user) + await config.api.users.saveUser(user) expect(events.user.created).toBeCalledTimes(1) expect(events.user.updated).not.toBeCalled() @@ -182,7 +190,7 @@ describe("/api/global/users", () => { it("should be able to create a builder user", async () => { const user = structures.users.builderUser() - await api.users.saveUser(user) + await config.api.users.saveUser(user) expect(events.user.created).toBeCalledTimes(1) expect(events.user.updated).not.toBeCalled() @@ -197,7 +205,7 @@ describe("/api/global/users", () => { app_456: "role2", } - await api.users.saveUser(user) + await config.api.users.saveUser(user) const savedUser = await config.getUser(user.email) expect(events.user.created).toBeCalledTimes(1) @@ -213,7 +221,7 @@ describe("/api/global/users", () => { delete user._id delete user._rev - const response = await api.users.saveUser(user, 400) + const response = await config.api.users.saveUser(user, 400) expect(response.body.message).toBe(`Unavailable`) expect(events.user.created).toBeCalledTimes(0) @@ -225,7 +233,7 @@ describe("/api/global/users", () => { await tenancy.doInTenant(TENANT_1, async () => { delete user._id - const response = await api.users.saveUser(user, 400) + const response = await config.api.users.saveUser(user, 400) expect(response.body.message).toBe(`Unavailable`) expect(events.user.created).toBeCalledTimes(0) @@ -237,7 +245,7 @@ describe("/api/global/users", () => { const account = structures.accounts.cloudAccount() mocks.accounts.getAccount.mockReturnValueOnce(account) - const response = await api.users.saveUser(user, 400) + const response = await config.api.users.saveUser(user, 400) expect(response.body.message).toBe(`Unavailable`) expect(events.user.created).toBeCalledTimes(0) @@ -245,20 +253,20 @@ describe("/api/global/users", () => { it("should not be able to create a user with the same email and different casing", async () => { const user = structures.users.user() - await api.users.saveUser(user) + await config.api.users.saveUser(user) user.email = user.email.toUpperCase() - await api.users.saveUser(user, 400) + await config.api.users.saveUser(user, 400) expect(events.user.created).toBeCalledTimes(1) }) it("should not be able to bulk create a user with the same email and different casing", async () => { const user = structures.users.user() - await api.users.saveUser(user) + await config.api.users.saveUser(user) user.email = user.email.toUpperCase() - await api.users.bulkCreateUsers([user]) + await config.api.users.bulkCreateUsers([user]) expect(events.user.created).toBeCalledTimes(1) }) @@ -269,7 +277,7 @@ describe("/api/global/users", () => { const user = await config.createUser() jest.clearAllMocks() - await api.users.saveUser(user) + await config.api.users.saveUser(user) expect(events.user.created).not.toBeCalled() expect(events.user.updated).toBeCalledTimes(1) @@ -284,7 +292,7 @@ describe("/api/global/users", () => { user.forceResetPassword = true user.password = "tempPassword" - await api.users.saveUser(user) + await config.api.users.saveUser(user) expect(events.user.created).not.toBeCalled() expect(events.user.updated).toBeCalledTimes(1) @@ -297,7 +305,7 @@ describe("/api/global/users", () => { const user = await config.createUser() jest.clearAllMocks() - await api.users.saveUser(structures.users.adminUser(user)) + await config.api.users.saveUser(structures.users.adminUser(user)) expect(events.user.created).not.toBeCalled() expect(events.user.updated).toBeCalledTimes(1) @@ -309,7 +317,7 @@ describe("/api/global/users", () => { const user = await config.createUser() jest.clearAllMocks() - await api.users.saveUser(structures.users.builderUser(user)) + await config.api.users.saveUser(structures.users.builderUser(user)) expect(events.user.created).not.toBeCalled() expect(events.user.updated).toBeCalledTimes(1) @@ -323,7 +331,7 @@ describe("/api/global/users", () => { user.admin!.global = false user.builder!.global = false - await api.users.saveUser(user) + await config.api.users.saveUser(user) expect(events.user.created).not.toBeCalled() expect(events.user.updated).toBeCalledTimes(1) @@ -336,7 +344,7 @@ describe("/api/global/users", () => { jest.clearAllMocks() user.builder!.global = false - await api.users.saveUser(user) + await config.api.users.saveUser(user) expect(events.user.created).not.toBeCalled() expect(events.user.updated).toBeCalledTimes(1) @@ -352,7 +360,7 @@ describe("/api/global/users", () => { app_456: "role2", } - await api.users.saveUser(user) + await config.api.users.saveUser(user) const savedUser = await config.getUser(user.email) expect(events.user.created).not.toBeCalled() @@ -372,7 +380,7 @@ describe("/api/global/users", () => { jest.clearAllMocks() user.roles = {} - await api.users.saveUser(user) + await config.api.users.saveUser(user) const savedUser = await config.getUser(user.email) expect(events.user.created).not.toBeCalled() @@ -395,7 +403,7 @@ describe("/api/global/users", () => { app_456: "role2-edit", } - await api.users.saveUser(user) + await config.api.users.saveUser(user) const savedUser = await config.getUser(user.email) expect(events.user.created).not.toBeCalled() @@ -411,7 +419,7 @@ describe("/api/global/users", () => { const user = await config.createUser(structures.users.user({ email })) user.email = "new@test.com" - const response = await api.users.saveUser(user, 400) + const response = await config.api.users.saveUser(user, 400) const dbUser = await config.getUser(email) user.email = email @@ -424,7 +432,7 @@ describe("/api/global/users", () => { it("should not be able to bulk delete current user", async () => { const user = await config.defaultUser! - const response = await api.users.bulkDeleteUsers([user._id!], 400) + const response = await config.api.users.bulkDeleteUsers([user._id!], 400) expect(response.message).toBe("Unable to delete self.") expect(events.user.deleted).not.toBeCalled() @@ -436,7 +444,7 @@ describe("/api/global/users", () => { account.budibaseUserId = user._id! mocks.accounts.getAccountByTenantId.mockReturnValue(account) - const response = await api.users.bulkDeleteUsers([user._id!]) + const response = await config.api.users.bulkDeleteUsers([user._id!]) expect(response.deleted?.successful.length).toBe(0) expect(response.deleted?.unsuccessful.length).toBe(1) @@ -454,7 +462,7 @@ describe("/api/global/users", () => { const builder = structures.users.builderUser() const admin = structures.users.adminUser() const user = structures.users.user() - const createdUsers = await api.users.bulkCreateUsers([ + const createdUsers = await config.api.users.bulkCreateUsers([ builder, admin, user, @@ -463,7 +471,7 @@ describe("/api/global/users", () => { const toDelete = createdUsers.created?.successful.map( u => u._id! ) as string[] - const response = await api.users.bulkDeleteUsers(toDelete) + const response = await config.api.users.bulkDeleteUsers(toDelete) expect(response.deleted?.successful.length).toBe(3) expect(response.deleted?.unsuccessful.length).toBe(0) @@ -478,7 +486,7 @@ describe("/api/global/users", () => { const user = await config.createUser() jest.clearAllMocks() - await api.users.deleteUser(user._id!) + await config.api.users.deleteUser(user._id!) expect(events.user.deleted).toBeCalledTimes(1) expect(events.user.permissionBuilderRemoved).not.toBeCalled() @@ -489,7 +497,7 @@ describe("/api/global/users", () => { const user = await config.createUser(structures.users.adminUser()) jest.clearAllMocks() - await api.users.deleteUser(user._id!) + await config.api.users.deleteUser(user._id!) expect(events.user.deleted).toBeCalledTimes(1) expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) @@ -500,7 +508,7 @@ describe("/api/global/users", () => { const user = await config.createUser(structures.users.builderUser()) jest.clearAllMocks() - await api.users.deleteUser(user._id!) + await config.api.users.deleteUser(user._id!) expect(events.user.deleted).toBeCalledTimes(1) expect(events.user.permissionBuilderRemoved).toBeCalledTimes(1) @@ -512,7 +520,7 @@ describe("/api/global/users", () => { const account = structures.accounts.cloudAccount() mocks.accounts.getAccount.mockReturnValueOnce(account) - const response = await api.users.deleteUser(user._id!, 400) + const response = await config.api.users.deleteUser(user._id!, 400) expect(response.body.message).toBe("Account holder cannot be deleted") }) @@ -523,7 +531,7 @@ describe("/api/global/users", () => { account.email = user.email mocks.accounts.getAccount.mockReturnValueOnce(account) - const response = await api.users.deleteUser(user._id!, 400) + const response = await config.api.users.deleteUser(user._id!, 400) expect(response.body.message).toBe("Unable to delete self.") }) diff --git a/packages/worker/src/api/routes/global/tests/workspaces.spec.ts b/packages/worker/src/api/routes/global/tests/workspaces.spec.ts new file mode 100644 index 0000000000..1a30c6525c --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/workspaces.spec.ts @@ -0,0 +1,29 @@ +import { TestConfiguration } from "../../../../tests" + +// TODO + +describe("/api/global/workspaces", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("GET /api/global/workspaces", () => { + it("retrieves workspaces", () => {}) + }) + + describe("DELETE /api/global/workspaces/:id", () => {}) + + describe("GET /api/global/workspaces", () => {}) + + describe("GET /api/global/workspaces/:id", () => {}) +}) diff --git a/packages/worker/src/api/routes/system/environment.js b/packages/worker/src/api/routes/system/environment.js deleted file mode 100644 index 3d34046317..0000000000 --- a/packages/worker/src/api/routes/system/environment.js +++ /dev/null @@ -1,8 +0,0 @@ -const Router = require("@koa/router") -const controller = require("../../controllers/system/environment") - -const router = new Router() - -router.get("/api/system/environment", controller.fetch) - -module.exports = router diff --git a/packages/worker/src/api/routes/system/environment.ts b/packages/worker/src/api/routes/system/environment.ts new file mode 100644 index 0000000000..360ec7ed84 --- /dev/null +++ b/packages/worker/src/api/routes/system/environment.ts @@ -0,0 +1,8 @@ +import Router from "@koa/router" +import * as controller from "../../controllers/system/environment" + +const router = new Router() + +router.get("/api/system/environment", controller.fetch) + +export default router diff --git a/packages/worker/src/api/routes/system/status.js b/packages/worker/src/api/routes/system/status.js deleted file mode 100644 index 17d2f8a5a6..0000000000 --- a/packages/worker/src/api/routes/system/status.js +++ /dev/null @@ -1,8 +0,0 @@ -const Router = require("@koa/router") -const controller = require("../../controllers/system/status") - -const router = new Router() - -router.get("/api/system/status", controller.fetch) - -module.exports = router diff --git a/packages/worker/src/api/routes/system/status.ts b/packages/worker/src/api/routes/system/status.ts new file mode 100644 index 0000000000..a5b393b421 --- /dev/null +++ b/packages/worker/src/api/routes/system/status.ts @@ -0,0 +1,8 @@ +import Router from "@koa/router" +import * as controller from "../../controllers/system/status" + +const router = new Router() + +router.get("/api/system/status", controller.fetch) + +export default router diff --git a/packages/worker/src/api/routes/system/tenants.js b/packages/worker/src/api/routes/system/tenants.js deleted file mode 100644 index 6247e76058..0000000000 --- a/packages/worker/src/api/routes/system/tenants.js +++ /dev/null @@ -1,12 +0,0 @@ -const Router = require("@koa/router") -const controller = require("../../controllers/system/tenants") -const { adminOnly } = require("@budibase/backend-core/auth") - -const router = new Router() - -router - .get("/api/system/tenants/:tenantId/exists", controller.exists) - .get("/api/system/tenants", adminOnly, controller.fetch) - .delete("/api/system/tenants/:tenantId", adminOnly, controller.delete) - -module.exports = router diff --git a/packages/worker/src/api/routes/system/tenants.ts b/packages/worker/src/api/routes/system/tenants.ts new file mode 100644 index 0000000000..7feb73a234 --- /dev/null +++ b/packages/worker/src/api/routes/system/tenants.ts @@ -0,0 +1,13 @@ +import Router from "@koa/router" +import * as controller from "../../controllers/system/tenants" +import { middleware } from "@budibase/backend-core" + +const router = new Router() + +router.delete( + "/api/system/tenants/:tenantId", + middleware.adminOnly, + controller.delete +) + +export default router diff --git a/packages/worker/src/api/routes/system/tests/accounts.spec.ts b/packages/worker/src/api/routes/system/tests/accounts.spec.ts index f977d22cd9..fd54dd2b0a 100644 --- a/packages/worker/src/api/routes/system/tests/accounts.spec.ts +++ b/packages/worker/src/api/routes/system/tests/accounts.spec.ts @@ -1,10 +1,9 @@ import sdk from "../../../../sdk" -import { TestConfiguration, structures, API } from "../../../../tests" +import { TestConfiguration, structures } from "../../../../tests" import { v4 as uuid } from "uuid" describe("accounts", () => { const config = new TestConfiguration() - const api = new API(config) beforeAll(async () => { await config.beforeAll() @@ -23,7 +22,7 @@ describe("accounts", () => { it("saves account metadata", async () => { let account = structures.accounts.account() - const response = await api.accounts.saveMetadata(account) + const response = await config.api.accounts.saveMetadata(account) const id = sdk.accounts.formatAccountMetadataId(account.accountId) const metadata = await sdk.accounts.getMetadata(id) @@ -34,9 +33,9 @@ describe("accounts", () => { describe("destroyMetadata", () => { it("destroys account metadata", async () => { const account = structures.accounts.account() - await api.accounts.saveMetadata(account) + await config.api.accounts.saveMetadata(account) - await api.accounts.destroyMetadata(account.accountId) + await config.api.accounts.destroyMetadata(account.accountId) const deleted = await sdk.accounts.getMetadata(account.accountId) expect(deleted).toBe(undefined) @@ -45,7 +44,7 @@ describe("accounts", () => { it("destroys account metadata that does not exist", async () => { const id = uuid() - const response = await api.accounts.destroyMetadata(id) + const response = await config.api.accounts.destroyMetadata(id) expect(response.status).toBe(204) }) diff --git a/packages/worker/src/api/routes/system/tests/environment.spec.ts b/packages/worker/src/api/routes/system/tests/environment.spec.ts new file mode 100644 index 0000000000..f18ae1ba91 --- /dev/null +++ b/packages/worker/src/api/routes/system/tests/environment.spec.ts @@ -0,0 +1,29 @@ +import { TestConfiguration } from "../../../../tests" + +describe("/api/system/environment", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("GET /api/system/environment", () => { + it("returns the expected environment", async () => { + const env = await config.api.environment.getEnvironment() + expect(env.body).toEqual({ + cloud: true, + disableAccountPortal: false, + isDev: false, + multiTenancy: true, + }) + }) + }) +}) diff --git a/packages/worker/src/api/routes/system/tests/migrations.spec.ts b/packages/worker/src/api/routes/system/tests/migrations.spec.ts new file mode 100644 index 0000000000..304a64761e --- /dev/null +++ b/packages/worker/src/api/routes/system/tests/migrations.spec.ts @@ -0,0 +1,63 @@ +const migrateFn = jest.fn() + +import { TestConfiguration } from "../../../../tests" + +jest.mock("../../../../migrations", () => { + return { + ...jest.requireActual("../../../../migrations"), + migrate: migrateFn, + } +}) + +describe("/api/system/migrations", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("POST /api/system/migrations/run", () => { + it("fails with no internal api key", async () => { + const res = await config.api.migrations.runMigrations({ + headers: {}, + status: 403, + }) + expect(res.text).toBe("Unauthorized - no public worker access") + expect(migrateFn).toBeCalledTimes(0) + }) + + it("runs migrations", async () => { + const res = await config.api.migrations.runMigrations() + expect(res.text).toBe("OK") + expect(migrateFn).toBeCalledTimes(1) + }) + }) + + describe("DELETE /api/system/migrations/definitions", () => { + it("fails with no internal api key", async () => { + const res = await config.api.migrations.getMigrationDefinitions({ + headers: {}, + status: 403, + }) + expect(res.text).toBe("Unauthorized - no public worker access") + }) + + it("returns definitions", async () => { + const res = await config.api.migrations.getMigrationDefinitions() + expect(res.body).toEqual([ + { + name: "global_info_sync_users", + type: "global", + }, + ]) + }) + }) +}) diff --git a/packages/worker/src/api/routes/system/tests/restore.spec.ts b/packages/worker/src/api/routes/system/tests/restore.spec.ts new file mode 100644 index 0000000000..4dd973270f --- /dev/null +++ b/packages/worker/src/api/routes/system/tests/restore.spec.ts @@ -0,0 +1,36 @@ +import { TestConfiguration } from "../../../../tests" + +describe("/api/system/restore", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("POST /api/global/restore", () => { + it("doesn't allow restore in cloud", async () => { + const res = await config.api.restore.restored({ status: 405 }) + expect(res.body).toEqual({ + message: "This operation is not allowed in cloud.", + status: 405, + }) + }) + + it("restores in self host", async () => { + config.modeSelf() + const res = await config.api.restore.restored() + expect(res.body).toEqual({ + message: "System prepared after restore.", + }) + config.modeCloud() + }) + }) +}) diff --git a/packages/worker/src/api/routes/system/tests/status.spec.ts b/packages/worker/src/api/routes/system/tests/status.spec.ts new file mode 100644 index 0000000000..afd3f8ac46 --- /dev/null +++ b/packages/worker/src/api/routes/system/tests/status.spec.ts @@ -0,0 +1,48 @@ +import { TestConfiguration } from "../../../../tests" +import { accounts } from "@budibase/backend-core" +import { mocks } from "@budibase/backend-core/tests" + +describe("/api/system/status", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("GET /api/system/status", () => { + it("returns status in self host", async () => { + config.modeSelf() + const res = await config.api.status.getStatus() + expect(res.body).toEqual({ + health: { + passing: true, + }, + }) + expect(accounts.getStatus).toBeCalledTimes(0) + config.modeCloud() + }) + + it("returns status in cloud", async () => { + const value = { + health: { + passing: false, + }, + } + + mocks.accounts.getStatus.mockReturnValueOnce(value) + + const res = await config.api.status.getStatus() + + expect(accounts.getStatus).toBeCalledTimes(1) + expect(res.body).toEqual(value) + }) + }) +}) diff --git a/packages/worker/src/api/routes/system/tests/tenants.spec.ts b/packages/worker/src/api/routes/system/tests/tenants.spec.ts new file mode 100644 index 0000000000..af509b402e --- /dev/null +++ b/packages/worker/src/api/routes/system/tests/tenants.spec.ts @@ -0,0 +1,61 @@ +import { TestConfiguration } from "../../../../tests" +import { tenancy } from "@budibase/backend-core" + +describe("/api/global/tenants", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + describe("DELETE /api/system/tenants/:tenantId", () => { + it("allows deleting the current tenant", async () => { + const user = await config.createTenant() + + await config.api.tenants.delete(user.tenantId, { + headers: config.authHeaders(user), + }) + }) + + it("rejects deleting another tenant", async () => { + const user1 = await config.createTenant() + // create a second user in another tenant + const user2 = await config.createTenant() + + const status = 403 + const res = await config.api.tenants.delete(user1.tenantId, { + status, + headers: config.authHeaders(user2), + }) + + expect(res.body).toEqual({ + message: "Tenant ID does not match current user", + status, + }) + }) + + it("rejects non-admin", async () => { + const user1 = await config.createTenant() + // create an internal non-admin user + const user2 = await tenancy.doInTenant(user1.tenantId, () => { + return config.createUser() + }) + await config.createSession(user2) + + const res = await config.api.tenants.delete(user1.tenantId, { + status: 403, + headers: config.authHeaders(user2), + }) + + expect(res.body).toEqual(config.adminOnlyResponse()) + }) + }) +}) diff --git a/packages/worker/src/constants/index.js b/packages/worker/src/constants/index.ts similarity index 100% rename from packages/worker/src/constants/index.js rename to packages/worker/src/constants/index.ts diff --git a/packages/worker/src/middleware/tests/tenancy.spec.ts b/packages/worker/src/middleware/tests/tenancy.spec.ts new file mode 100644 index 0000000000..72c00fb6fb --- /dev/null +++ b/packages/worker/src/middleware/tests/tenancy.spec.ts @@ -0,0 +1,73 @@ +import { TestConfiguration, structures } from "../../tests" +import { constants } from "@budibase/backend-core" + +describe("tenancy middleware", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it("should get tenant id from user", async () => { + const user = await config.createTenant() + await config.createSession(user) + const res = await config.api.self.getSelf(user) + expect(res.headers[constants.Headers.TENANT_ID]).toBe(user.tenantId) + }) + + it("should get tenant id from header", async () => { + const tenantId = structures.uuid() + const headers = { + [constants.Headers.TENANT_ID]: tenantId, + } + const res = await config.request + .get(`/api/global/configs/checklist`) + .set(headers) + expect(res.headers[constants.Headers.TENANT_ID]).toBe(tenantId) + }) + + it("should get tenant id from query param", async () => { + const tenantId = structures.uuid() + const res = await config.request.get( + `/api/global/configs/checklist?tenantId=${tenantId}` + ) + expect(res.headers[constants.Headers.TENANT_ID]).toBe(tenantId) + }) + + it("should get tenant id from subdomain", async () => { + const tenantId = structures.uuid() + const headers = { + host: `${tenantId}.localhost:10000`, + } + const res = await config.request + .get(`/api/global/configs/checklist`) + .set(headers) + expect(res.headers[constants.Headers.TENANT_ID]).toBe(tenantId) + }) + + it("should get tenant id from path variable", async () => { + const user = await config.createTenant() + const res = await config.request + .post(`/api/global/auth/${user.tenantId}/login`) + .send({ + username: user.email, + password: user.password, + }) + expect(res.headers[constants.Headers.TENANT_ID]).toBe(user.tenantId) + }) + + it("should throw when no tenant id is found", async () => { + const res = await config.request.get(`/api/global/configs/checklist`) + expect(res.status).toBe(403) + expect(res.text).toBe("Tenant id not set") + expect(res.headers[constants.Headers.TENANT_ID]).toBe(undefined) + }) +}) diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index f52a852ebf..015ebb6258 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -8,34 +8,35 @@ import { Config } from "../constants" import { users, tenancy, - Cookie, - Header, sessions, auth, + constants, + env as coreEnv, } from "@budibase/backend-core" -import { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures" -import structures from "./structures" +import structures, { TENANT_ID, TENANT_1, CSRF_TOKEN } from "./structures" import { CreateUserResponse, User, AuthToken } from "@budibase/types" +import API from "./api" enum Mode { - ACCOUNT = "account", + CLOUD = "cloud", SELF = "self", } class TestConfiguration { server: any request: any + api: API defaultUser?: User tenant1User?: User constructor( opts: { openServer: boolean; mode: Mode } = { openServer: true, - mode: Mode.ACCOUNT, + mode: Mode.CLOUD, } ) { - if (opts.mode === Mode.ACCOUNT) { - this.modeAccount() + if (opts.mode === Mode.CLOUD) { + this.modeCloud() } else if (opts.mode === Mode.SELF) { this.modeSelf() } @@ -46,6 +47,8 @@ class TestConfiguration { // we need the request for logging in, involves cookies, hard to fake this.request = supertest(this.server) } + + this.api = new API(this) } getRequest() { @@ -54,20 +57,24 @@ class TestConfiguration { // MODES - modeAccount = () => { - env.SELF_HOSTED = false - // @ts-ignore - env.MULTI_TENANCY = true - // @ts-ignore - env.DISABLE_ACCOUNT_PORTAL = false + setMultiTenancy = (value: boolean) => { + env._set("MULTI_TENANCY", value) + coreEnv._set("MULTI_TENANCY", value) + } + + setSelfHosted = (value: boolean) => { + env._set("SELF_HOSTED", value) + coreEnv._set("SELF_HOSTED", value) + } + + modeCloud = () => { + this.setSelfHosted(false) + this.setMultiTenancy(true) } modeSelf = () => { - env.SELF_HOSTED = true - // @ts-ignore - env.MULTI_TENANCY = false - // @ts-ignore - env.DISABLE_ACCOUNT_PORTAL = true + this.setSelfHosted(true) + this.setMultiTenancy(false) } // UTILS @@ -114,6 +121,25 @@ class TestConfiguration { // TENANCY + createTenant = async (): Promise => { + // create user / new tenant + const res = await this.api.users.createAdminUser() + + // return the created user + const userRes = await this.api.users.getUser(res.userId, { + headers: { + ...this.internalAPIHeaders(), + [constants.Header.TENANT_ID]: res.tenantId, + }, + }) + + // create a session for the new user + const user = userRes.body + await this.createSession(user) + + return user + } + getTenantId() { try { return tenancy.getTenantId() @@ -122,7 +148,69 @@ class TestConfiguration { } } - // USER / AUTH + // AUTH + + async _createSession({ + userId, + tenantId, + }: { + userId: string + tenantId: string + }) { + await sessions.createASession(userId!, { + sessionId: "sessionid", + tenantId: tenantId, + csrfToken: CSRF_TOKEN, + }) + } + + async createSession(user: User) { + return this._createSession({ userId: user._id!, tenantId: user.tenantId }) + } + + cookieHeader(cookies: any) { + if (!Array.isArray(cookies)) { + cookies = [cookies] + } + return { + Cookie: cookies, + } + } + + authHeaders(user: User) { + const authToken: AuthToken = { + userId: user._id!, + sessionId: "sessionid", + tenantId: user.tenantId, + } + const authCookie = auth.jwt.sign(authToken, env.JWT_SECRET) + return { + Accept: "application/json", + ...this.cookieHeader([`${constants.Cookie.Auth}=${authCookie}`]), + [constants.Header.CSRF_TOKEN]: CSRF_TOKEN, + } + } + + defaultHeaders() { + const tenantId = this.getTenantId() + if (tenantId === TENANT_ID) { + return this.authHeaders(this.defaultUser!) + } else if (tenantId === TENANT_1) { + return this.authHeaders(this.tenant1User!) + } else { + throw new Error("could not determine auth headers to use") + } + } + + internalAPIHeaders() { + return { [constants.Header.API_KEY]: env.INTERNAL_API_KEY } + } + + adminOnlyResponse = () => { + return { message: "Admin user only endpoint.", status: 403 } + } + + // USERS async createDefaultUser() { const user = structures.users.adminUser({ @@ -140,45 +228,6 @@ class TestConfiguration { this.tenant1User = await this.createUser(user) } - async createSession(user: User) { - await sessions.createASession(user._id!, { - sessionId: "sessionid", - tenantId: user.tenantId, - csrfToken: CSRF_TOKEN, - }) - } - - cookieHeader(cookies: any) { - return { - Cookie: [cookies], - } - } - - authHeaders(user: User) { - const authToken: AuthToken = { - userId: user._id!, - sessionId: "sessionid", - tenantId: user.tenantId, - } - const authCookie = auth.jwt.sign(authToken, env.JWT_SECRET) - return { - Accept: "application/json", - ...this.cookieHeader([`${Cookie.Auth}=${authCookie}`]), - [Header.CSRF_TOKEN]: CSRF_TOKEN, - } - } - - defaultHeaders() { - const tenantId = this.getTenantId() - if (tenantId === TENANT_ID) { - return this.authHeaders(this.defaultUser!) - } else if (tenantId === TENANT_1) { - return this.authHeaders(this.tenant1User!) - } else { - throw new Error("could not determine auth headers to use") - } - } - async getUser(email: string): Promise { return tenancy.doInTenant(this.getTenantId(), () => { return users.getGlobalUserByEmail(email) @@ -242,7 +291,7 @@ class TestConfiguration { getOIDConfigCookie(configId: string) { const token = auth.jwt.sign(configId, env.JWT_SECRET) - return this.cookieHeader([[`${Cookie.OIDC_CONFIG}=${token}`]]) + return this.cookieHeader([[`${constants.Cookie.OIDC_CONFIG}=${token}`]]) } async saveOIDCConfig() { diff --git a/packages/worker/src/tests/api/accounts.ts b/packages/worker/src/tests/api/accounts.ts index fe6bf31192..bc6d055b77 100644 --- a/packages/worker/src/tests/api/accounts.ts +++ b/packages/worker/src/tests/api/accounts.ts @@ -1,20 +1,17 @@ import { Account, AccountMetadata } from "@budibase/types" import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" -export class AccountAPI { - config: TestConfiguration - request: any - +export class AccountAPI extends TestAPI { constructor(config: TestConfiguration) { - this.config = config - this.request = config.request + super(config) } saveMetadata = async (account: Account) => { const res = await this.request .put(`/api/system/accounts/${account.accountId}/metadata`) .send(account) - .set(this.config.defaultHeaders()) + .set(this.config.internalAPIHeaders()) .expect("Content-Type", /json/) .expect(200) return res.body as AccountMetadata @@ -23,6 +20,6 @@ export class AccountAPI { destroyMetadata = (accountId: string) => { return this.request .del(`/api/system/accounts/${accountId}/metadata`) - .set(this.config.defaultHeaders()) + .set(this.config.internalAPIHeaders()) } } diff --git a/packages/worker/src/tests/api/auth.ts b/packages/worker/src/tests/api/auth.ts index 204ae9f5dd..dda50976bd 100644 --- a/packages/worker/src/tests/api/auth.ts +++ b/packages/worker/src/tests/api/auth.ts @@ -1,12 +1,9 @@ import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" -export class AuthAPI { - config: TestConfiguration - request: any - +export class AuthAPI extends TestAPI { constructor(config: TestConfiguration) { - this.config = config - this.request = config.request + super(config) } updatePassword = (code: string) => { diff --git a/packages/worker/src/tests/api/base.ts b/packages/worker/src/tests/api/base.ts new file mode 100644 index 0000000000..c1263ed5cb --- /dev/null +++ b/packages/worker/src/tests/api/base.ts @@ -0,0 +1,16 @@ +import TestConfiguration from "../TestConfiguration" + +export interface TestAPIOpts { + headers?: any + status?: number +} + +export abstract class TestAPI { + config: TestConfiguration + request: any + + protected constructor(config: TestConfiguration) { + this.config = config + this.request = config.request + } +} diff --git a/packages/worker/src/tests/api/configs.ts b/packages/worker/src/tests/api/configs.ts index 3a3c433fa0..10413dfdd6 100644 --- a/packages/worker/src/tests/api/configs.ts +++ b/packages/worker/src/tests/api/configs.ts @@ -1,12 +1,9 @@ import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" -export class ConfigAPI { - config: TestConfiguration - request: any - +export class ConfigAPI extends TestAPI { constructor(config: TestConfiguration) { - this.config = config - this.request = config.request + super(config) } getConfigChecklist = () => { @@ -26,10 +23,20 @@ export class ConfigAPI { .expect(200) } - OIDCCallback = (configId: string) => { + OIDCCallback = (configId: string, preAuthRes: any) => { + const cookie = this.config.cookieHeader(preAuthRes.get("set-cookie")) + const setKoaSession = cookie.Cookie.find((c: string) => + c.includes("koa:sess") + ) + const koaSession = setKoaSession.split("=")[1] + "==" + const sessionContent = JSON.parse( + Buffer.from(koaSession, "base64").toString("utf-8") + ) + const handle = sessionContent["openidconnect:localhost"].state.handle return this.request .get(`/api/global/auth/${this.config.getTenantId()}/oidc/callback`) - .set(this.config.getOIDConfigCookie(configId)) + .query({ code: "test", state: handle }) + .set(cookie) } getOIDCConfig = (configId: string) => { diff --git a/packages/worker/src/tests/api/email.ts b/packages/worker/src/tests/api/email.ts index ea026c22ac..ba7c7dbec0 100644 --- a/packages/worker/src/tests/api/email.ts +++ b/packages/worker/src/tests/api/email.ts @@ -1,12 +1,9 @@ import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" -export class EmailAPI { - config: TestConfiguration - request: any - +export class EmailAPI extends TestAPI { constructor(config: TestConfiguration) { - this.config = config - this.request = config.request + super(config) } sendEmail = (purpose: string) => { diff --git a/packages/worker/src/tests/api/environment.ts b/packages/worker/src/tests/api/environment.ts new file mode 100644 index 0000000000..d9f82c5f0d --- /dev/null +++ b/packages/worker/src/tests/api/environment.ts @@ -0,0 +1,15 @@ +import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" + +export class EnvironmentAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + getEnvironment = () => { + return this.request + .get(`/api/system/environment`) + .expect("Content-Type", /json/) + .expect(200) + } +} diff --git a/packages/worker/src/tests/api/index.ts b/packages/worker/src/tests/api/index.ts index a12e489a78..bc0271b9c6 100644 --- a/packages/worker/src/tests/api/index.ts +++ b/packages/worker/src/tests/api/index.ts @@ -5,6 +5,11 @@ import { ConfigAPI } from "./configs" import { EmailAPI } from "./email" import { SelfAPI } from "./self" import { UserAPI } from "./users" +import { EnvironmentAPI } from "./environment" +import { MigrationAPI } from "./migrations" +import { StatusAPI } from "./status" +import { RestoreAPI } from "./restore" +import { TenantAPI } from "./tenants" export default class API { accounts: AccountAPI @@ -13,6 +18,11 @@ export default class API { emails: EmailAPI self: SelfAPI users: UserAPI + environment: EnvironmentAPI + migrations: MigrationAPI + status: StatusAPI + restore: RestoreAPI + tenants: TenantAPI constructor(config: TestConfiguration) { this.accounts = new AccountAPI(config) @@ -21,5 +31,10 @@ export default class API { this.emails = new EmailAPI(config) this.self = new SelfAPI(config) this.users = new UserAPI(config) + this.environment = new EnvironmentAPI(config) + this.migrations = new MigrationAPI(config) + this.status = new StatusAPI(config) + this.restore = new RestoreAPI(config) + this.tenants = new TenantAPI(config) } } diff --git a/packages/worker/src/tests/api/migrations.ts b/packages/worker/src/tests/api/migrations.ts new file mode 100644 index 0000000000..6038cbd5d8 --- /dev/null +++ b/packages/worker/src/tests/api/migrations.ts @@ -0,0 +1,22 @@ +import TestConfiguration from "../TestConfiguration" +import { TestAPI, TestAPIOpts } from "./base" + +export class MigrationAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + runMigrations = (opts?: TestAPIOpts) => { + return this.request + .post(`/api/system/migrations/run`) + .set(opts?.headers ? opts.headers : this.config.internalAPIHeaders()) + .expect(opts?.status ? opts.status : 200) + } + + getMigrationDefinitions = (opts?: TestAPIOpts) => { + return this.request + .get(`/api/system/migrations/definitions`) + .set(opts?.headers ? opts.headers : this.config.internalAPIHeaders()) + .expect(opts?.status ? opts.status : 200) + } +} diff --git a/packages/worker/src/tests/api/restore.ts b/packages/worker/src/tests/api/restore.ts new file mode 100644 index 0000000000..6069c20185 --- /dev/null +++ b/packages/worker/src/tests/api/restore.ts @@ -0,0 +1,14 @@ +import TestConfiguration from "../TestConfiguration" +import { TestAPI, TestAPIOpts } from "./base" + +export class RestoreAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + restored = (opts?: TestAPIOpts) => { + return this.request + .post(`/api/system/restored`) + .expect(opts?.status ? opts.status : 200) + } +} diff --git a/packages/worker/src/tests/api/self.ts b/packages/worker/src/tests/api/self.ts index b1cd4a48c6..dcc6c1a98b 100644 --- a/packages/worker/src/tests/api/self.ts +++ b/packages/worker/src/tests/api/self.ts @@ -1,13 +1,10 @@ import TestConfiguration from "../TestConfiguration" import { User } from "@budibase/types" +import { TestAPI } from "./base" -export class SelfAPI { - config: TestConfiguration - request: any - +export class SelfAPI extends TestAPI { constructor(config: TestConfiguration) { - this.config = config - this.request = config.request + super(config) } updateSelf = (user: User) => { @@ -18,4 +15,12 @@ export class SelfAPI { .expect("Content-Type", /json/) .expect(200) } + + getSelf = (user: User) => { + return this.request + .get(`/api/global/self`) + .set(this.config.authHeaders(user)) + .expect("Content-Type", /json/) + .expect(200) + } } diff --git a/packages/worker/src/tests/api/status.ts b/packages/worker/src/tests/api/status.ts new file mode 100644 index 0000000000..5b0f77efc6 --- /dev/null +++ b/packages/worker/src/tests/api/status.ts @@ -0,0 +1,12 @@ +import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" + +export class StatusAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + getStatus = () => { + return this.request.get(`/api/system/status`).expect(200) + } +} diff --git a/packages/worker/src/tests/api/tenants.ts b/packages/worker/src/tests/api/tenants.ts new file mode 100644 index 0000000000..b28b55697f --- /dev/null +++ b/packages/worker/src/tests/api/tenants.ts @@ -0,0 +1,15 @@ +import TestConfiguration from "../TestConfiguration" +import { TestAPI, TestAPIOpts } from "./base" + +export class TenantAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + delete = (tenantId: string, opts?: TestAPIOpts) => { + return this.request + .delete(`/api/system/tenants/${tenantId}`) + .set(opts?.headers) + .expect(opts?.status ? opts.status : 204) + } +} diff --git a/packages/worker/src/tests/api/users.ts b/packages/worker/src/tests/api/users.ts index 3677bfffc6..c9c5e33403 100644 --- a/packages/worker/src/tests/api/users.ts +++ b/packages/worker/src/tests/api/users.ts @@ -3,16 +3,16 @@ import { BulkUserRequest, InviteUsersRequest, User, + CreateAdminUserRequest, } from "@budibase/types" +import * as structures from "../structures" +import { generator } from "@budibase/backend-core/tests" import TestConfiguration from "../TestConfiguration" +import { TestAPI, TestAPIOpts } from "./base" -export class UserAPI { - config: TestConfiguration - request: any - +export class UserAPI extends TestAPI { constructor(config: TestConfiguration) { - this.config = config - this.request = config.request + super(config) } // INVITE @@ -91,6 +91,30 @@ export class UserAPI { // USER + createAdminUser = async ( + request?: CreateAdminUserRequest, + opts?: TestAPIOpts + ) => { + if (!request) { + request = { + email: structures.email(), + password: generator.string(), + tenantId: structures.uuid(), + } + } + const res = await this.request + .post(`/api/global/users/init`) + .send(request) + .set(this.config.internalAPIHeaders()) + .expect("Content-Type", /json/) + .expect(opts?.status ? opts.status : 200) + + return { + ...request, + userId: res.body._id, + } + } + saveUser = (user: User, status?: number) => { return this.request .post(`/api/global/users`) @@ -107,4 +131,12 @@ export class UserAPI { .expect("Content-Type", /json/) .expect(status ? status : 200) } + + getUser = (userId: string, opts?: TestAPIOpts) => { + return this.request + .get(`/api/global/users/${userId}`) + .set(opts?.headers ? opts.headers : this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(opts?.status ? opts.status : 200) + } } diff --git a/packages/worker/src/tests/index.ts b/packages/worker/src/tests/index.ts index 3d9c06f5e6..d153390da0 100644 --- a/packages/worker/src/tests/index.ts +++ b/packages/worker/src/tests/index.ts @@ -1,10 +1,14 @@ import mocks from "./mocks" +import { generator } from "@budibase/backend-core/tests" import TestConfiguration from "./TestConfiguration" import structures from "./structures" import API from "./api" +import { v4 as uuid } from "uuid" const pkg = { structures, + generator, + uuid, TENANT_1: structures.TENANT_1, mocks, TestConfiguration, diff --git a/packages/worker/src/tests/jestSetup.ts b/packages/worker/src/tests/jestSetup.ts index 2af4deff8e..fee704ae45 100644 --- a/packages/worker/src/tests/jestSetup.ts +++ b/packages/worker/src/tests/jestSetup.ts @@ -1,6 +1,6 @@ import env from "../environment" -env._set("SELF_HOSTED", "1") +env._set("SELF_HOSTED", "0") env._set("NODE_ENV", "jest") env._set("JWT_SECRET", "test-jwtsecret") env._set("LOG_LEVEL", "silent") @@ -8,9 +8,15 @@ env._set("MULTI_TENANCY", true) env._set("MINIO_URL", "http://localhost") env._set("MINIO_ACCESS_KEY", "test") env._set("MINIO_SECRET_KEY", "test") +env._set("PLATFORM_URL", "http://localhost:10000") +env._set("INTERNAL_API_KEY", "test") +env._set("DISABLE_ACCOUNT_PORTAL", false) import { mocks } from "@budibase/backend-core/tests" +// must explicitly enable fetch mock +mocks.fetch.enable() + // mock all dates to 2020-01-01T00:00:00.000Z // use tk.reset() to use real dates in individual tests const tk = require("timekeeper") diff --git a/packages/worker/src/tests/structures/accounts.ts b/packages/worker/src/tests/structures/accounts.ts deleted file mode 100644 index df6b993684..0000000000 --- a/packages/worker/src/tests/structures/accounts.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Account, AuthType, Hosting, CloudAccount } from "@budibase/types" -import { v4 as uuid } from "uuid" -import { utils } from "@budibase/backend-core" - -export const account = (): Account => { - return { - email: `${uuid()}@test.com`, - tenantId: utils.newid(), - hosting: Hosting.SELF, - authType: AuthType.SSO, - accountId: uuid(), - createdAt: Date.now(), - verified: true, - verificationSent: true, - tier: "FREE", - } -} - -export const cloudAccount = (): CloudAccount => { - return { - ...account(), - budibaseUserId: uuid(), - } -} diff --git a/packages/worker/src/tests/structures/index.ts b/packages/worker/src/tests/structures/index.ts index a3029b0105..3a4c3324df 100644 --- a/packages/worker/src/tests/structures/index.ts +++ b/packages/worker/src/tests/structures/index.ts @@ -1,16 +1,18 @@ +import { structures } from "@budibase/backend-core/tests" import configs from "./configs" import * as users from "./users" import * as groups from "./groups" -import * as accounts from "./accounts" +import { v4 as uuid } from "uuid" const TENANT_ID = "default" const TENANT_1 = "tenant1" const CSRF_TOKEN = "e3727778-7af0-4226-b5eb-f43cbe60a306" const pkg = { + ...structures, + uuid, configs, users, - accounts, TENANT_ID, TENANT_1, CSRF_TOKEN, diff --git a/packages/worker/src/tests/structures/users.ts b/packages/worker/src/tests/structures/users.ts index 4bf24ec780..bef9f38586 100644 --- a/packages/worker/src/tests/structures/users.ts +++ b/packages/worker/src/tests/structures/users.ts @@ -5,6 +5,7 @@ import { v4 as uuid } from "uuid" export const newEmail = () => { return `${uuid()}@test.com` } + export const user = (userProps?: any): User => { return { email: newEmail(), diff --git a/packages/worker/src/utilities/email.js b/packages/worker/src/utilities/email.ts similarity index 100% rename from packages/worker/src/utilities/email.js rename to packages/worker/src/utilities/email.ts diff --git a/packages/worker/src/utilities/redis.js b/packages/worker/src/utilities/redis.ts similarity index 100% rename from packages/worker/src/utilities/redis.js rename to packages/worker/src/utilities/redis.ts diff --git a/packages/worker/src/utilities/templates.js b/packages/worker/src/utilities/templates.ts similarity index 100% rename from packages/worker/src/utilities/templates.js rename to packages/worker/src/utilities/templates.ts diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index cfdba32e7a..1bcc1f0534 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -470,12 +470,12 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@budibase/backend-core@2.1.22-alpha.3": - version "2.1.22-alpha.3" - resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.1.22-alpha.3.tgz#5cdff781cf6a448677304aa80a9658e9e51c36a5" - integrity sha512-je1mKTb1h9f+tyCAiFNJykg9O8ZM3smVQ3JJf24rXDYvLYN8GG8hxZ5fOJuWoeF/aGu8m+w3v3+EdWuTlQPcKg== +"@budibase/backend-core@2.1.22-alpha.6": + version "2.1.22-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/backend-core/-/backend-core-2.1.22-alpha.6.tgz#e9886620fcbee6fe0348365cb96ac6484fc5f8fd" + integrity sha512-P3NSgNuQXKmdeT8MLfeCji3ibRSeIIMSOQeNSQBWpaOTA69rpXQk753lHRwUWMpqil/ybsOuE/h1/Y3eTa+/UA== dependencies: - "@budibase/types" "2.1.22-alpha.3" + "@budibase/types" "2.1.22-alpha.6" "@shopify/jest-koa-mocks" "5.0.1" "@techpass/passport-openidconnect" "0.3.2" aws-sdk "2.1030.0" @@ -507,22 +507,22 @@ uuid "8.3.2" zlib "1.0.5" -"@budibase/pro@2.1.22-alpha.3": - version "2.1.22-alpha.3" - resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.1.22-alpha.3.tgz#8f87ffc1c6c158ad467216d5cfcb7844e9a08363" - integrity sha512-IE0eHPswBycPYzvduZCAp4T6U8t1qTcT1A9jVJ8TeX2jiL22hPOQ+8nVNIaqTGfQvJ7foGiqtDcugQbk3QLGOg== +"@budibase/pro@2.1.22-alpha.6": + version "2.1.22-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/pro/-/pro-2.1.22-alpha.6.tgz#e4d8238f1727eab85ac72b3ce2fcaec2a5c18456" + integrity sha512-1CKqQ2HMX+/5p24aHpPlUgxoMjKRZxRoyK5fPD/X35Z0mDj+9Ohny3oqbF1fC20pl/20bmcaE4J+q2ph/pbxdQ== dependencies: - "@budibase/backend-core" "2.1.22-alpha.3" - "@budibase/types" "2.1.22-alpha.3" + "@budibase/backend-core" "2.1.22-alpha.6" + "@budibase/types" "2.1.22-alpha.6" "@koa/router" "8.0.8" bull "4.10.1" joi "17.6.0" node-fetch "^2.6.1" -"@budibase/types@2.1.22-alpha.3": - version "2.1.22-alpha.3" - resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.1.22-alpha.3.tgz#30cfcb6a58989a94dfba43ec7f9052e4855b6803" - integrity sha512-3WKZ5DVkygUi9H3KJTL8geQf9cjmssM8tsbDEFi8KJHogfszJPia9di/DnN8rd/CXbsx3Zbsbe8LHiAAADz5og== +"@budibase/types@2.1.22-alpha.6": + version "2.1.22-alpha.6" + resolved "https://registry.yarnpkg.com/@budibase/types/-/types-2.1.22-alpha.6.tgz#a280321373c26a5bf1a52ed119de9178292893c0" + integrity sha512-rVrhs9u7OTzlCxgUFqBu5H6jsMHhwH8uduPhT8Eo7J+Wr4J/0Io7WeuAt1egK6t83JiEPxDBH3WLzAH+04hPVA== "@cspotcode/source-map-support@^0.8.0": version "0.8.1" @@ -1284,6 +1284,13 @@ resolved "https://registry.yarnpkg.com/@types/json-buffer/-/json-buffer-3.0.0.tgz#85c1ff0f0948fc159810d4b5be35bf8c20875f64" integrity sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ== +"@types/jsonwebtoken@8.5.1": + version "8.5.1" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#56958cb2d80f6d74352bd2e501a018e2506a8a84" + integrity sha512-rNAPdomlIUX0i0cg2+I+Q1wOUr531zHBQ+cV/28PJ39bSPKjahatZZ2LMuhiguETkCgLVzfruw/ZvNMNkKoSzw== + dependencies: + "@types/node" "*" + "@types/keygrip@*": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" @@ -1334,6 +1341,14 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== +"@types/node-fetch@2.6.1": + version "2.6.1" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975" + integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA== + dependencies: + "@types/node" "*" + form-data "^3.0.0" + "@types/node@*": version "17.0.41" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.41.tgz#1607b2fd3da014ae5d4d1b31bc792a39348dfb9b" @@ -3412,6 +3427,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" diff --git a/scripts/localdomain.sh b/scripts/localdomain.sh new file mode 100755 index 0000000000..d32dbcc116 --- /dev/null +++ b/scripts/localdomain.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +enable=$1 +domain=$2 + +if [ "$enable" = "enable" ]; then + lerna run env:localdomain:enable -- "$domain" + cd ../account-portal + yarn env:localdomain:enable "$domain" + cd - +else + lerna run env:localdomain:disable + cd ../account-portal + yarn env:localdomain:disable + cd - +fi \ No newline at end of file