1
0
Fork 0
mirror of synced 2024-09-30 00:57:16 +13:00

Merge pull request #12417 from Budibase/fix/admin-user-backup

Allowing use of BB_ADMIN environment variables at all times
This commit is contained in:
Michael Drury 2023-11-22 11:06:50 +00:00 committed by GitHub
commit 26f07ad4f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 106 additions and 81 deletions

View file

@ -2,7 +2,7 @@ import env from "../environment"
import * as eventHelpers from "./events" import * as eventHelpers from "./events"
import * as accountSdk from "../accounts" import * as accountSdk from "../accounts"
import * as cache from "../cache" import * as cache from "../cache"
import { getGlobalDB, getIdentity, getTenantId } from "../context" import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context"
import * as dbUtils from "../db" import * as dbUtils from "../db"
import { EmailUnavailableError, HTTPError } from "../errors" import { EmailUnavailableError, HTTPError } from "../errors"
import * as platform from "../platform" import * as platform from "../platform"
@ -10,12 +10,10 @@ import * as sessions from "../security/sessions"
import * as usersCore from "./users" import * as usersCore from "./users"
import { import {
Account, Account,
AllDocsResponse,
BulkUserCreated, BulkUserCreated,
BulkUserDeleted, BulkUserDeleted,
isSSOAccount, isSSOAccount,
isSSOUser, isSSOUser,
RowResponse,
SaveUserOpts, SaveUserOpts,
User, User,
UserStatus, UserStatus,
@ -487,6 +485,37 @@ export class UserDB {
await sessions.invalidateSessions(userId, { reason: "deletion" }) 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[]) { static async getGroups(groupIds: string[]) {
return await this.groups.getBulk(groupIds) return await this.groups.getBulk(groupIds)
} }

View file

@ -43,7 +43,7 @@ function removeUserPassword(users: User | User[]) {
return users return users
} }
export const isSupportedUserSearch = (query: SearchQuery) => { export function isSupportedUserSearch(query: SearchQuery) {
const allowed = [ const allowed = [
{ op: SearchQueryOperators.STRING, key: "email" }, { op: SearchQueryOperators.STRING, key: "email" },
{ op: SearchQueryOperators.EQUAL, key: "_id" }, { op: SearchQueryOperators.EQUAL, key: "_id" },
@ -68,10 +68,10 @@ export const isSupportedUserSearch = (query: SearchQuery) => {
return true return true
} }
export const bulkGetGlobalUsersById = async ( export async function bulkGetGlobalUsersById(
userIds: string[], userIds: string[],
opts?: GetOpts opts?: GetOpts
) => { ) {
const db = getGlobalDB() const db = getGlobalDB()
let users = ( let users = (
await db.allDocs({ await db.allDocs({
@ -85,7 +85,7 @@ export const bulkGetGlobalUsersById = async (
return users return users
} }
export const getAllUserIds = async () => { export async function getAllUserIds() {
const db = getGlobalDB() const db = getGlobalDB()
const startKey = `${DocumentType.USER}${SEPARATOR}` const startKey = `${DocumentType.USER}${SEPARATOR}`
const response = await db.allDocs({ const response = await db.allDocs({
@ -95,7 +95,7 @@ export const getAllUserIds = async () => {
return response.rows.map(row => row.id) return response.rows.map(row => row.id)
} }
export const bulkUpdateGlobalUsers = async (users: User[]) => { export async function bulkUpdateGlobalUsers(users: User[]) {
const db = getGlobalDB() const db = getGlobalDB()
return (await db.bulkDocs(users)) as BulkDocsResponse return (await db.bulkDocs(users)) as BulkDocsResponse
} }
@ -113,10 +113,10 @@ export async function getById(id: string, opts?: GetOpts): Promise<User> {
* Given an email address this will use a view to search through * Given an email address this will use a view to search through
* all the users to find one with this email address. * all the users to find one with this email address.
*/ */
export const getGlobalUserByEmail = async ( export async function getGlobalUserByEmail(
email: String, email: String,
opts?: GetOpts opts?: GetOpts
): Promise<User | undefined> => { ): Promise<User | undefined> {
if (email == null) { if (email == null) {
throw "Must supply an email address to view" throw "Must supply an email address to view"
} }
@ -139,11 +139,23 @@ export const getGlobalUserByEmail = async (
return user 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, appId: any,
opts: DatabaseQueryOpts, opts: DatabaseQueryOpts,
getOpts?: GetOpts getOpts?: GetOpts
) => { ) {
if (typeof appId !== "string") { if (typeof appId !== "string") {
throw new Error("Must provide a string based app ID") 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 Return any user who potentially has access to the application
Admins, developers and app users with the explicitly role. Admins, developers and app users with the explicitly role.
*/ */
export const searchGlobalUsersByAppAccess = async ( export async function searchGlobalUsersByAppAccess(
appId: any, appId: any,
opts?: { limit?: number } opts?: { limit?: number }
) => { ) {
const roleSelector = `roles.${appId}` const roleSelector = `roles.${appId}`
let orQuery: any[] = [ let orQuery: any[] = [
@ -205,7 +217,7 @@ export const searchGlobalUsersByAppAccess = async (
return resp.rows return resp.rows
} }
export const getGlobalUserByAppPage = (appId: string, user: User) => { export function getGlobalUserByAppPage(appId: string, user: User) {
if (!user) { if (!user) {
return return
} }
@ -215,11 +227,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => {
/** /**
* Performs a starts with search on the global email view. * Performs a starts with search on the global email view.
*/ */
export const searchGlobalUsersByEmail = async ( export async function searchGlobalUsersByEmail(
email: string | unknown, email: string | unknown,
opts: any, opts: any,
getOpts?: GetOpts getOpts?: GetOpts
) => { ) {
if (typeof email !== "string") { if (typeof email !== "string") {
throw new Error("Must provide a string to search by") throw new Error("Must provide a string to search by")
} }
@ -242,12 +254,12 @@ export const searchGlobalUsersByEmail = async (
} }
const PAGE_LIMIT = 8 const PAGE_LIMIT = 8
export const paginatedUsers = async ({ export async function paginatedUsers({
bookmark, bookmark,
query, query,
appId, appId,
limit, limit,
}: SearchUsersRequest = {}) => { }: SearchUsersRequest = {}) {
const db = getGlobalDB() const db = getGlobalDB()
const pageSize = limit ?? PAGE_LIMIT const pageSize = limit ?? PAGE_LIMIT
const pageLimit = pageSize + 1 const pageLimit = pageSize + 1

View file

@ -1,11 +1,13 @@
import env from "./environment" import env from "./environment"
import * as redis from "./utilities/redis" import * as redis from "./utilities/redis"
import { generateApiKey, getChecklist } from "./utilities/workerRequests"
import { import {
createAdminUser, events,
generateApiKey, installation,
getChecklist, logging,
} from "./utilities/workerRequests" tenancy,
import { events, installation, logging, tenancy } from "@budibase/backend-core" users,
} from "@budibase/backend-core"
import fs from "fs" import fs from "fs"
import { watch } from "./watch" import { watch } from "./watch"
import * as automations from "./automations" import * as automations from "./automations"
@ -110,34 +112,37 @@ export async function startup(app?: any, server?: any) {
// check and create admin user if required // check and create admin user if required
// this must be run after the api has been initialised due to // this must be run after the api has been initialised due to
// the app user sync // the app user sync
const bbAdminEmail = env.BB_ADMIN_USER_EMAIL,
bbAdminPassword = env.BB_ADMIN_USER_PASSWORD
if ( if (
env.SELF_HOSTED && env.SELF_HOSTED &&
!env.MULTI_TENANCY && !env.MULTI_TENANCY &&
env.BB_ADMIN_USER_EMAIL && bbAdminEmail &&
env.BB_ADMIN_USER_PASSWORD bbAdminPassword
) { ) {
const checklist = await getChecklist() const tenantId = tenancy.getTenantId()
if (!checklist?.adminUser?.checked) { await tenancy.doInTenant(tenantId, async () => {
try { const exists = await users.doesUserExist(bbAdminEmail)
const tenantId = tenancy.getTenantId() const checklist = await getChecklist()
const user = await createAdminUser( if (!checklist?.adminUser?.checked || !exists) {
env.BB_ADMIN_USER_EMAIL, try {
env.BB_ADMIN_USER_PASSWORD, const user = await users.UserDB.createAdminUser(
tenantId bbAdminEmail,
) bbAdminPassword,
// Need to set up an API key for automated integration tests tenantId,
if (env.isTest()) { { hashPassword: true, requirePassword: true }
await generateApiKey(user._id) )
} // Need to set up an API key for automated integration tests
if (env.isTest()) {
await generateApiKey(user._id!)
}
console.log( console.log("Admin account automatically created for", bbAdminEmail)
"Admin account automatically created for", } catch (e) {
env.BB_ADMIN_USER_EMAIL logging.logAlert("Error creating initial admin user. Exiting.", e)
) shutdown(server)
} catch (e) { }
logging.logAlert("Error creating initial admin user. Exiting.", e)
shutdown(server)
} }
} })
} }
} }

View file

@ -155,19 +155,9 @@ export async function readGlobalUser(ctx: Ctx): Promise<User> {
return checkResponse(response, "get user", { ctx }) return checkResponse(response, "get user", { ctx })
} }
export async function createAdminUser( export async function getChecklist(): Promise<{
email: string, adminUser: { checked: boolean }
password: string, }> {
tenantId: string
) {
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + "/api/global/users/init"),
request(undefined, { method: "POST", body: { email, password, tenantId } })
)
return checkResponse(response, "create admin user")
}
export async function getChecklist() {
const response = await fetch( const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + "/api/global/configs/checklist"), checkSlashesInUrl(env.WORKER_URL + "/api/global/configs/checklist"),
request(undefined, { method: "GET" }) request(undefined, { method: "GET" })

View file

@ -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 { try {
// always bust checklist beforehand, if an error occurs but can proceed, don't get const finalUser = await userSdk.db.createAdminUser(
// stuck in a cycle email,
await cache.bustCache(cache.CacheKey.CHECKLIST) password,
const finalUser = await userSdk.db.save(user, { tenantId,
hashPassword, {
requirePassword, ssoId,
}) hashPassword,
requirePassword,
}
)
// events // events
let account: CloudAccount | undefined let account: CloudAccount | undefined