diff --git a/lerna.json b/lerna.json index 671935c34b..bbe4da4264 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.13.50", + "version": "2.13.51", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/account-portal b/packages/account-portal index e46a352a63..d6a1f89aa5 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit e46a352a6326a838faa00f912de069aee95d7300 +Subproject commit d6a1f89aa543bdce7acde5fbe4ce650a1344e2fe diff --git a/packages/backend-core/src/cache/passwordReset.ts b/packages/backend-core/src/cache/passwordReset.ts index 7f5a93f149..db32b520f7 100644 --- a/packages/backend-core/src/cache/passwordReset.ts +++ b/packages/backend-core/src/cache/passwordReset.ts @@ -1,6 +1,6 @@ import * as redis from "../redis/init" import * as utils from "../utils" -import { Duration, DurationType } from "../utils" +import { Duration } from "../utils" const TTL_SECONDS = Duration.fromHours(1).toSeconds() @@ -32,7 +32,18 @@ export async function getCode(code: string): Promise { const client = await redis.getPasswordResetClient() const value = (await client.get(code)) as PasswordReset | undefined if (!value) { - throw "Provided information is not valid, cannot reset password - please try again." + throw new Error( + "Provided information is not valid, cannot reset password - please try again." + ) } return value } + +/** + * Given a reset code this will invalidate it. + * @param code The code provided via the email link. + */ +export async function invalidateCode(code: string): Promise { + const client = await redis.getPasswordResetClient() + await client.delete(code) +} diff --git a/packages/backend-core/src/queue/queue.ts b/packages/backend-core/src/queue/queue.ts index 0657437a3b..b95dace5b2 100644 --- a/packages/backend-core/src/queue/queue.ts +++ b/packages/backend-core/src/queue/queue.ts @@ -47,7 +47,7 @@ export function createQueue( cleanupInterval = timers.set(cleanup, CLEANUP_PERIOD_MS) // fire off an initial cleanup cleanup().catch(err => { - console.error(`Unable to cleanup automation queue initially - ${err}`) + console.error(`Unable to cleanup ${jobQueue} initially - ${err}`) }) } return queue diff --git a/packages/backend-core/src/redis/redis.ts b/packages/backend-core/src/redis/redis.ts index 701e262091..d15453ba62 100644 --- a/packages/backend-core/src/redis/redis.ts +++ b/packages/backend-core/src/redis/redis.ts @@ -18,6 +18,7 @@ import { SelectableDatabase, getRedisConnectionDetails, } from "./utils" +import { logAlert } from "../logging" import * as timers from "../timers" const RETRY_PERIOD_MS = 2000 @@ -39,21 +40,16 @@ function pickClient(selectDb: number): any { return CLIENTS[selectDb] } -function connectionError( - selectDb: number, - timeout: NodeJS.Timeout, - err: Error | string -) { +function connectionError(timeout: NodeJS.Timeout, err: Error | string) { // manually shut down, ignore errors if (CLOSED) { return } - pickClient(selectDb).disconnect() CLOSED = true // always clear this on error clearTimeout(timeout) CONNECTED = false - console.error("Redis connection failed - " + err) + logAlert("Redis connection failed", err) setTimeout(() => { init() }, RETRY_PERIOD_MS) @@ -79,11 +75,7 @@ function init(selectDb = DEFAULT_SELECT_DB) { // start the timer - only allowed 5 seconds to connect timeout = setTimeout(() => { if (!CONNECTED) { - connectionError( - selectDb, - timeout, - "Did not successfully connect in timeout" - ) + connectionError(timeout, "Did not successfully connect in timeout") } }, STARTUP_TIMEOUT_MS) @@ -106,12 +98,13 @@ function init(selectDb = DEFAULT_SELECT_DB) { // allow the process to exit return } - connectionError(selectDb, timeout, err) + connectionError(timeout, err) }) client.on("error", (err: Error) => { - connectionError(selectDb, timeout, err) + connectionError(timeout, err) }) client.on("connect", () => { + console.log(`Connected to Redis DB: ${selectDb}`) clearTimeout(timeout) CONNECTED = true }) diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 326bed3cc5..01fa4899d1 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -2,7 +2,7 @@ import env from "../environment" import * as eventHelpers from "./events" import * as accountSdk from "../accounts" import * as cache from "../cache" -import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context" +import { getGlobalDB, getIdentity, getTenantId } from "../context" import * as dbUtils from "../db" import { EmailUnavailableError, HTTPError } from "../errors" import * as platform from "../platform" diff --git a/packages/worker/src/api/routes/global/tests/scim.spec.ts b/packages/worker/src/api/routes/global/tests/scim.spec.ts index 884625805c..56b7ca9f40 100644 --- a/packages/worker/src/api/routes/global/tests/scim.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim.spec.ts @@ -1,6 +1,6 @@ import tk from "timekeeper" import _ from "lodash" -import { mocks, structures } from "@budibase/backend-core/tests" +import { generator, mocks, structures } from "@budibase/backend-core/tests" import { ScimCreateUserRequest, ScimGroupResponse, @@ -14,9 +14,14 @@ import { events } from "@budibase/backend-core" jest.retryTimes(2, { logErrorsBeforeRetry: true }) jest.setTimeout(30000) -mocks.licenses.useScimIntegration() - describe("scim", () => { + beforeAll(async () => { + tk.freeze(mocks.date.MOCK_DATE) + mocks.licenses.useScimIntegration() + + await config.setSCIMConfig(true) + }) + beforeEach(async () => { jest.resetAllMocks() tk.freeze(mocks.date.MOCK_DATE) @@ -570,8 +575,15 @@ describe("scim", () => { beforeAll(async () => { groups = [] - for (let i = 0; i < groupCount; i++) { - const body = structures.scim.createGroupRequest() + const groupNames = generator.unique( + () => generator.word(), + groupCount + ) + + for (const groupName of groupNames) { + const body = structures.scim.createGroupRequest({ + displayName: groupName, + }) groups.push(await config.api.scimGroupsAPI.post({ body })) } diff --git a/packages/worker/src/sdk/auth/auth.ts b/packages/worker/src/sdk/auth/auth.ts index 1f9da8a260..3f24de440a 100644 --- a/packages/worker/src/sdk/auth/auth.ts +++ b/packages/worker/src/sdk/auth/auth.ts @@ -79,6 +79,9 @@ export const resetUpdate = async (resetCode: string, password: string) => { user.password = password user = await userSdk.db.save(user) + await cache.passwordReset.invalidateCode(resetCode) + await sessions.invalidateSessions(userId) + // remove password from the user before sending events delete user.password await events.user.passwordReset(user) diff --git a/packages/worker/src/sdk/auth/tests/auth.spec.ts b/packages/worker/src/sdk/auth/tests/auth.spec.ts new file mode 100644 index 0000000000..e9f348f7c7 --- /dev/null +++ b/packages/worker/src/sdk/auth/tests/auth.spec.ts @@ -0,0 +1,70 @@ +import { cache, context, sessions, utils } from "@budibase/backend-core" +import { loginUser, resetUpdate } from "../auth" +import { generator, structures } from "@budibase/backend-core/tests" +import { TestConfiguration } from "../../../tests" + +describe("auth", () => { + const config = new TestConfiguration() + + describe("resetUpdate", () => { + it("providing a valid code will update the password", async () => { + await context.doInTenant(structures.tenant.id(), async () => { + const user = await config.createUser() + const previousPassword = user.password + + const code = await cache.passwordReset.createCode(user._id!, {}) + const newPassword = generator.hash() + + await resetUpdate(code, newPassword) + + const persistedUser = await config.getUser(user.email) + expect(persistedUser.password).not.toBe(previousPassword) + expect( + await utils.compare(newPassword, persistedUser.password!) + ).toBeTruthy() + }) + }) + + it("wrong code will not allow to reset the password", async () => { + await context.doInTenant(structures.tenant.id(), async () => { + const code = generator.hash() + const newPassword = generator.hash() + + await expect(resetUpdate(code, newPassword)).rejects.toThrow( + "Provided information is not valid, cannot reset password - please try again." + ) + }) + }) + + it("the same code cannot be used twice", async () => { + await context.doInTenant(structures.tenant.id(), async () => { + const user = await config.createUser() + + const code = await cache.passwordReset.createCode(user._id!, {}) + const newPassword = generator.hash() + + await resetUpdate(code, newPassword) + await expect(resetUpdate(code, newPassword)).rejects.toThrow( + "Provided information is not valid, cannot reset password - please try again." + ) + }) + }) + + it("updating the password will invalidate all the sessions", async () => { + await context.doInTenant(structures.tenant.id(), async () => { + const user = await config.createUser() + + await loginUser(user) + + expect(await sessions.getSessionsForUser(user._id!)).toHaveLength(1) + + const code = await cache.passwordReset.createCode(user._id!, {}) + const newPassword = generator.hash() + + await resetUpdate(code, newPassword) + + expect(await sessions.getSessionsForUser(user._id!)).toHaveLength(0) + }) + }) + }) +}) diff --git a/packages/worker/src/sdk/users/tests/users.spec.ts b/packages/worker/src/sdk/users/tests/users.spec.ts index df1aa74200..a02ca42c2f 100644 --- a/packages/worker/src/sdk/users/tests/users.spec.ts +++ b/packages/worker/src/sdk/users/tests/users.spec.ts @@ -1,6 +1,5 @@ import { structures, mocks } from "../../../tests" import { env, context } from "@budibase/backend-core" -import * as users from "../users" import { db as userDb } from "../" import { CloudAccount } from "@budibase/types"