diff --git a/lerna.json b/lerna.json index fb239ee35d..5e28c36166 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "2.29.24", + "version": "2.29.25", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/package.json b/package.json index 29b87898ac..f3cbd75836 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@types/proper-lockfile": "^4.1.4", "@typescript-eslint/parser": "6.9.0", "esbuild": "^0.18.17", - "esbuild-node-externals": "^1.8.0", + "esbuild-node-externals": "^1.14.0", "eslint": "^8.52.0", "eslint-plugin-import": "^2.29.0", "eslint-plugin-jest": "^27.9.0", diff --git a/packages/backend-core/src/db/couch/DatabaseImpl.ts b/packages/backend-core/src/db/couch/DatabaseImpl.ts index f3c3beeaab..feeba6061e 100644 --- a/packages/backend-core/src/db/couch/DatabaseImpl.ts +++ b/packages/backend-core/src/db/couch/DatabaseImpl.ts @@ -56,24 +56,24 @@ class CouchDBError extends Error implements DBError { constructor( message: string, info: { - status: number | undefined - statusCode: number | undefined + status?: number + statusCode?: number name: string - errid: string - description: string - reason: string - error: string + errid?: string + description?: string + reason?: string + error?: string } ) { super(message) const statusCode = info.status || info.statusCode || 500 this.status = statusCode this.statusCode = statusCode - this.reason = info.reason + this.reason = info.reason || "Unknown" this.name = info.name - this.errid = info.errid - this.description = info.description - this.error = info.error + this.errid = info.errid || "Unknown" + this.description = info.description || "Unknown" + this.error = info.error || "Not found" } } @@ -246,6 +246,35 @@ export class DatabaseImpl implements Database { }) } + async bulkRemove(documents: Document[], opts?: { silenceErrors?: boolean }) { + const response: Nano.DocumentBulkResponse[] = await this.performCall(db => { + return () => + db.bulk({ + docs: documents.map(doc => ({ + ...doc, + _deleted: true, + })), + }) + }) + if (opts?.silenceErrors) { + return + } + let errorFound = false + let errorMessage: string = "Unable to bulk remove documents: " + for (let res of response) { + if (res.error) { + errorFound = true + errorMessage += res.error + } + } + if (errorFound) { + throw new CouchDBError(errorMessage, { + name: this.name, + status: 400, + }) + } + } + async post(document: AnyDocument, opts?: DatabasePutOpts) { if (!document._id) { document._id = newid() diff --git a/packages/backend-core/src/db/instrumentation.ts b/packages/backend-core/src/db/instrumentation.ts index 4e2b147ef3..7026224564 100644 --- a/packages/backend-core/src/db/instrumentation.ts +++ b/packages/backend-core/src/db/instrumentation.ts @@ -71,6 +71,16 @@ export class DDInstrumentedDatabase implements Database { }) } + bulkRemove( + documents: Document[], + opts?: { silenceErrors?: boolean } + ): Promise { + return tracer.trace("db.bulkRemove", span => { + span?.addTags({ db_name: this.name, num_docs: documents.length }) + return this.db.bulkRemove(documents, opts) + }) + } + put( document: AnyDocument, opts?: DatabasePutOpts | undefined diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index 5d9c5b74d3..6ee06d12ef 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -199,9 +199,8 @@ export const createPlatformUserView = async () => { export const queryPlatformView = async ( viewName: ViewName, - params: DatabaseQueryOpts, - opts?: QueryViewOptions -): Promise => { + params: DatabaseQueryOpts +): Promise => { const CreateFuncByName: any = { [ViewName.ACCOUNT_BY_EMAIL]: createPlatformAccountEmailView, [ViewName.PLATFORM_USERS_LOWERCASE]: createPlatformUserView, @@ -209,7 +208,9 @@ export const queryPlatformView = async ( return doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: Database) => { const createFn = CreateFuncByName[viewName] - return queryView(viewName, params, db, createFn, opts) + return queryView(viewName, params, db, createFn, { + arrayResponse: true, + }) as Promise }) } diff --git a/packages/backend-core/src/platform/users.ts b/packages/backend-core/src/platform/users.ts index ccaad76b19..9378e23724 100644 --- a/packages/backend-core/src/platform/users.ts +++ b/packages/backend-core/src/platform/users.ts @@ -25,6 +25,11 @@ export async function getUserDoc(emailOrId: string): Promise { return db.get(emailOrId) } +export async function updateUserDoc(platformUser: PlatformUserById) { + const db = getPlatformDB() + await db.put(platformUser) +} + // CREATE function newUserIdDoc(id: string, tenantId: string): PlatformUserById { @@ -113,15 +118,12 @@ export async function addUser( export async function removeUser(user: User) { const db = getPlatformDB() const keys = [user._id!, user.email] - const userDocs = await db.allDocs({ + const userDocs = await db.allDocs({ keys, include_docs: true, }) - const toDelete = userDocs.rows.map((row: any) => { - return { - ...row.doc, - _deleted: true, - } - }) - await db.bulkDocs(toDelete) + await db.bulkRemove( + userDocs.rows.map(row => row.doc!), + { silenceErrors: true } + ) } diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index 4865ebb5bc..c96c615f4b 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -18,6 +18,9 @@ import { User, UserStatus, UserGroup, + PlatformUserBySsoId, + PlatformUserById, + AnyDocument, } from "@budibase/types" import { getAccountHolderFromUserIds, @@ -25,7 +28,11 @@ import { isCreator, validateUniqueUser, } from "./utils" -import { searchExistingEmails } from "./lookup" +import { + getFirstPlatformUser, + getPlatformUsers, + searchExistingEmails, +} from "./lookup" import { hash } from "../utils" import { validatePassword } from "../security" @@ -446,9 +453,32 @@ export class UserDB { creator => !!creator ).length + const ssoUsersToDelete: AnyDocument[] = [] for (let user of usersToDelete) { + const platformUser = (await getFirstPlatformUser( + user._id! + )) as PlatformUserById + const ssoId = platformUser.ssoId + if (ssoId) { + // Need to get the _rev of the SSO user doc to delete it. The view also returns docs that have the ssoId property, so we need to ignore those. + const ssoUsers = (await getPlatformUsers( + ssoId + )) as PlatformUserBySsoId[] + ssoUsers + .filter(user => user.ssoId == null) + .forEach(user => { + ssoUsersToDelete.push({ + ...user, + _deleted: true, + }) + }) + } await bulkDeleteProcessing(user) } + + // Delete any associated SSO user docs + await platform.getPlatformDB().bulkDocs(ssoUsersToDelete) + await UserDB.quotas.removeUsers(toDelete.length, creatorsToDeleteCount) // Build Response diff --git a/packages/backend-core/src/users/lookup.ts b/packages/backend-core/src/users/lookup.ts index 355be74dab..5324ba950f 100644 --- a/packages/backend-core/src/users/lookup.ts +++ b/packages/backend-core/src/users/lookup.ts @@ -34,15 +34,22 @@ export async function searchExistingEmails(emails: string[]) { } // lookup, could be email or userId, either will return a doc -export async function getPlatformUser( +export async function getPlatformUsers( identifier: string -): Promise { +): Promise { // use the view here and allow to find anyone regardless of casing // Use lowercase to ensure email login is case insensitive - return (await dbUtils.queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, { + return await dbUtils.queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, { keys: [identifier.toLowerCase()], include_docs: true, - })) as PlatformUser + }) +} + +export async function getFirstPlatformUser( + identifier: string +): Promise { + const platformUserDocs = await getPlatformUsers(identifier) + return platformUserDocs[0] ?? null } export async function getExistingTenantUsers( @@ -74,15 +81,10 @@ export async function getExistingPlatformUsers( keys: lcEmails, include_docs: true, } - - const opts = { - arrayResponse: true, - } - return (await dbUtils.queryPlatformView( + return await dbUtils.queryPlatformView( ViewName.PLATFORM_USERS_LOWERCASE, - params, - opts - )) as PlatformUserByEmail[] + params + ) } export async function getExistingAccounts( @@ -93,14 +95,5 @@ export async function getExistingAccounts( keys: lcEmails, include_docs: true, } - - const opts = { - arrayResponse: true, - } - - return (await dbUtils.queryPlatformView( - ViewName.ACCOUNT_BY_EMAIL, - params, - opts - )) as AccountMetadata[] + return await dbUtils.queryPlatformView(ViewName.ACCOUNT_BY_EMAIL, params) } diff --git a/packages/backend-core/src/users/utils.ts b/packages/backend-core/src/users/utils.ts index 348ad1532f..e1e3da181d 100644 --- a/packages/backend-core/src/users/utils.ts +++ b/packages/backend-core/src/users/utils.ts @@ -1,7 +1,7 @@ import { CloudAccount, ContextUser, User, UserGroup } from "@budibase/types" import * as accountSdk from "../accounts" import env from "../environment" -import { getPlatformUser } from "./lookup" +import { getFirstPlatformUser } from "./lookup" import { EmailUnavailableError } from "../errors" import { getTenantId } from "../context" import { sdk } from "@budibase/shared-core" @@ -51,7 +51,7 @@ async function isCreatorByGroupMembership(user?: User | ContextUser) { export async function validateUniqueUser(email: string, tenantId: string) { // check budibase users in other tenants if (env.MULTI_TENANCY) { - const tenantUser = await getPlatformUser(email) + const tenantUser = await getFirstPlatformUser(email) if (tenantUser != null && tenantUser.tenantId !== tenantId) { throw new EmailUnavailableError(email) } diff --git a/packages/backend-core/tests/core/utilities/jestUtils.ts b/packages/backend-core/tests/core/utilities/jestUtils.ts index a49c2a795e..683a4e025b 100644 --- a/packages/backend-core/tests/core/utilities/jestUtils.ts +++ b/packages/backend-core/tests/core/utilities/jestUtils.ts @@ -1,6 +1,6 @@ import { - CONSTANT_EXTERNAL_ROW_COLS, - CONSTANT_INTERNAL_ROW_COLS, + PROTECTED_EXTERNAL_COLUMNS, + PROTECTED_INTERNAL_COLUMNS, } from "@budibase/shared-core" export function expectFunctionWasCalledTimesWith( @@ -14,7 +14,7 @@ export function expectFunctionWasCalledTimesWith( } export const expectAnyInternalColsAttributes: { - [K in (typeof CONSTANT_INTERNAL_ROW_COLS)[number]]: any + [K in (typeof PROTECTED_INTERNAL_COLUMNS)[number]]: any } = { tableId: expect.anything(), type: expect.anything(), @@ -25,7 +25,7 @@ export const expectAnyInternalColsAttributes: { } export const expectAnyExternalColsAttributes: { - [K in (typeof CONSTANT_EXTERNAL_ROW_COLS)[number]]: any + [K in (typeof PROTECTED_EXTERNAL_COLUMNS)[number]]: any } = { tableId: expect.anything(), _id: expect.anything(), diff --git a/packages/bbui/src/InlineAlert/InlineAlert.svelte b/packages/bbui/src/InlineAlert/InlineAlert.svelte index bfc56818cb..3b98936f62 100644 --- a/packages/bbui/src/InlineAlert/InlineAlert.svelte +++ b/packages/bbui/src/InlineAlert/InlineAlert.svelte @@ -36,9 +36,11 @@
{header}
- {#each split as splitMsg} -
{splitMsg}
- {/each} + + {#each split as splitMsg} +
{splitMsg}
+ {/each} +
{#if onConfirm}