1
0
Fork 0
mirror of synced 2024-06-28 11:00:55 +12: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 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"
@ -10,12 +10,10 @@ import * as sessions from "../security/sessions"
import * as usersCore from "./users"
import {
Account,
AllDocsResponse,
BulkUserCreated,
BulkUserDeleted,
isSSOAccount,
isSSOUser,
RowResponse,
SaveUserOpts,
User,
UserStatus,
@ -487,6 +485,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)
}

View file

@ -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<User> {
* 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<User | undefined> => {
): Promise<User | undefined> {
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

View file

@ -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)
}
}
}
})
}
}

View file

@ -155,19 +155,9 @@ export async function readGlobalUser(ctx: Ctx): Promise<User> {
return checkResponse(response, "get user", { ctx })
}
export async function createAdminUser(
email: string,
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() {
export async function getChecklist(): Promise<{
adminUser: { checked: boolean }
}> {
const response = await fetch(
checkSlashesInUrl(env.WORKER_URL + "/api/global/configs/checklist"),
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 {
// 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