Moving app builder API into pro, along with the changes involved with achieving this.
This commit is contained in:
parent
d62b2bdbe0
commit
812f1af5ca
|
@ -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
|
|
@ -9,6 +9,7 @@ export enum Feature {
|
|||
BRANDING = "branding",
|
||||
SCIM = "scim",
|
||||
SYNC_AUTOMATIONS = "syncAutomations",
|
||||
APP_BUILDERS = "appBuilders",
|
||||
OFFLINE = "offline",
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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.` }
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import env from "../../environment"
|
|||
export const routes: Router[] = [
|
||||
configRoutes,
|
||||
userRoutes,
|
||||
pro.users,
|
||||
workspaceRoutes,
|
||||
authRoutes,
|
||||
templateRoutes,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue