From ddd84820136583cc48f6b59f6a441d303c429524 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 21 Nov 2023 17:30:11 +0000 Subject: [PATCH] Updating bb admin user creation so that it can be used incase in self host a user gets locked out, the environment variables can be used to create a simple user to access the system. --- packages/backend-core/src/users/db.ts | 35 ++++++++++- packages/backend-core/src/users/users.ts | 44 ++++++++----- packages/server/src/startup.ts | 61 ++++++++++--------- .../server/src/utilities/workerRequests.ts | 4 +- .../src/api/controllers/global/users.ts | 31 +++------- 5 files changed, 106 insertions(+), 69 deletions(-) diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index bd85097bbd..61b9c186b8 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -3,7 +3,7 @@ import * as eventHelpers from "./events" import * as accounts from "../accounts" import * as accountSdk from "../accounts" import * as cache from "../cache" -import { getGlobalDB, getIdentity, getTenantId } from "../context" +import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context" import * as dbUtils from "../db" import { EmailUnavailableError, HTTPError } from "../errors" import * as platform from "../platform" @@ -11,12 +11,10 @@ import * as sessions from "../security/sessions" import * as usersCore from "./users" import { Account, - AllDocsResponse, BulkUserCreated, BulkUserDeleted, isSSOAccount, isSSOUser, - RowResponse, SaveUserOpts, User, UserStatus, @@ -488,6 +486,37 @@ export class UserDB { await sessions.invalidateSessions(userId, { reason: "deletion" }) } + static async createAdminUser( + email: string, + password: string, + tenantId: string, + opts?: { ssoId?: string; hashPassword?: boolean; requirePassword?: boolean } + ) { + const user: User = { + email: email, + password: password, + createdAt: Date.now(), + roles: {}, + builder: { + global: true, + }, + admin: { + global: true, + }, + tenantId, + } + if (opts?.ssoId) { + user.ssoId = opts.ssoId + } + // always bust checklist beforehand, if an error occurs but can proceed, don't get + // stuck in a cycle + await cache.bustCache(cache.CacheKey.CHECKLIST) + return await UserDB.save(user, { + hashPassword: opts?.hashPassword, + requirePassword: opts?.requirePassword, + }) + } + static async getGroups(groupIds: string[]) { return await this.groups.getBulk(groupIds) } diff --git a/packages/backend-core/src/users/users.ts b/packages/backend-core/src/users/users.ts index 9f4a41f6df..6aed45371a 100644 --- a/packages/backend-core/src/users/users.ts +++ b/packages/backend-core/src/users/users.ts @@ -43,7 +43,7 @@ function removeUserPassword(users: User | User[]) { return users } -export const isSupportedUserSearch = (query: SearchQuery) => { +export function isSupportedUserSearch(query: SearchQuery) { const allowed = [ { op: SearchQueryOperators.STRING, key: "email" }, { op: SearchQueryOperators.EQUAL, key: "_id" }, @@ -68,10 +68,10 @@ export const isSupportedUserSearch = (query: SearchQuery) => { return true } -export const bulkGetGlobalUsersById = async ( +export async function bulkGetGlobalUsersById( userIds: string[], opts?: GetOpts -) => { +) { const db = getGlobalDB() let users = ( await db.allDocs({ @@ -85,7 +85,7 @@ export const bulkGetGlobalUsersById = async ( return users } -export const getAllUserIds = async () => { +export async function getAllUserIds() { const db = getGlobalDB() const startKey = `${DocumentType.USER}${SEPARATOR}` const response = await db.allDocs({ @@ -95,7 +95,7 @@ export const getAllUserIds = async () => { return response.rows.map(row => row.id) } -export const bulkUpdateGlobalUsers = async (users: User[]) => { +export async function bulkUpdateGlobalUsers(users: User[]) { const db = getGlobalDB() return (await db.bulkDocs(users)) as BulkDocsResponse } @@ -113,10 +113,10 @@ export async function getById(id: string, opts?: GetOpts): Promise { * Given an email address this will use a view to search through * all the users to find one with this email address. */ -export const getGlobalUserByEmail = async ( +export async function getGlobalUserByEmail( email: String, opts?: GetOpts -): Promise => { +): Promise { if (email == null) { throw "Must supply an email address to view" } @@ -139,11 +139,23 @@ export const getGlobalUserByEmail = async ( return user } -export const searchGlobalUsersByApp = async ( +export async function doesUserExist(email: string) { + try { + const user = await getGlobalUserByEmail(email) + if (Array.isArray(user) || user != null) { + return true + } + } catch (err) { + return false + } + return false +} + +export async function searchGlobalUsersByApp( appId: any, opts: DatabaseQueryOpts, getOpts?: GetOpts -) => { +) { if (typeof appId !== "string") { throw new Error("Must provide a string based app ID") } @@ -167,10 +179,10 @@ export const searchGlobalUsersByApp = async ( Return any user who potentially has access to the application Admins, developers and app users with the explicitly role. */ -export const searchGlobalUsersByAppAccess = async ( +export async function searchGlobalUsersByAppAccess( appId: any, opts?: { limit?: number } -) => { +) { const roleSelector = `roles.${appId}` let orQuery: any[] = [ @@ -205,7 +217,7 @@ export const searchGlobalUsersByAppAccess = async ( return resp.rows } -export const getGlobalUserByAppPage = (appId: string, user: User) => { +export function getGlobalUserByAppPage(appId: string, user: User) { if (!user) { return } @@ -215,11 +227,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => { /** * Performs a starts with search on the global email view. */ -export const searchGlobalUsersByEmail = async ( +export async function searchGlobalUsersByEmail( email: string | unknown, opts: any, getOpts?: GetOpts -) => { +) { if (typeof email !== "string") { throw new Error("Must provide a string to search by") } @@ -242,12 +254,12 @@ export const searchGlobalUsersByEmail = async ( } const PAGE_LIMIT = 8 -export const paginatedUsers = async ({ +export async function paginatedUsers({ bookmark, query, appId, limit, -}: SearchUsersRequest = {}) => { +}: SearchUsersRequest = {}) { const db = getGlobalDB() const pageSize = limit ?? PAGE_LIMIT const pageLimit = pageSize + 1 diff --git a/packages/server/src/startup.ts b/packages/server/src/startup.ts index b4a287d2d4..06b8157850 100644 --- a/packages/server/src/startup.ts +++ b/packages/server/src/startup.ts @@ -1,11 +1,13 @@ import env from "./environment" import * as redis from "./utilities/redis" +import { generateApiKey, getChecklist } from "./utilities/workerRequests" import { - createAdminUser, - generateApiKey, - getChecklist, -} from "./utilities/workerRequests" -import { events, installation, logging, tenancy } from "@budibase/backend-core" + events, + installation, + logging, + tenancy, + users, +} from "@budibase/backend-core" import fs from "fs" import { watch } from "./watch" import * as automations from "./automations" @@ -110,34 +112,37 @@ export async function startup(app?: any, server?: any) { // check and create admin user if required // this must be run after the api has been initialised due to // the app user sync + const bbAdminEmail = env.BB_ADMIN_USER_EMAIL, + bbAdminPassword = env.BB_ADMIN_USER_PASSWORD if ( env.SELF_HOSTED && !env.MULTI_TENANCY && - env.BB_ADMIN_USER_EMAIL && - env.BB_ADMIN_USER_PASSWORD + bbAdminEmail && + bbAdminPassword ) { - const checklist = await getChecklist() - if (!checklist?.adminUser?.checked) { - try { - const tenantId = tenancy.getTenantId() - const user = await createAdminUser( - env.BB_ADMIN_USER_EMAIL, - env.BB_ADMIN_USER_PASSWORD, - tenantId - ) - // Need to set up an API key for automated integration tests - if (env.isTest()) { - await generateApiKey(user._id) - } + const tenantId = tenancy.getTenantId() + await tenancy.doInTenant(tenantId, async () => { + const exists = await users.doesUserExist(bbAdminEmail) + const checklist = await getChecklist() + if (!checklist?.adminUser?.checked || !exists) { + try { + const user = await users.UserDB.createAdminUser( + bbAdminEmail, + bbAdminPassword, + tenantId, + { hashPassword: true, requirePassword: true } + ) + // Need to set up an API key for automated integration tests + if (env.isTest()) { + await generateApiKey(user._id!) + } - console.log( - "Admin account automatically created for", - env.BB_ADMIN_USER_EMAIL - ) - } catch (e) { - logging.logAlert("Error creating initial admin user. Exiting.", e) - shutdown(server) + console.log("Admin account automatically created for", bbAdminEmail) + } catch (e) { + logging.logAlert("Error creating initial admin user. Exiting.", e) + shutdown(server) + } } - } + }) } } diff --git a/packages/server/src/utilities/workerRequests.ts b/packages/server/src/utilities/workerRequests.ts index fa9fde7297..81db7df3c3 100644 --- a/packages/server/src/utilities/workerRequests.ts +++ b/packages/server/src/utilities/workerRequests.ts @@ -167,7 +167,9 @@ export async function createAdminUser( return checkResponse(response, "create admin user") } -export async function getChecklist() { +export async function getChecklist(): Promise<{ + adminUser: { checked: boolean } +}> { const response = await fetch( checkSlashesInUrl(env.WORKER_URL + "/api/global/configs/checklist"), request(undefined, { method: "GET" }) diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 82a1578c88..58979ec799 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -120,28 +120,17 @@ export const adminUser = async ( ) } - const user: User = { - email: email, - password: password, - createdAt: Date.now(), - roles: {}, - builder: { - global: true, - }, - admin: { - global: true, - }, - tenantId, - ssoId, - } try { - // always bust checklist beforehand, if an error occurs but can proceed, don't get - // stuck in a cycle - await cache.bustCache(cache.CacheKey.CHECKLIST) - const finalUser = await userSdk.db.save(user, { - hashPassword, - requirePassword, - }) + const finalUser = await userSdk.db.createAdminUser( + email, + password, + tenantId, + { + ssoId, + hashPassword, + requirePassword, + } + ) // events let account: CloudAccount | undefined