Moving app builder API into pro, along with the changes involved with achieving this.

This commit is contained in:
mike12345567 2023-07-27 18:46:55 +01:00
parent d62b2bdbe0
commit 812f1af5ca
10 changed files with 112 additions and 174 deletions

View File

@ -16,22 +16,25 @@ import {
SaveUserOpts,
User,
Account,
isSSOUser,
isSSOAccount,
UserStatus,
} from "@budibase/types"
import * as accountSdk from "../accounts"
import { validateUniqueUser, getAccountHolderFromUserIds } from "./utils"
import {
validateUniqueUser,
getAccountHolderFromUserIds,
isAdmin,
} from "./utils"
import { searchExistingEmails } from "./lookup"
import { hash } from "../utils"
type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any>
type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any>
type FeatureFn = () => Promise<Boolean>
type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn }
type GroupFns = { addUsers: GroupUpdateFn }
type BuildUserFn = (
user: User,
opts: SaveUserOpts,
tenantId: string,
dbUser?: User,
account?: Account
) => Promise<any>
type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
const bulkDeleteProcessing = async (dbUser: User) => {
const userId = dbUser._id as string
@ -44,19 +47,92 @@ const bulkDeleteProcessing = async (dbUser: User) => {
export class UserDB {
quotas: QuotaFns
groups: GroupFns
ssoEnforcedFn: () => Promise<boolean>
buildUserFn: BuildUserFn
features: FeatureFns
constructor(
quotaFns: QuotaFns,
groupFns: GroupFns,
ssoEnforcedFn: () => Promise<boolean>,
buildUserFn: BuildUserFn
) {
constructor(quotaFns: QuotaFns, groupFns: GroupFns, featureFns: FeatureFns) {
this.quotas = quotaFns
this.groups = groupFns
this.ssoEnforcedFn = ssoEnforcedFn
this.buildUserFn = buildUserFn
this.features = featureFns
}
async 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 this.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))
}
async buildUser(
user: User,
opts: SaveUserOpts = {
hashPassword: true,
requirePassword: true,
},
tenantId: string,
dbUser?: any,
account?: Account
): Promise<User> {
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 this.isPreventPasswordActions(user, account)) {
throw new HTTPError("Password change is disabled for this user", 400)
}
hashedPassword = opts.hashPassword ? await hash(password) : password
} else if (dbUser) {
hashedPassword = dbUser.password
}
// passwords are never required if sso is enforced
const requirePasswords =
opts.requirePassword && !(await this.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
}
async allUsers() {
@ -150,7 +226,7 @@ export class UserDB {
return this.quotas.addUsers(change, async () => {
await validateUniqueUser(email, tenantId)
let builtUser = await this.buildUserFn(user, opts, tenantId, dbUser)
let builtUser = await this.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
@ -231,7 +307,7 @@ export class UserDB {
// create the promises array that will be called by bulkDocs
newUsers.forEach((user: any) => {
usersToSave.push(
this.buildUserFn(
this.buildUser(
user,
{
hashPassword: true,

@ -1 +1 @@
Subproject commit a60183319f410d05aaa1c2f2718b772978b54d64
Subproject commit 547b21c09a86c0cef204c89b7c179642ec70670f

View File

@ -9,6 +9,7 @@ export enum Feature {
BRANDING = "branding",
SCIM = "scim",
SYNC_AUTOMATIONS = "syncAutomations",
APP_BUILDERS = "appBuilders",
OFFLINE = "offline",
}

View File

@ -56,7 +56,7 @@ export const login = async (ctx: Ctx<LoginRequest>, next: any) => {
const email = ctx.request.body.username
const user = await userSdk.db.getUserByEmail(email)
if (user && (await userSdk.isPreventPasswordActions(user))) {
if (user && (await userSdk.db.isPreventPasswordActions(user))) {
ctx.throw(403, "Invalid credentials")
}

View File

@ -432,38 +432,3 @@ export const inviteAccept = async (
ctx.throw(400, "Unable to create new user, invitation invalid.")
}
}
export const addAppBuilder = async (ctx: Ctx) => {
const { userId, appId } = ctx.params
const user = await userSdk.db.getUser(userId)
if (userSdk.core.isGlobalBuilder(user)) {
ctx.body = { message: "User already admin - no permissions updated." }
return
}
const prodAppId = dbCore.getProdAppID(appId)
if (!user.builder) {
user.builder = {}
}
if (!user.builder.apps) {
user.builder.apps = []
}
user.builder.apps.push(prodAppId)
await userSdk.db.save(user, { hashPassword: false })
ctx.body = { message: `User "${user.email}" app builder access updated.` }
}
export const removeAppBuilder = async (ctx: Ctx) => {
const { userId, appId } = ctx.params
const user = await userSdk.db.getUser(userId)
if (userSdk.core.isGlobalBuilder(user)) {
ctx.body = { message: "User already admin - no permissions removed." }
return
}
const prodAppId = dbCore.getProdAppID(appId)
const indexOf = user.builder?.apps?.indexOf(prodAppId)
if (user.builder && indexOf != undefined && indexOf !== -1) {
user.builder.apps = user.builder.apps!.splice(indexOf, 1)
}
await userSdk.db.save(user, { hashPassword: false })
ctx.body = { message: `User "${user.email}" app builder access removed.` }
}

View File

@ -23,6 +23,7 @@ import env from "../../environment"
export const routes: Router[] = [
configRoutes,
userRoutes,
pro.users,
workspaceRoutes,
authRoutes,
templateRoutes,

View File

@ -58,7 +58,7 @@ export const reset = async (email: string) => {
}
// exit if user has sso
if (await userSdk.isPreventPasswordActions(user)) {
if (await userSdk.db.isPreventPasswordActions(user)) {
return
}

View File

@ -1,12 +1,6 @@
export * from "./users"
import { buildUser } from "./users"
import { users } from "@budibase/backend-core"
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 const db = new users.UserDB(pro.quotas, pro.groups, pro.features)
export { users as core } from "@budibase/backend-core"

View File

@ -1,6 +1,7 @@
import { structures, mocks } from "../../../tests"
import { env, context } from "@budibase/backend-core"
import * as users from "../users"
import { db as userDb } from "../"
import { CloudAccount } from "@budibase/types"
describe("users", () => {
@ -12,7 +13,7 @@ describe("users", () => {
it("returns false for non sso user", async () => {
await context.doInTenant(structures.tenant.id(), async () => {
const user = structures.users.user()
const result = await users.isPreventPasswordActions(user)
const result = await userDb.isPreventPasswordActions(user)
expect(result).toBe(false)
})
})
@ -23,7 +24,7 @@ describe("users", () => {
const account = structures.accounts.ssoAccount() as CloudAccount
account.email = user.email
mocks.accounts.getAccountByTenantId.mockResolvedValueOnce(account)
const result = await users.isPreventPasswordActions(user)
const result = await userDb.isPreventPasswordActions(user)
expect(result).toBe(true)
})
})
@ -33,7 +34,7 @@ describe("users", () => {
const user = structures.users.user()
const account = structures.accounts.ssoAccount() as CloudAccount
mocks.accounts.getAccountByTenantId.mockResolvedValueOnce(account)
const result = await users.isPreventPasswordActions(user)
const result = await userDb.isPreventPasswordActions(user)
expect(result).toBe(false)
})
})
@ -41,7 +42,7 @@ describe("users", () => {
it("returns true for sso user", async () => {
await context.doInTenant(structures.tenant.id(), async () => {
const user = structures.users.ssoUser()
const result = await users.isPreventPasswordActions(user)
const result = await userDb.isPreventPasswordActions(user)
expect(result).toBe(true)
})
})
@ -51,7 +52,7 @@ describe("users", () => {
await context.doInTenant(structures.tenant.id(), async () => {
const user = structures.users.user()
mocks.pro.features.isSSOEnforced.mockResolvedValueOnce(true)
const result = await users.isPreventPasswordActions(user)
const result = await userDb.isPreventPasswordActions(user)
expect(result).toBe(true)
})
})
@ -69,7 +70,7 @@ describe("users", () => {
describe("non-admin user", () => {
it("returns true", async () => {
const user = structures.users.ssoUser()
const result = await users.isPreventPasswordActions(user)
const result = await userDb.isPreventPasswordActions(user)
expect(result).toBe(true)
})
})
@ -79,7 +80,7 @@ describe("users", () => {
const user = structures.users.ssoUser({
user: structures.users.adminUser(),
})
const result = await users.isPreventPasswordActions(user)
const result = await userDb.isPreventPasswordActions(user)
expect(result).toBe(false)
})
})

View File

@ -1,107 +1,7 @@
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 { events, tenancy, users as usersCore } from "@budibase/backend-core"
import { InviteUsersRequest, InviteUsersResponse } from "@budibase/types"
import { sendEmail } from "../../utilities/email"
import { EmailTemplatePurpose } from "../../constants"
import * as pro from "@budibase/pro"
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<User> {
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