1
0
Fork 0
mirror of synced 2024-06-30 20:10:54 +12:00

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, SaveUserOpts,
User, User,
Account, Account,
isSSOUser,
isSSOAccount,
UserStatus,
} from "@budibase/types" } from "@budibase/types"
import * as accountSdk from "../accounts" import * as accountSdk from "../accounts"
import { validateUniqueUser, getAccountHolderFromUserIds } from "./utils" import {
validateUniqueUser,
getAccountHolderFromUserIds,
isAdmin,
} from "./utils"
import { searchExistingEmails } from "./lookup" import { searchExistingEmails } from "./lookup"
import { hash } from "../utils"
type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any> type QuotaUpdateFn = (change: number, cb?: () => Promise<any>) => Promise<any>
type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any> type GroupUpdateFn = (groupId: string, userIds: string[]) => Promise<any>
type FeatureFn = () => Promise<Boolean>
type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn } type QuotaFns = { addUsers: QuotaUpdateFn; removeUsers: QuotaUpdateFn }
type GroupFns = { addUsers: GroupUpdateFn } type GroupFns = { addUsers: GroupUpdateFn }
type BuildUserFn = ( type FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
user: User,
opts: SaveUserOpts,
tenantId: string,
dbUser?: User,
account?: Account
) => Promise<any>
const bulkDeleteProcessing = async (dbUser: User) => { const bulkDeleteProcessing = async (dbUser: User) => {
const userId = dbUser._id as string const userId = dbUser._id as string
@ -44,19 +47,92 @@ const bulkDeleteProcessing = async (dbUser: User) => {
export class UserDB { export class UserDB {
quotas: QuotaFns quotas: QuotaFns
groups: GroupFns groups: GroupFns
ssoEnforcedFn: () => Promise<boolean> features: FeatureFns
buildUserFn: BuildUserFn
constructor( constructor(quotaFns: QuotaFns, groupFns: GroupFns, featureFns: FeatureFns) {
quotaFns: QuotaFns,
groupFns: GroupFns,
ssoEnforcedFn: () => Promise<boolean>,
buildUserFn: BuildUserFn
) {
this.quotas = quotaFns this.quotas = quotaFns
this.groups = groupFns this.groups = groupFns
this.ssoEnforcedFn = ssoEnforcedFn this.features = featureFns
this.buildUserFn = buildUserFn }
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() { async allUsers() {
@ -150,7 +226,7 @@ export class UserDB {
return this.quotas.addUsers(change, async () => { return this.quotas.addUsers(change, async () => {
await validateUniqueUser(email, tenantId) 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 // don't allow a user to update its own roles/perms
if (opts.currentUserId && opts.currentUserId === dbUser?._id) { if (opts.currentUserId && opts.currentUserId === dbUser?._id) {
builtUser = usersCore.cleanseUserObject(builtUser, dbUser) as User builtUser = usersCore.cleanseUserObject(builtUser, dbUser) as User
@ -231,7 +307,7 @@ export class UserDB {
// create the promises array that will be called by bulkDocs // create the promises array that will be called by bulkDocs
newUsers.forEach((user: any) => { newUsers.forEach((user: any) => {
usersToSave.push( usersToSave.push(
this.buildUserFn( this.buildUser(
user, user,
{ {
hashPassword: true, hashPassword: true,

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

View file

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

View file

@ -56,7 +56,7 @@ export const login = async (ctx: Ctx<LoginRequest>, next: any) => {
const email = ctx.request.body.username const email = ctx.request.body.username
const user = await userSdk.db.getUserByEmail(email) 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") 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.") 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[] = [ export const routes: Router[] = [
configRoutes, configRoutes,
userRoutes, userRoutes,
pro.users,
workspaceRoutes, workspaceRoutes,
authRoutes, authRoutes,
templateRoutes, templateRoutes,

View file

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

View file

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

View file

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

View file

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