From 0718f998500964f51a3a2bda5b7edfbc998fc3ef Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Mon, 25 Oct 2021 16:59:09 +0100 Subject: [PATCH 1/6] Prevent non builder from accessing dev apps --- .vscode/launch.json | 34 ++++------ packages/server/scripts/jestSetup.js | 2 + .../server/src/api/routes/tests/auth.spec.js | 3 +- .../src/api/routes/tests/permissions.spec.js | 3 + .../server/src/api/routes/tests/query.spec.js | 4 +- .../src/api/routes/tests/routing.spec.js | 32 ++++++--- .../server/src/api/routes/tests/user.spec.js | 2 +- .../routes/tests/utilities/TestFunctions.js | 16 ++--- .../src/api/routes/tests/webhook.spec.js | 5 +- packages/server/src/middleware/currentapp.js | 8 ++- .../src/tests/utilities/TestConfiguration.js | 66 +++++++++++++------ .../server/src/tests/utilities/controllers.js | 1 + .../src/utilities/queue/inMemoryQueue.js | 7 ++ .../worker/src/api/routes/tests/auth.spec.js | 2 +- 14 files changed, 118 insertions(+), 67 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 1587bfc537..34951b6310 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,39 +4,27 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "program": "${workspaceFolder}/app.js", - "skipFiles": [ - "/**" - ] - }, - { - "type": "node", - "request": "launch", - "name": "Debug External", - "program": "${workspaceFolder}/packages/cli/bin/budi", - "args": [], - "cwd":"C:/code/my-apps", - "console": "externalTerminal" - }, { "name": "Budibase Server", "type": "node", "request": "launch", - "runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"], - "args": ["${workspaceFolder}/packages/server/src/index.ts"], + "runtimeArgs": [ + "--nolazy", + "-r", + "ts-node/register/transpile-only" + ], + "args": [ + "${workspaceFolder}/packages/server/src/index.ts" + ], "cwd": "${workspaceFolder}/packages/server" - }, - { + }, + { "name": "Budibase Worker", "type": "node", "request": "launch", "program": "${workspaceFolder}/packages/worker/src/index.js", "cwd": "${workspaceFolder}/packages/worker" - } + } ], "compounds": [ { diff --git a/packages/server/scripts/jestSetup.js b/packages/server/scripts/jestSetup.js index 1f3551bf5f..6999d92315 100644 --- a/packages/server/scripts/jestSetup.js +++ b/packages/server/scripts/jestSetup.js @@ -8,3 +8,5 @@ env._set("CLIENT_ID", "test-client-id") env._set("BUDIBASE_DIR", tmpdir("budibase-unittests")) env._set("LOG_LEVEL", "silent") env._set("PORT", 0) + +global.console.log = jest.fn() // console.log are ignored in tests diff --git a/packages/server/src/api/routes/tests/auth.spec.js b/packages/server/src/api/routes/tests/auth.spec.js index e8abc2abe8..c50780a8d5 100644 --- a/packages/server/src/api/routes/tests/auth.spec.js +++ b/packages/server/src/api/routes/tests/auth.spec.js @@ -13,8 +13,7 @@ describe("/authenticate", () => { describe("fetch self", () => { it("should be able to fetch self", async () => { - await config.createUser("test@test.com", "p4ssw0rd") - const headers = await config.login("test@test.com", "p4ssw0rd", { userId: "us_uuid1" }) + const headers = await config.login() const res = await request .get(`/api/self`) .set(headers) diff --git a/packages/server/src/api/routes/tests/permissions.spec.js b/packages/server/src/api/routes/tests/permissions.spec.js index ce9f24a572..0304e551cc 100644 --- a/packages/server/src/api/routes/tests/permissions.spec.js +++ b/packages/server/src/api/routes/tests/permissions.spec.js @@ -89,6 +89,9 @@ describe("/permission", () => { describe("check public user allowed", () => { it("should be able to read the row", async () => { + // replicate changes before checking permissions + await config.deploy() + const res = await request .get(`/api/${table._id}/rows`) .set(config.publicHeaders()) diff --git a/packages/server/src/api/routes/tests/query.spec.js b/packages/server/src/api/routes/tests/query.spec.js index 716817509b..8dc153f28e 100644 --- a/packages/server/src/api/routes/tests/query.spec.js +++ b/packages/server/src/api/routes/tests/query.spec.js @@ -94,9 +94,9 @@ describe("/queries", () => { const query = await config.createQuery() const res = await request .get(`/api/queries/${query._id}`) - .set(await config.roleHeaders({})) - .expect("Content-Type", /json/) + .set(await config.defaultHeaders()) .expect(200) + .expect("Content-Type", /json/) expect(res.body.fields).toBeUndefined() expect(res.body.parameters).toBeUndefined() expect(res.body.schema).toBeUndefined() diff --git a/packages/server/src/api/routes/tests/routing.spec.js b/packages/server/src/api/routes/tests/routing.spec.js index 81f56a939d..079fe0dedc 100644 --- a/packages/server/src/api/routes/tests/routing.spec.js +++ b/packages/server/src/api/routes/tests/routing.spec.js @@ -21,16 +21,31 @@ describe("/routing", () => { screen2.routing.roleId = BUILTIN_ROLE_IDS.POWER screen2.routing.route = route screen2 = await config.createScreen(screen2) + await config.deploy() }) describe("fetch", () => { + it("prevents a public user from accessing development app", async () => { + await request + .get(`/api/routing/client`) + .set(config.publicHeaders({ prodApp: false })) + .expect(302) + }) + + it("prevents a non builder from accessing development app", async () => { + await request + .get(`/api/routing/client`) + .set(await config.roleHeaders({ + roleId: BUILTIN_ROLE_IDS.BASIC, + prodApp: false + })) + .expect(302) + }) it("returns the correct routing for basic user", async () => { const res = await request .get(`/api/routing/client`) .set(await config.roleHeaders({ - email: "basic@test.com", - roleId: BUILTIN_ROLE_IDS.BASIC, - builder: false + roleId: BUILTIN_ROLE_IDS.BASIC })) .expect("Content-Type", /json/) .expect(200) @@ -49,9 +64,7 @@ describe("/routing", () => { const res = await request .get(`/api/routing/client`) .set(await config.roleHeaders({ - email: "basic@test.com", - roleId: BUILTIN_ROLE_IDS.POWER, - builder: false, + roleId: BUILTIN_ROLE_IDS.POWER })) .expect("Content-Type", /json/) .expect(200) @@ -71,9 +84,12 @@ describe("/routing", () => { it("should fetch all routes for builder", async () => { const res = await request .get(`/api/routing`) - .set(config.defaultHeaders()) - .expect("Content-Type", /json/) + .set(await config.roleHeaders({ + roleId: BUILTIN_ROLE_IDS.POWER, + builder: true, + })) .expect(200) + .expect("Content-Type", /json/) expect(res.body.routes).toBeDefined() expect(res.body.routes[route].subpaths[route]).toBeDefined() const subpath = res.body.routes[route].subpaths[route] diff --git a/packages/server/src/api/routes/tests/user.spec.js b/packages/server/src/api/routes/tests/user.spec.js index 48117fbe4b..a9e98f2555 100644 --- a/packages/server/src/api/routes/tests/user.spec.js +++ b/packages/server/src/api/routes/tests/user.spec.js @@ -38,7 +38,7 @@ describe("/users", () => { }) it("should apply authorization to endpoint", async () => { - await config.createUser("brenda@brenda.com", "brendas_password") + await config.createUser() await checkPermissionsEndpoint({ config, request, diff --git a/packages/server/src/api/routes/tests/utilities/TestFunctions.js b/packages/server/src/api/routes/tests/utilities/TestFunctions.js index 87190c3f76..857e902b66 100644 --- a/packages/server/src/api/routes/tests/utilities/TestFunctions.js +++ b/packages/server/src/api/routes/tests/utilities/TestFunctions.js @@ -50,9 +50,10 @@ exports.createRequest = (request, method, url, body) => { } exports.checkBuilderEndpoint = async ({ config, method, url, body }) => { - const headers = await config.login("test@test.com", "test", { + const headers = await config.login({ userId: "us_fail", builder: false, + prodApp: true, }) await exports .createRequest(config.request, method, url, body) @@ -68,11 +69,9 @@ exports.checkPermissionsEndpoint = async ({ passRole, failRole, }) => { - const password = "PASSWORD" - let user = await config.createUser("pass@budibase.com", password, passRole) - const passHeader = await config.login("pass@budibase.com", password, { + const passHeader = await config.login({ roleId: passRole, - userId: user.globalId, + prodApp: true, }) await exports @@ -82,13 +81,12 @@ exports.checkPermissionsEndpoint = async ({ let failHeader if (failRole === BUILTIN_ROLE_IDS.PUBLIC) { - failHeader = config.publicHeaders() + failHeader = config.publicHeaders({ prodApp: true }) } else { - user = await config.createUser("fail@budibase.com", password, failRole) - failHeader = await config.login("fail@budibase.com", password, { + failHeader = await config.login({ roleId: failRole, - userId: user.globalId, builder: false, + prodApp: true, }) } diff --git a/packages/server/src/api/routes/tests/webhook.spec.js b/packages/server/src/api/routes/tests/webhook.spec.js index 75331ea4c7..ed2709a0e6 100644 --- a/packages/server/src/api/routes/tests/webhook.spec.js +++ b/packages/server/src/api/routes/tests/webhook.spec.js @@ -112,8 +112,11 @@ describe("/webhooks", () => { describe("trigger", () => { it("should allow triggering from public", async () => { + // replicate changes before checking webhook + await config.deploy() + const res = await request - .post(`/api/webhooks/trigger/${config.getAppId()}/${webhook._id}`) + .post(`/api/webhooks/trigger/${config.prodAppId}/${webhook._id}`) .expect("Content-Type", /json/) .expect(200) expect(res.body.message).toBeDefined() diff --git a/packages/server/src/middleware/currentapp.js b/packages/server/src/middleware/currentapp.js index a39d2eba0a..505bebc694 100644 --- a/packages/server/src/middleware/currentapp.js +++ b/packages/server/src/middleware/currentapp.js @@ -3,7 +3,7 @@ const { getAppId, setCookie, getCookie, clearCookie } = const { Cookies } = require("@budibase/auth").constants const { getRole } = require("@budibase/auth/roles") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") -const { generateUserMetadataID } = require("../db/utils") +const { generateUserMetadataID, isDevAppID } = require("../db/utils") const { dbExists } = require("@budibase/auth/db") const { isUserInAppTenant } = require("@budibase/auth/tenancy") const { getCachedSelf } = require("../utilities/global") @@ -35,6 +35,12 @@ module.exports = async (ctx, next) => { requestAppId = requestAppId || appId } + // deny access to application preview + if (isDevAppID(requestAppId) && (!ctx.user || !ctx.user.builder?.global)) { + clearCookie(ctx, Cookies.CurrentApp) + return ctx.redirect("/") + } + let appId, roleId = BUILTIN_ROLE_IDS.PUBLIC if (!ctx.user) { diff --git a/packages/server/src/tests/utilities/TestConfiguration.js b/packages/server/src/tests/utilities/TestConfiguration.js index 0eb3851d98..5f1d57467c 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.js +++ b/packages/server/src/tests/utilities/TestConfiguration.js @@ -26,7 +26,6 @@ auth.init(CouchDB) const GLOBAL_USER_ID = "us_uuid1" const EMAIL = "babs@babs.com" -const PASSWORD = "babs_password" class TestConfiguration { constructor(openServer = true) { @@ -67,13 +66,18 @@ class TestConfiguration { return request.body } - async globalUser(id = GLOBAL_USER_ID, builder = true, roles) { + async globalUser({ + id = GLOBAL_USER_ID, + builder = true, + email = EMAIL, + roles, + } = {}) { const db = getGlobalDB(TENANT_ID) let existing try { existing = await db.get(id) } catch (err) { - existing = {} + existing = { email } } const user = { _id: id, @@ -84,6 +88,8 @@ class TestConfiguration { await createASession(id, { sessionId: "sessionid", tenantId: TENANT_ID }) if (builder) { user.builder = { global: true } + } else { + user.builder = { global: false } } const resp = await db.put(user) return { @@ -132,12 +138,14 @@ class TestConfiguration { return headers } - publicHeaders() { + publicHeaders({ prodApp = true } = {}) { + const appId = prodApp ? this.prodAppId : this.appId + const headers = { Accept: "application/json", } - if (this.appId) { - headers[Headers.APP_ID] = this.appId + if (this.prodAppId) { + headers[Headers.APP_ID] = appId } return headers } @@ -146,17 +154,37 @@ class TestConfiguration { email = EMAIL, roleId = BUILTIN_ROLE_IDS.ADMIN, builder = false, - }) { - return this.login(email, PASSWORD, { roleId, builder }) + prodApp = true, + } = {}) { + return this.login({ email, roleId, builder, prodApp }) } async createApp(appName) { + // create dev app this.app = await this._req({ name: appName }, null, controllers.app.create) this.appId = this.app.appId + + // create production app + this.prodApp = await this.deploy() + this.prodAppId = this.prodApp.appId + + this.allApps.push(this.prodApp) this.allApps.push(this.app) + return this.app } + async deploy() { + const deployment = await this._req(null, null, controllers.deploy.deployApp) + const prodAppId = deployment.appId.replace("_dev", "") + const appPackage = await this._req( + null, + { appId: prodAppId }, + controllers.app.fetchAppPackage + ) + return appPackage.application + } + async updateTable(config = null) { config = config || basicTable() this.table = await this._req(config, null, controllers.table.save) @@ -313,9 +341,9 @@ class TestConfiguration { return await this._req(config, null, controllers.layout.save) } - async createUser(id = null) { + async createUser(id = null, email = EMAIL) { const globalId = !id ? `us_${Math.random()}` : `us_${id}` - const resp = await this.globalUser(globalId) + const resp = await this.globalUser({ id: globalId, email }) await userCache.invalidateUser(globalId) return { ...resp, @@ -323,21 +351,21 @@ class TestConfiguration { } } - async login(email, password, { roleId, userId, builder } = {}) { + async login({ roleId, userId, builder, prodApp = false } = {}) { + const appId = prodApp ? this.prodAppId : this.appId + userId = !userId ? `us_uuid1` : userId if (!this.request) { throw "Server has not been opened, cannot login." } // make sure the user exists in the global DB if (roleId !== BUILTIN_ROLE_IDS.PUBLIC) { - const appId = `app${this.getAppId().split("app_dev")[1]}` - await this.globalUser(userId, builder, { - [appId]: roleId, + await this.globalUser({ + userId, + builder, + roles: { [this.prodAppId]: roleId }, }) } - if (!email || !password) { - await this.createUser() - } await createASession(userId, { sessionId: "sessionid", tenantId: TENANT_ID, @@ -350,7 +378,7 @@ class TestConfiguration { } const app = { roleId: roleId, - appId: this.appId, + appId, } const authToken = jwt.sign(auth, env.JWT_SECRET) const appToken = jwt.sign(app, env.JWT_SECRET) @@ -363,7 +391,7 @@ class TestConfiguration { `${Cookies.Auth}=${authToken}`, `${Cookies.CurrentApp}=${appToken}`, ], - [Headers.APP_ID]: this.appId, + [Headers.APP_ID]: appId, } } } diff --git a/packages/server/src/tests/utilities/controllers.js b/packages/server/src/tests/utilities/controllers.js index b07754038f..8da6c97047 100644 --- a/packages/server/src/tests/utilities/controllers.js +++ b/packages/server/src/tests/utilities/controllers.js @@ -12,4 +12,5 @@ module.exports = { screen: require("../../api/controllers/screen"), webhook: require("../../api/controllers/webhook"), layout: require("../../api/controllers/layout"), + deploy: require("../../api/controllers/deploy"), } diff --git a/packages/server/src/utilities/queue/inMemoryQueue.js b/packages/server/src/utilities/queue/inMemoryQueue.js index d4b1d297d8..c57bac978c 100644 --- a/packages/server/src/utilities/queue/inMemoryQueue.js +++ b/packages/server/src/utilities/queue/inMemoryQueue.js @@ -82,6 +82,13 @@ class InMemoryQueue { // TODO: implement for testing console.log(cronJobId) } + + /** + * Implemented for tests + */ + getRepeatableJobs() { + return [] + } } module.exports = InMemoryQueue diff --git a/packages/worker/src/api/routes/tests/auth.spec.js b/packages/worker/src/api/routes/tests/auth.spec.js index dacff30ce3..bb66c547c4 100644 --- a/packages/worker/src/api/routes/tests/auth.spec.js +++ b/packages/worker/src/api/routes/tests/auth.spec.js @@ -24,7 +24,7 @@ describe("/api/global/auth", () => { // initially configure settings await config.saveSmtpConfig() await config.saveSettingsConfig() - await config.createUser("test@test.com") + await config.createUser() const res = await request .post(`/api/global/auth/${TENANT_ID}/reset`) .send({ From b8c10c9e9f9855b4d0084874dc0812fdf595f769 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Tue, 26 Oct 2021 09:42:19 +0100 Subject: [PATCH 2/6] Linting --- packages/server/src/middleware/currentapp.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/server/src/middleware/currentapp.js b/packages/server/src/middleware/currentapp.js index 505bebc694..01b9dcc248 100644 --- a/packages/server/src/middleware/currentapp.js +++ b/packages/server/src/middleware/currentapp.js @@ -36,7 +36,10 @@ module.exports = async (ctx, next) => { } // deny access to application preview - if (isDevAppID(requestAppId) && (!ctx.user || !ctx.user.builder?.global)) { + if ( + isDevAppID(requestAppId) && + (!ctx.user || !ctx.user.builder || !ctx.user.builder.global) + ) { clearCookie(ctx, Cookies.CurrentApp) return ctx.redirect("/") } From 2011e579a119ec3e5abdcc5be3853352c99cb516 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Tue, 26 Oct 2021 12:40:30 +0100 Subject: [PATCH 3/6] Ignore case when finding user by email --- packages/auth/src/db/constants.js | 1 + packages/auth/src/db/views.js | 2 +- .../passport/tests/third-party-common.spec.js | 19 +++++- .../middleware/passport/third-party-common.js | 7 +- packages/auth/src/migrations/index.js | 65 +++++++++++++++++++ packages/auth/src/utils.js | 11 +++- 6 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 packages/auth/src/migrations/index.js diff --git a/packages/auth/src/db/constants.js b/packages/auth/src/db/constants.js index 477968975a..ecdaae5bad 100644 --- a/packages/auth/src/db/constants.js +++ b/packages/auth/src/db/constants.js @@ -13,6 +13,7 @@ exports.DocumentTypes = { APP_DEV: `${PRE_APP}${exports.SEPARATOR}${PRE_DEV}`, APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`, ROLE: "role", + MIGRATIONS: "migrations", } exports.StaticDatabases = { diff --git a/packages/auth/src/db/views.js b/packages/auth/src/db/views.js index 1b48786e24..fd004ca0c2 100644 --- a/packages/auth/src/db/views.js +++ b/packages/auth/src/db/views.js @@ -21,7 +21,7 @@ exports.createUserEmailView = async db => { // if using variables in a map function need to inject them before use map: `function(doc) { if (doc._id.startsWith("${DocumentTypes.USER}")) { - emit(doc.email, doc._id) + emit(doc.email.toLowerCase(), doc._id) } }`, } diff --git a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js index e2ad9a9300..81b8c3e6b0 100644 --- a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js +++ b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js @@ -72,7 +72,6 @@ describe("third party common", () => { const expectUserIsSynced = (user, thirdPartyUser) => { expect(user.provider).toBe(thirdPartyUser.provider) - expect(user.email).toBe(thirdPartyUser.email) expect(user.firstName).toBe(thirdPartyUser.profile.name.givenName) expect(user.lastName).toBe(thirdPartyUser.profile.name.familyName) expect(user.thirdPartyProfile).toStrictEqual(thirdPartyUser.profile._json) @@ -135,6 +134,24 @@ describe("third party common", () => { }) }) + describe("exists by email with different casing", () => { + beforeEach(async () => { + id = generateGlobalUserID(newid()) // random id + email = thirdPartyUser.email.toUpperCase() // matching email except for casing + await createUser() + }) + + it("syncs and authenticates the user", async () => { + await authenticateThirdParty(thirdPartyUser, true, done, saveUser) + + const user = expectUserIsAuthenticated() + expectUserIsSynced(user, thirdPartyUser) + expectUserIsUpdated(user) + expect(user.email).toBe(thirdPartyUser.email.toUpperCase()) + }) + }) + + describe("exists by id", () => { beforeEach(async () => { id = generateGlobalUserID(thirdPartyUser.userId) // matching id diff --git a/packages/auth/src/middleware/passport/third-party-common.js b/packages/auth/src/middleware/passport/third-party-common.js index 54a5504712..b467c0b10b 100644 --- a/packages/auth/src/middleware/passport/third-party-common.js +++ b/packages/auth/src/middleware/passport/third-party-common.js @@ -66,12 +66,16 @@ exports.authenticateThirdParty = async function ( // setup a blank user using the third party id dbUser = { _id: userId, + email: thirdPartyUser.email, roles: {}, } } dbUser = await syncUser(dbUser, thirdPartyUser) + // never prompt for password reset + dbUser.forceResetPassword = false + // create or sync the user let response try { @@ -122,9 +126,6 @@ async function syncUser(user, thirdPartyUser) { user.provider = thirdPartyUser.provider user.providerType = thirdPartyUser.providerType - // email - user.email = thirdPartyUser.email - if (thirdPartyUser.profile) { const profile = thirdPartyUser.profile diff --git a/packages/auth/src/migrations/index.js b/packages/auth/src/migrations/index.js new file mode 100644 index 0000000000..ca06188a8a --- /dev/null +++ b/packages/auth/src/migrations/index.js @@ -0,0 +1,65 @@ +const { DocumentTypes } = require("../db/constants") +const { getGlobalDB } = require("../tenancy") + +exports.MIGRATION_DBS = { + GLOBAL_DB: "GLOBAL_DB", +} + +exports.MIGRATIONS = { + USER_EMAIL_VIEW_CASING: "user_email_view_casing", +} + +const DB_LOOKUP = { + [exports.MIGRATION_DBS.GLOBAL_DB]: [ + exports.MIGRATIONS.USER_EMAIL_VIEW_CASING, + ], +} + +const getMigrationsDoc = async db => { + // get the migrations doc + try { + return await db.get(DocumentTypes.MIGRATIONS) + } catch (err) { + if (err.status && err.status === 404) { + return { _id: DocumentTypes.MIGRATIONS } + } + } +} + +exports.migrateIfRequired = async (migrationDb, migrationName, migrateFn) => { + let db + if (migrationDb === exports.MIGRATION_DBS.GLOBAL_DB) { + db = getGlobalDB() + } else { + throw new Error(`Unrecognised migration db [${migrationDb}]`) + } + + if (!DB_LOOKUP[migrationDb].includes(migrationName)) { + throw new Error( + `Unrecognised migration name [${migrationName}] for db [${migrationDb}]` + ) + } + return tryMigrate(db, migrationName, migrateFn) +} + +const tryMigrate = async (db, migrationName, migrateFn) => { + try { + const doc = await getMigrationsDoc(db) + + // exit if the migration has been performed + if (doc[migrationName]) { + return + } + + console.log(`Performing migration: ${migrationName}`) + await migrateFn() + console.log(`Migration complete: ${migrationName}`) + + // mark as complete + doc[migrationName] = Date.now() + await db.put(doc) + } catch (err) { + console.error(`Error performing migration: ${migrationName}: `, err) + throw err + } +} diff --git a/packages/auth/src/utils.js b/packages/auth/src/utils.js index 823fd06322..e1df289d6e 100644 --- a/packages/auth/src/utils.js +++ b/packages/auth/src/utils.js @@ -20,6 +20,9 @@ const { hash } = require("./hashing") const userCache = require("./cache/user") const env = require("./environment") const { getUserSessions, invalidateSessions } = require("./security/sessions") +const { migrateIfRequired } = require("./migrations") +const { USER_EMAIL_VIEW_CASING } = require("./migrations").MIGRATIONS +const { GLOBAL_DB } = require("./migrations").MIGRATION_DBS const APP_PREFIX = DocumentTypes.APP + SEPARATOR @@ -128,10 +131,16 @@ exports.getGlobalUserByEmail = async email => { throw "Must supply an email address to view" } const db = getGlobalDB() + + await migrateIfRequired(GLOBAL_DB, USER_EMAIL_VIEW_CASING, async () => { + // re-create the view with latest changes + await createUserEmailView(db) + }) + try { let users = ( await db.query(`database/${ViewNames.USER_BY_EMAIL}`, { - key: email, + key: email.toLowerCase(), include_docs: true, }) ).rows From 957ce7ef04e9a46b2c0708fb04418a44292b43c3 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Tue, 26 Oct 2021 15:47:36 +0100 Subject: [PATCH 4/6] Add migration unit tests --- .../passport/tests/third-party-common.spec.js | 2 +- packages/auth/src/migrations/index.js | 32 +++++----- .../tests/__snapshots__/index.spec.js.snap | 9 +++ .../auth/src/migrations/tests/index.spec.js | 60 +++++++++++++++++++ .../passport => }/tests/utilities/db.js | 2 +- .../utilities/dbConfig.js} | 2 +- 6 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 packages/auth/src/migrations/tests/__snapshots__/index.spec.js.snap create mode 100644 packages/auth/src/migrations/tests/index.spec.js rename packages/auth/src/{middleware/passport => }/tests/utilities/db.js (87%) rename packages/auth/src/{middleware/passport/tests/utilities/test-config.js => tests/utilities/dbConfig.js} (53%) diff --git a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js index 81b8c3e6b0..3a3c55bfa0 100644 --- a/packages/auth/src/middleware/passport/tests/third-party-common.spec.js +++ b/packages/auth/src/middleware/passport/tests/third-party-common.spec.js @@ -1,6 +1,6 @@ // Mock data -require("./utilities/test-config") +require("../../../tests/utilities/dbConfig") const database = require("../../../db") const { authenticateThirdParty } = require("../third-party-common") diff --git a/packages/auth/src/migrations/index.js b/packages/auth/src/migrations/index.js index ca06188a8a..7492e94511 100644 --- a/packages/auth/src/migrations/index.js +++ b/packages/auth/src/migrations/index.js @@ -15,7 +15,7 @@ const DB_LOOKUP = { ], } -const getMigrationsDoc = async db => { +exports.getMigrationsDoc = async db => { // get the migrations doc try { return await db.get(DocumentTypes.MIGRATIONS) @@ -27,25 +27,21 @@ const getMigrationsDoc = async db => { } exports.migrateIfRequired = async (migrationDb, migrationName, migrateFn) => { - let db - if (migrationDb === exports.MIGRATION_DBS.GLOBAL_DB) { - db = getGlobalDB() - } else { - throw new Error(`Unrecognised migration db [${migrationDb}]`) - } - - if (!DB_LOOKUP[migrationDb].includes(migrationName)) { - throw new Error( - `Unrecognised migration name [${migrationName}] for db [${migrationDb}]` - ) - } - return tryMigrate(db, migrationName, migrateFn) -} - -const tryMigrate = async (db, migrationName, migrateFn) => { try { - const doc = await getMigrationsDoc(db) + let db + if (migrationDb === exports.MIGRATION_DBS.GLOBAL_DB) { + db = getGlobalDB() + } else { + throw new Error(`Unrecognised migration db [${migrationDb}]`) + } + if (!DB_LOOKUP[migrationDb].includes(migrationName)) { + throw new Error( + `Unrecognised migration name [${migrationName}] for db [${migrationDb}]` + ) + } + + const doc = await exports.getMigrationsDoc(db) // exit if the migration has been performed if (doc[migrationName]) { return diff --git a/packages/auth/src/migrations/tests/__snapshots__/index.spec.js.snap b/packages/auth/src/migrations/tests/__snapshots__/index.spec.js.snap new file mode 100644 index 0000000000..e9a18eadde --- /dev/null +++ b/packages/auth/src/migrations/tests/__snapshots__/index.spec.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`migrations should match snapshot 1`] = ` +Object { + "_id": "migrations", + "_rev": "1-af6c272fe081efafecd2ea49a8fcbb40", + "user_email_view_casing": 1487076708000, +} +`; diff --git a/packages/auth/src/migrations/tests/index.spec.js b/packages/auth/src/migrations/tests/index.spec.js new file mode 100644 index 0000000000..0ed16fc184 --- /dev/null +++ b/packages/auth/src/migrations/tests/index.spec.js @@ -0,0 +1,60 @@ +require("../../tests/utilities/dbConfig") + +const { migrateIfRequired, MIGRATION_DBS, MIGRATIONS, getMigrationsDoc } = require("../index") +const database = require("../../db") +const { + StaticDatabases, +} = require("../../db/utils") + +Date.now = jest.fn(() => 1487076708000) +let db + +describe("migrations", () => { + + const migrationFunction = jest.fn() + + beforeEach(() => { + db = database.getDB(StaticDatabases.GLOBAL.name) + }) + + afterEach(async () => { + jest.clearAllMocks() + await db.destroy() + }) + + const validMigration = () => { + return migrateIfRequired(MIGRATION_DBS.GLOBAL_DB, MIGRATIONS.USER_EMAIL_VIEW_CASING, migrationFunction) + } + + it("should run a new migration", async () => { + await validMigration() + expect(migrationFunction).toHaveBeenCalled() + }) + + it("should match snapshot", async () => { + await validMigration() + const doc = await getMigrationsDoc(db) + expect(doc).toMatchSnapshot() + }) + + it("should skip a previously run migration", async () => { + await validMigration() + await validMigration() + expect(migrationFunction).toHaveBeenCalledTimes(1) + }) + + it("should reject an unknown migration name", async () => { + expect(async () => { + await migrateIfRequired(MIGRATION_DBS.GLOBAL_DB, "bogus_name", migrationFunction) + }).rejects.toThrow() + expect(migrationFunction).not.toHaveBeenCalled() + }) + + it("should reject an unknown database name", async () => { + expect(async () => { + await migrateIfRequired("bogus_db", MIGRATIONS.USER_EMAIL_VIEW_CASING, migrationFunction) + }).rejects.toThrow() + expect(migrationFunction).not.toHaveBeenCalled() + }) + +}) \ No newline at end of file diff --git a/packages/auth/src/middleware/passport/tests/utilities/db.js b/packages/auth/src/tests/utilities/db.js similarity index 87% rename from packages/auth/src/middleware/passport/tests/utilities/db.js rename to packages/auth/src/tests/utilities/db.js index e83784471b..bb99592d1c 100644 --- a/packages/auth/src/middleware/passport/tests/utilities/db.js +++ b/packages/auth/src/tests/utilities/db.js @@ -1,5 +1,5 @@ const PouchDB = require("pouchdb") -const env = require("../../../../environment") +const env = require("../../environment") let POUCH_DB_DEFAULTS diff --git a/packages/auth/src/middleware/passport/tests/utilities/test-config.js b/packages/auth/src/tests/utilities/dbConfig.js similarity index 53% rename from packages/auth/src/middleware/passport/tests/utilities/test-config.js rename to packages/auth/src/tests/utilities/dbConfig.js index 57768d4071..45b9ff33f9 100644 --- a/packages/auth/src/middleware/passport/tests/utilities/test-config.js +++ b/packages/auth/src/tests/utilities/dbConfig.js @@ -1,3 +1,3 @@ -const packageConfiguration = require("../../../../index") +const packageConfiguration = require("../../index") const CouchDB = require("./db") packageConfiguration.init(CouchDB) From fd9796f715b1351d5799fd74cb6c469bc14dc215 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Tue, 26 Oct 2021 16:21:26 +0100 Subject: [PATCH 5/6] Update app id check in tests --- packages/server/src/tests/utilities/TestConfiguration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/tests/utilities/TestConfiguration.js b/packages/server/src/tests/utilities/TestConfiguration.js index 5f1d57467c..dfdc770389 100644 --- a/packages/server/src/tests/utilities/TestConfiguration.js +++ b/packages/server/src/tests/utilities/TestConfiguration.js @@ -144,7 +144,7 @@ class TestConfiguration { const headers = { Accept: "application/json", } - if (this.prodAppId) { + if (appId) { headers[Headers.APP_ID] = appId } return headers From 4aaffe23c00d8166d63067d6e46f9204701542b3 Mon Sep 17 00:00:00 2001 From: Budibase Staging Release Bot <> Date: Wed, 27 Oct 2021 09:02:08 +0000 Subject: [PATCH 6/6] v0.9.173-alpha.2 --- lerna.json | 2 +- packages/auth/package.json | 2 +- packages/bbui/package.json | 2 +- packages/builder/package.json | 8 ++++---- packages/cli/package.json | 2 +- packages/client/package.json | 6 +++--- packages/server/package.json | 8 ++++---- packages/string-templates/package.json | 2 +- packages/worker/package.json | 6 +++--- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lerna.json b/lerna.json index 6ffbee8ab8..9502e07e4f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "0.9.173-alpha.1", + "version": "0.9.173-alpha.2", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/auth/package.json b/packages/auth/package.json index 09c376a09c..f86fac70e5 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/auth", - "version": "0.9.173-alpha.1", + "version": "0.9.173-alpha.2", "description": "Authentication middlewares for budibase builder and apps", "main": "src/index.js", "author": "Budibase", diff --git a/packages/bbui/package.json b/packages/bbui/package.json index e244fefa4b..a43eca1557 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": "0.9.173-alpha.1", + "version": "0.9.173-alpha.2", "license": "AGPL-3.0", "svelte": "src/index.js", "module": "dist/bbui.es.js", diff --git a/packages/builder/package.json b/packages/builder/package.json index 3db119aac1..8179cc88ac 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/builder", - "version": "0.9.173-alpha.1", + "version": "0.9.173-alpha.2", "license": "AGPL-3.0", "private": true, "scripts": { @@ -65,10 +65,10 @@ } }, "dependencies": { - "@budibase/bbui": "^0.9.173-alpha.1", - "@budibase/client": "^0.9.173-alpha.1", + "@budibase/bbui": "^0.9.173-alpha.2", + "@budibase/client": "^0.9.173-alpha.2", "@budibase/colorpicker": "1.1.2", - "@budibase/string-templates": "^0.9.173-alpha.1", + "@budibase/string-templates": "^0.9.173-alpha.2", "@sentry/browser": "5.19.1", "@spectrum-css/page": "^3.0.1", "@spectrum-css/vars": "^3.0.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index 45b27e3d85..3f0d3d0d30 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/cli", - "version": "0.9.173-alpha.1", + "version": "0.9.173-alpha.2", "description": "Budibase CLI, for developers, self hosting and migrations.", "main": "src/index.js", "bin": { diff --git a/packages/client/package.json b/packages/client/package.json index 1dd17b5ba0..3b5725ce7f 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/client", - "version": "0.9.173-alpha.1", + "version": "0.9.173-alpha.2", "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": "^0.9.173-alpha.1", + "@budibase/bbui": "^0.9.173-alpha.2", "@budibase/standard-components": "^0.9.139", - "@budibase/string-templates": "^0.9.173-alpha.1", + "@budibase/string-templates": "^0.9.173-alpha.2", "regexparam": "^1.3.0", "shortid": "^2.2.15", "svelte-spa-router": "^3.0.5" diff --git a/packages/server/package.json b/packages/server/package.json index 22ed030a25..af2d54ae51 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/server", "email": "hi@budibase.com", - "version": "0.9.173-alpha.1", + "version": "0.9.173-alpha.2", "description": "Budibase Web Server", "main": "src/index.js", "repository": { @@ -68,9 +68,9 @@ "author": "Budibase", "license": "AGPL-3.0-or-later", "dependencies": { - "@budibase/auth": "^0.9.173-alpha.1", - "@budibase/client": "^0.9.173-alpha.1", - "@budibase/string-templates": "^0.9.173-alpha.1", + "@budibase/auth": "^0.9.173-alpha.2", + "@budibase/client": "^0.9.173-alpha.2", + "@budibase/string-templates": "^0.9.173-alpha.2", "@elastic/elasticsearch": "7.10.0", "@koa/router": "8.0.0", "@sendgrid/mail": "7.1.1", diff --git a/packages/string-templates/package.json b/packages/string-templates/package.json index 3d86987db7..53aa019450 100644 --- a/packages/string-templates/package.json +++ b/packages/string-templates/package.json @@ -1,6 +1,6 @@ { "name": "@budibase/string-templates", - "version": "0.9.173-alpha.1", + "version": "0.9.173-alpha.2", "description": "Handlebars wrapper for Budibase templating.", "main": "src/index.cjs", "module": "dist/bundle.mjs", diff --git a/packages/worker/package.json b/packages/worker/package.json index c21ed1f96d..a489956a5c 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -1,7 +1,7 @@ { "name": "@budibase/worker", "email": "hi@budibase.com", - "version": "0.9.173-alpha.1", + "version": "0.9.173-alpha.2", "description": "Budibase background service", "main": "src/index.js", "repository": { @@ -29,8 +29,8 @@ "author": "Budibase", "license": "AGPL-3.0-or-later", "dependencies": { - "@budibase/auth": "^0.9.173-alpha.1", - "@budibase/string-templates": "^0.9.173-alpha.1", + "@budibase/auth": "^0.9.173-alpha.2", + "@budibase/string-templates": "^0.9.173-alpha.2", "@koa/router": "^8.0.0", "@sentry/node": "^6.0.0", "@techpass/passport-openidconnect": "^0.3.0",