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,
|
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
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.` }
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue