diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index cb1129d079..5e0a2bf31d 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -2,409 +2,36 @@ import env from "../environment" import * as eventHelpers from "./events" import * as accounts from "../accounts" import * as cache from "../cache" -import { UserStatus, ViewName } from "../constants" import { getIdentity, getTenantId, getGlobalDB } from "../context" import * as dbUtils from "../db" import { EmailUnavailableError, HTTPError } from "../errors" import * as platform from "../platform" import * as sessions from "../security/sessions" -import * as utils from "../utils" import * as usersCore from "./users" import { - Account, AllDocsResponse, BulkUserCreated, BulkUserDeleted, - PlatformUser, RowResponse, SaveUserOpts, User, + Account, } from "@budibase/types" -import * as pro from "@budibase/pro" import * as accountSdk from "../accounts" -import { - isPreventPasswordActions, - validateUniqueUser, - getAccountHolderFromUserIds, -} from "./utils" +import { validateUniqueUser, getAccountHolderFromUserIds } from "./utils" import { searchExistingEmails } from "./lookup" -export async function allUsers() { - const db = getGlobalDB() - const response = await db.allDocs( - dbUtils.getGlobalUserParams(null, { - include_docs: true, - }) - ) - return response.rows.map((row: any) => row.doc) -} - -export async function countUsersByApp(appId: string) { - let response: any = await usersCore.searchGlobalUsersByApp(appId, {}) - return { - userCount: response.length, - } -} - -export async function getUsersByAppAccess(appId?: string) { - const opts: any = { - include_docs: true, - limit: 50, - } - let response: User[] = await usersCore.searchGlobalUsersByAppAccess( - appId, - opts - ) - return response -} - -export async function getUserByEmail(email: string) { - return usersCore.getGlobalUserByEmail(email) -} - -/** - * Gets a user by ID from the global database, based on the current tenancy. - */ -export async function getUser(userId: string) { - const user = await usersCore.getById(userId) - if (user) { - delete user.password - } - return user -} - -export async function buildUser( +type QuotaUpdateFn = (change: number, cb?: () => Promise) => Promise +type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise +type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn } +type GroupFns = { addUsers: GroupUpdateFn } +type BuildUserFn = ( user: User, - opts: SaveUserOpts = { - hashPassword: true, - requirePassword: true, - }, + opts: SaveUserOpts, tenantId: string, - dbUser?: any, + dbUser?: User, account?: Account -): Promise { - let { password, _id } = user - - // don't require a password if the db user doesn't already have one - if (dbUser && !dbUser.password) { - opts.requirePassword = false - } - - let hashedPassword - if (password) { - if (await isPreventPasswordActions(user, account)) { - throw new HTTPError("Password change is disabled for this user", 400) - } - hashedPassword = opts.hashPassword ? await utils.hash(password) : password - } else if (dbUser) { - hashedPassword = dbUser.password - } - - // passwords are never required if sso is enforced - const requirePasswords = - opts.requirePassword && !(await pro.features.isSSOEnforced()) - if (!hashedPassword && requirePasswords) { - throw "Password must be specified." - } - - _id = _id || dbUtils.generateGlobalUserID() - - const fullUser = { - createdAt: Date.now(), - ...dbUser, - ...user, - _id, - password: hashedPassword, - tenantId, - } - // make sure the roles object is always present - if (!fullUser.roles) { - fullUser.roles = {} - } - // add the active status to a user if its not provided - if (fullUser.status == null) { - fullUser.status = UserStatus.ACTIVE - } - - return fullUser -} - -export const save = async ( - user: User, - opts: SaveUserOpts = {} -): Promise => { - // default booleans to true - if (opts.hashPassword == null) { - opts.hashPassword = true - } - if (opts.requirePassword == null) { - opts.requirePassword = true - } - const tenantId = getTenantId() - const db = getGlobalDB() - - let { email, _id, userGroups = [], roles } = user - - if (!email && !_id) { - throw new Error("_id or email is required") - } - - let dbUser: User | undefined - if (_id) { - // try to get existing user from db - try { - dbUser = (await db.get(_id)) as User - if (email && dbUser.email !== email) { - throw "Email address cannot be changed" - } - email = dbUser.email - } catch (e: any) { - if (e.status === 404) { - // do nothing, save this new user with the id specified - required for SSO auth - } else { - throw e - } - } - } - - if (!dbUser && email) { - // no id was specified - load from email instead - dbUser = await usersCore.getGlobalUserByEmail(email) - if (dbUser && dbUser._id !== _id) { - throw new EmailUnavailableError(email) - } - } - - const change = dbUser ? 0 : 1 // no change if there is existing user - return pro.quotas.addUsers(change, async () => { - await validateUniqueUser(email, tenantId) - - let builtUser = await buildUser(user, opts, tenantId, dbUser) - // don't allow a user to update its own roles/perms - if (opts.currentUserId && opts.currentUserId === dbUser?._id) { - builtUser = usersCore.cleanseUserObject(builtUser, dbUser) as User - } - - if (!dbUser && roles?.length) { - builtUser.roles = { ...roles } - } - - // make sure we set the _id field for a new user - // Also if this is a new user, associate groups with them - let groupPromises = [] - if (!_id) { - _id = builtUser._id! - - if (userGroups.length > 0) { - for (let groupId of userGroups) { - groupPromises.push(pro.groups.addUsers(groupId, [_id])) - } - } - } - - try { - // save the user to db - let response = await db.put(builtUser) - builtUser._rev = response.rev - - await eventHelpers.handleSaveEvents(builtUser, dbUser) - await platform.users.addUser(tenantId, builtUser._id!, builtUser.email) - await cache.user.invalidateUser(response.id) - - await Promise.all(groupPromises) - - // finally returned the saved user from the db - return db.get(builtUser._id!) - } catch (err: any) { - if (err.status === 409) { - throw "User exists already" - } else { - throw err - } - } - }) -} - -export const bulkCreate = async ( - newUsersRequested: User[], - groups: string[] -): Promise => { - const tenantId = getTenantId() - - let usersToSave: any[] = [] - let newUsers: any[] = [] - - const emails = newUsersRequested.map((user: User) => user.email) - const existingEmails = await searchExistingEmails(emails) - const unsuccessful: { email: string; reason: string }[] = [] - - for (const newUser of newUsersRequested) { - if ( - newUsers.find( - (x: User) => x.email.toLowerCase() === newUser.email.toLowerCase() - ) || - existingEmails.includes(newUser.email.toLowerCase()) - ) { - unsuccessful.push({ - email: newUser.email, - reason: `Unavailable`, - }) - continue - } - newUser.userGroups = groups - newUsers.push(newUser) - } - - const account = await accountSdk.getAccountByTenantId(tenantId) - return pro.quotas.addUsers(newUsers.length, async () => { - // create the promises array that will be called by bulkDocs - newUsers.forEach((user: any) => { - usersToSave.push( - buildUser( - user, - { - hashPassword: true, - requirePassword: user.requirePassword, - }, - tenantId, - undefined, // no dbUser - account - ) - ) - }) - - const usersToBulkSave = await Promise.all(usersToSave) - await usersCore.bulkUpdateGlobalUsers(usersToBulkSave) - - // Post-processing of bulk added users, e.g. events and cache operations - for (const user of usersToBulkSave) { - // TODO: Refactor to bulk insert users into the info db - // instead of relying on looping tenant creation - await platform.users.addUser(tenantId, user._id, user.email) - await eventHelpers.handleSaveEvents(user, undefined) - } - - const saved = usersToBulkSave.map(user => { - return { - _id: user._id, - email: user.email, - } - }) - - // now update the groups - if (Array.isArray(saved) && groups) { - const groupPromises = [] - const createdUserIds = saved.map(user => user._id) - for (let groupId of groups) { - groupPromises.push(pro.groups.addUsers(groupId, createdUserIds)) - } - await Promise.all(groupPromises) - } - - return { - successful: saved, - unsuccessful, - } - }) -} - -export const bulkDelete = async ( - userIds: string[] -): Promise => { - const db = getGlobalDB() - - const response: BulkUserDeleted = { - successful: [], - unsuccessful: [], - } - - // remove the account holder from the delete request if present - const account = await getAccountHolderFromUserIds(userIds) - if (account) { - userIds = userIds.filter(u => u !== account.budibaseUserId) - // mark user as unsuccessful - response.unsuccessful.push({ - _id: account.budibaseUserId, - email: account.email, - reason: "Account holder cannot be deleted", - }) - } - - // Get users and delete - const allDocsResponse: AllDocsResponse = await db.allDocs({ - include_docs: true, - keys: userIds, - }) - const usersToDelete: User[] = allDocsResponse.rows.map( - (user: RowResponse) => { - return user.doc - } - ) - - // Delete from DB - const toDelete = usersToDelete.map(user => ({ - ...user, - _deleted: true, - })) - const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete) - - await pro.quotas.removeUsers(toDelete.length) - for (let user of usersToDelete) { - await bulkDeleteProcessing(user) - } - - // Build Response - // index users by id - const userIndex: { [key: string]: User } = {} - usersToDelete.reduce((prev, current) => { - prev[current._id!] = current - return prev - }, userIndex) - - // add the successful and unsuccessful users to response - dbResponse.forEach(item => { - const email = userIndex[item.id].email - if (item.ok) { - response.successful.push({ _id: item.id, email }) - } else { - response.unsuccessful.push({ - _id: item.id, - email, - reason: "Database error", - }) - } - }) - - return response -} - -export const destroy = async (id: string) => { - const db = getGlobalDB() - const dbUser = (await db.get(id)) as User - const userId = dbUser._id as string - - if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { - // root account holder can't be deleted from inside budibase - const email = dbUser.email - const account = await accounts.getAccount(email) - if (account) { - if (dbUser.userId === getIdentity()!._id) { - throw new HTTPError('Please visit "Account" to delete this user', 400) - } else { - throw new HTTPError("Account holder cannot be deleted", 400) - } - } - } - - await platform.users.removeUser(dbUser) - - await db.remove(userId, dbUser._rev) - - await pro.quotas.removeUsers(1) - await eventHelpers.handleDeleteEvents(dbUser) - await cache.user.invalidateUser(userId) - await sessions.invalidateSessions(userId, { reason: "deletion" }) -} +) => Promise const bulkDeleteProcessing = async (dbUser: User) => { const userId = dbUser._id as string @@ -413,3 +40,338 @@ const bulkDeleteProcessing = async (dbUser: User) => { await cache.user.invalidateUser(userId) await sessions.invalidateSessions(userId, { reason: "bulk-deletion" }) } + +export class UserDB { + quotas: QuotaFns + groups: GroupFns + ssoEnforcedFn: () => Promise + buildUserFn: BuildUserFn + + constructor( + quotaFns: QuotaFns, + groupFns: GroupFns, + ssoEnforcedFn: () => Promise, + buildUserFn: BuildUserFn + ) { + this.quotas = quotaFns + this.groups = groupFns + this.ssoEnforcedFn = ssoEnforcedFn + this.buildUserFn = buildUserFn + } + + async allUsers() { + const db = getGlobalDB() + const response = await db.allDocs( + dbUtils.getGlobalUserParams(null, { + include_docs: true, + }) + ) + return response.rows.map((row: any) => row.doc) + } + + async countUsersByApp(appId: string) { + let response: any = await usersCore.searchGlobalUsersByApp(appId, {}) + return { + userCount: response.length, + } + } + + async getUsersByAppAccess(appId?: string) { + const opts: any = { + include_docs: true, + limit: 50, + } + let response: User[] = await usersCore.searchGlobalUsersByAppAccess( + appId, + opts + ) + return response + } + + async getUserByEmail(email: string) { + return usersCore.getGlobalUserByEmail(email) + } + + /** + * Gets a user by ID from the global database, based on the current tenancy. + */ + async getUser(userId: string) { + const user = await usersCore.getById(userId) + if (user) { + delete user.password + } + return user + } + + async save(user: User, opts: SaveUserOpts = {}): Promise { + // default booleans to true + if (opts.hashPassword == null) { + opts.hashPassword = true + } + if (opts.requirePassword == null) { + opts.requirePassword = true + } + const tenantId = getTenantId() + const db = getGlobalDB() + + let { email, _id, userGroups = [], roles } = user + + if (!email && !_id) { + throw new Error("_id or email is required") + } + + let dbUser: User | undefined + if (_id) { + // try to get existing user from db + try { + dbUser = (await db.get(_id)) as User + if (email && dbUser.email !== email) { + throw "Email address cannot be changed" + } + email = dbUser.email + } catch (e: any) { + if (e.status === 404) { + // do nothing, save this new user with the id specified - required for SSO auth + } else { + throw e + } + } + } + + if (!dbUser && email) { + // no id was specified - load from email instead + dbUser = await usersCore.getGlobalUserByEmail(email) + if (dbUser && dbUser._id !== _id) { + throw new EmailUnavailableError(email) + } + } + + const change = dbUser ? 0 : 1 // no change if there is existing user + return this.quotas.addUsers(change, async () => { + await validateUniqueUser(email, tenantId) + + let builtUser = await this.buildUserFn(user, opts, tenantId, dbUser) + // don't allow a user to update its own roles/perms + if (opts.currentUserId && opts.currentUserId === dbUser?._id) { + builtUser = usersCore.cleanseUserObject(builtUser, dbUser) as User + } + + if (!dbUser && roles?.length) { + builtUser.roles = { ...roles } + } + + // make sure we set the _id field for a new user + // Also if this is a new user, associate groups with them + let groupPromises = [] + if (!_id) { + _id = builtUser._id! + + if (userGroups.length > 0) { + for (let groupId of userGroups) { + groupPromises.push(this.groups.addUsers(groupId, [_id!])) + } + } + } + + try { + // save the user to db + let response = await db.put(builtUser) + builtUser._rev = response.rev + + await eventHelpers.handleSaveEvents(builtUser, dbUser) + await platform.users.addUser(tenantId, builtUser._id!, builtUser.email) + await cache.user.invalidateUser(response.id) + + await Promise.all(groupPromises) + + // finally returned the saved user from the db + return db.get(builtUser._id!) + } catch (err: any) { + if (err.status === 409) { + throw "User exists already" + } else { + throw err + } + } + }) + } + + async bulkCreate( + newUsersRequested: User[], + groups: string[] + ): Promise { + const tenantId = getTenantId() + + let usersToSave: any[] = [] + let newUsers: any[] = [] + + const emails = newUsersRequested.map((user: User) => user.email) + const existingEmails = await searchExistingEmails(emails) + const unsuccessful: { email: string; reason: string }[] = [] + + for (const newUser of newUsersRequested) { + if ( + newUsers.find( + (x: User) => x.email.toLowerCase() === newUser.email.toLowerCase() + ) || + existingEmails.includes(newUser.email.toLowerCase()) + ) { + unsuccessful.push({ + email: newUser.email, + reason: `Unavailable`, + }) + continue + } + newUser.userGroups = groups + newUsers.push(newUser) + } + + const account = await accountSdk.getAccountByTenantId(tenantId) + return this.quotas.addUsers(newUsers.length, async () => { + // create the promises array that will be called by bulkDocs + newUsers.forEach((user: any) => { + usersToSave.push( + this.buildUserFn( + user, + { + hashPassword: true, + requirePassword: user.requirePassword, + }, + tenantId, + undefined, // no dbUser + account + ) + ) + }) + + const usersToBulkSave = await Promise.all(usersToSave) + await usersCore.bulkUpdateGlobalUsers(usersToBulkSave) + + // Post-processing of bulk added users, e.g. events and cache operations + for (const user of usersToBulkSave) { + // TODO: Refactor to bulk insert users into the info db + // instead of relying on looping tenant creation + await platform.users.addUser(tenantId, user._id, user.email) + await eventHelpers.handleSaveEvents(user, undefined) + } + + const saved = usersToBulkSave.map(user => { + return { + _id: user._id, + email: user.email, + } + }) + + // now update the groups + if (Array.isArray(saved) && groups) { + const groupPromises = [] + const createdUserIds = saved.map(user => user._id) + for (let groupId of groups) { + groupPromises.push(this.groups.addUsers(groupId, createdUserIds)) + } + await Promise.all(groupPromises) + } + + return { + successful: saved, + unsuccessful, + } + }) + } + + async bulkDelete(userIds: string[]): Promise { + const db = getGlobalDB() + + const response: BulkUserDeleted = { + successful: [], + unsuccessful: [], + } + + // remove the account holder from the delete request if present + const account = await getAccountHolderFromUserIds(userIds) + if (account) { + userIds = userIds.filter(u => u !== account.budibaseUserId) + // mark user as unsuccessful + response.unsuccessful.push({ + _id: account.budibaseUserId, + email: account.email, + reason: "Account holder cannot be deleted", + }) + } + + // Get users and delete + const allDocsResponse: AllDocsResponse = await db.allDocs({ + include_docs: true, + keys: userIds, + }) + const usersToDelete: User[] = allDocsResponse.rows.map( + (user: RowResponse) => { + return user.doc + } + ) + + // Delete from DB + const toDelete = usersToDelete.map(user => ({ + ...user, + _deleted: true, + })) + const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete) + + await this.quotas.removeUsers(toDelete.length) + for (let user of usersToDelete) { + await bulkDeleteProcessing(user) + } + + // Build Response + // index users by id + const userIndex: { [key: string]: User } = {} + usersToDelete.reduce((prev, current) => { + prev[current._id!] = current + return prev + }, userIndex) + + // add the successful and unsuccessful users to response + dbResponse.forEach(item => { + const email = userIndex[item.id].email + if (item.ok) { + response.successful.push({ _id: item.id, email }) + } else { + response.unsuccessful.push({ + _id: item.id, + email, + reason: "Database error", + }) + } + }) + + return response + } + + async destroy(id: string) { + const db = getGlobalDB() + const dbUser = (await db.get(id)) as User + const userId = dbUser._id as string + + if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { + // root account holder can't be deleted from inside budibase + const email = dbUser.email + const account = await accounts.getAccount(email) + if (account) { + if (dbUser.userId === getIdentity()!._id) { + throw new HTTPError('Please visit "Account" to delete this user', 400) + } else { + throw new HTTPError("Account holder cannot be deleted", 400) + } + } + } + + await platform.users.removeUser(dbUser) + + await db.remove(userId, dbUser._rev) + + await this.quotas.removeUsers(1) + await eventHelpers.handleDeleteEvents(dbUser) + await cache.user.invalidateUser(userId) + await sessions.invalidateSessions(userId, { reason: "deletion" }) + } +} diff --git a/packages/backend-core/src/users/index.ts b/packages/backend-core/src/users/index.ts index 2e5e2f948c..c11d2a2c62 100644 --- a/packages/backend-core/src/users/index.ts +++ b/packages/backend-core/src/users/index.ts @@ -1,4 +1,4 @@ export * from "./users" export * from "./utils" export * from "./lookup" -export * as db from "./db" +export { UserDB } from "./db" diff --git a/packages/backend-core/src/users/utils.ts b/packages/backend-core/src/users/utils.ts index 0a9abd50bc..b6bf3f0abb 100644 --- a/packages/backend-core/src/users/utils.ts +++ b/packages/backend-core/src/users/utils.ts @@ -1,11 +1,4 @@ -import { - Account, - CloudAccount, - isSSOAccount, - isSSOUser, - User, -} from "@budibase/types" -import * as pro from "@budibase/pro" +import { CloudAccount } from "@budibase/types" import * as accountSdk from "../accounts" import env from "../environment" import { getPlatformUser } from "./lookup" @@ -40,30 +33,6 @@ export async function validateUniqueUser(email: string, tenantId: string) { } } -export async function isPreventPasswordActions(user: User, account?: Account) { - // when in maintenance mode we allow sso users with the admin role - // to perform any password action - this prevents lockout - if (env.ENABLE_SSO_MAINTENANCE_MODE && isAdmin(user)) { - return false - } - - // SSO is enforced for all users - if (await pro.features.isSSOEnforced()) { - return true - } - - // Check local sso - if (isSSOUser(user)) { - return true - } - - // Check account sso - if (!account) { - account = await accountSdk.getAccountByTenantId(getTenantId()) - } - return !!(account && account.email === user.email && isSSOAccount(account)) -} - /** * For the given user id's, return the account holder if it is in the ids. */ diff --git a/packages/worker/src/api/routes/global/tests/appBuilder.spec.ts b/packages/worker/src/api/routes/global/tests/appBuilder.spec.ts index 2974588cae..138ebbc595 100644 --- a/packages/worker/src/api/routes/global/tests/appBuilder.spec.ts +++ b/packages/worker/src/api/routes/global/tests/appBuilder.spec.ts @@ -40,13 +40,27 @@ describe("/api/global/users/:userId/app/builder", () => { describe("PATCH /api/global/users/:userId/app/:appId/builder", () => { it("shouldn't allow granting access to an app to a non-app builder", async () => { const user = await newUser() - await config.api.users.grantBuilderToApp(user._id!, MOCK_APP_ID) + await config.api.users.grantBuilderToApp(user._id!, MOCK_APP_ID, 400) }) it("should be able to grant a user access to a particular app", async () => { const user = await grantAppBuilder() + await config.api.users.grantBuilderToApp(user._id!, MOCK_APP_ID) + const updated = await getUser(user._id!) + expect(updated.builder?.appBuilder).toBe(true) + expect(updated.builder?.apps).toBe([MOCK_APP_ID]) }) }) - describe("DELETE /api/global/users/:userId/app/:appId/builder", () => {}) + describe("DELETE /api/global/users/:userId/app/:appId/builder", () => { + it("should allow revoking access", async () => { + const user = await grantAppBuilder() + await config.api.users.grantBuilderToApp(user._id!, MOCK_APP_ID) + let updated = await getUser(user._id!) + expect(updated.builder?.apps).toBe([MOCK_APP_ID]) + await config.api.users.revokeBuilderToApp(user._id!, MOCK_APP_ID) + updated = await getUser(user._id!) + expect(updated.builder?.apps).toBe([]) + }) + }) }) diff --git a/packages/worker/src/sdk/users/index.ts b/packages/worker/src/sdk/users/index.ts index b37ea07d94..d40ed1b045 100644 --- a/packages/worker/src/sdk/users/index.ts +++ b/packages/worker/src/sdk/users/index.ts @@ -1,4 +1,12 @@ export * from "./users" +import { buildUser } from "./users" import { users } from "@budibase/backend-core" -export const db = users.db +import * as pro from "@budibase/pro" +// pass in the components which are specific to the worker/the parts of pro which backend-core cannot access +export const db = new users.UserDB( + pro.quotas, + pro.groups, + pro.features.isSSOEnforced, + buildUser +) export { users as core } from "@budibase/backend-core" diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index f45c7dda20..45a69f5b32 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -1,11 +1,111 @@ -import { events, tenancy, users as usersCore } from "@budibase/backend-core" -import { InviteUsersRequest, InviteUsersResponse } from "@budibase/types" +import { + events, + HTTPError, + tenancy, + users as usersCore, + UserStatus, + db as dbUtils, + utils, + accounts as accountSdk, + context, + env as coreEnv, +} from "@budibase/backend-core" +import { + Account, + InviteUsersRequest, + InviteUsersResponse, + isSSOAccount, + isSSOUser, + SaveUserOpts, + User, +} from "@budibase/types" import { sendEmail } from "../../utilities/email" import { EmailTemplatePurpose } from "../../constants" +import * as pro from "@budibase/pro" -export const invite = async ( +export async function isPreventPasswordActions(user: User, account?: Account) { + // when in maintenance mode we allow sso users with the admin role + // to perform any password action - this prevents lockout + if (coreEnv.ENABLE_SSO_MAINTENANCE_MODE && usersCore.isAdmin(user)) { + return false + } + + // SSO is enforced for all users + if (await pro.features.isSSOEnforced()) { + return true + } + + // Check local sso + if (isSSOUser(user)) { + return true + } + + // Check account sso + if (!account) { + account = await accountSdk.getAccountByTenantId(context.getTenantId()) + } + return !!(account && account.email === user.email && isSSOAccount(account)) +} + +export async function buildUser( + user: User, + opts: SaveUserOpts = { + hashPassword: true, + requirePassword: true, + }, + tenantId: string, + dbUser?: any, + account?: Account +): Promise { + let { password, _id } = user + + // don't require a password if the db user doesn't already have one + if (dbUser && !dbUser.password) { + opts.requirePassword = false + } + + let hashedPassword + if (password) { + if (await isPreventPasswordActions(user, account)) { + throw new HTTPError("Password change is disabled for this user", 400) + } + hashedPassword = opts.hashPassword ? await utils.hash(password) : password + } else if (dbUser) { + hashedPassword = dbUser.password + } + + // passwords are never required if sso is enforced + const requirePasswords = + opts.requirePassword && !(await pro.features.isSSOEnforced()) + if (!hashedPassword && requirePasswords) { + throw "Password must be specified." + } + + _id = _id || dbUtils.generateGlobalUserID() + + const fullUser = { + createdAt: Date.now(), + ...dbUser, + ...user, + _id, + password: hashedPassword, + tenantId, + } + // make sure the roles object is always present + if (!fullUser.roles) { + fullUser.roles = {} + } + // add the active status to a user if its not provided + if (fullUser.status == null) { + fullUser.status = UserStatus.ACTIVE + } + + return fullUser +} + +export async function invite( users: InviteUsersRequest -): Promise => { +): Promise { const response: InviteUsersResponse = { successful: [], unsuccessful: [], diff --git a/packages/worker/src/tests/api/users.ts b/packages/worker/src/tests/api/users.ts index 39f7b64d59..605ac79416 100644 --- a/packages/worker/src/tests/api/users.ts +++ b/packages/worker/src/tests/api/users.ts @@ -149,15 +149,23 @@ export class UserAPI extends TestAPI { .expect(200) } - grantBuilderToApp = (userId: string, appId: string) => { + grantBuilderToApp = ( + userId: string, + appId: string, + statusCode: number = 200 + ) => { return this.request .patch(`/api/global/users/${userId}/app/${appId}/builder`) .set(this.config.defaultHeaders()) .expect("Content-Type", /json/) - .expect(200) + .expect(statusCode) } - revokeBuilderToApp = (userId: string, appId: string) => { + revokeBuilderToApp = ( + userId: string, + appId: string, + statusCode: number = 200 + ) => { return this.request .delete(`/api/global/users/${userId}/app/${appId}/builder`) .set(this.config.defaultHeaders())