Merge branch 'develop' into chore/typecheck_tests
This commit is contained in:
commit
e011ccd0c4
|
@ -23,6 +23,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/nano": "10.1.2",
|
"@budibase/nano": "10.1.2",
|
||||||
"@budibase/pouchdb-replication-stream": "1.2.10",
|
"@budibase/pouchdb-replication-stream": "1.2.10",
|
||||||
|
"@budibase/shared-core": "0.0.0",
|
||||||
"@budibase/types": "0.0.0",
|
"@budibase/types": "0.0.0",
|
||||||
"@techpass/passport-openidconnect": "0.3.2",
|
"@techpass/passport-openidconnect": "0.3.2",
|
||||||
"aws-cloudfront-sign": "2.2.0",
|
"aws-cloudfront-sign": "2.2.0",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export const SEPARATOR = "_"
|
import { prefixed, DocumentType } from "@budibase/types"
|
||||||
export const UNICODE_MAX = "\ufff0"
|
export { SEPARATOR, UNICODE_MAX, DocumentType } from "@budibase/types"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can be used to create a few different forms of querying a view.
|
* Can be used to create a few different forms of querying a view.
|
||||||
|
@ -14,8 +14,6 @@ export enum ViewName {
|
||||||
USER_BY_APP = "by_app",
|
USER_BY_APP = "by_app",
|
||||||
USER_BY_EMAIL = "by_email2",
|
USER_BY_EMAIL = "by_email2",
|
||||||
BY_API_KEY = "by_api_key",
|
BY_API_KEY = "by_api_key",
|
||||||
/** @deprecated - could be deleted */
|
|
||||||
USER_BY_BUILDERS = "by_builders",
|
|
||||||
LINK = "by_link",
|
LINK = "by_link",
|
||||||
ROUTING = "screen_routes",
|
ROUTING = "screen_routes",
|
||||||
AUTOMATION_LOGS = "automation_logs",
|
AUTOMATION_LOGS = "automation_logs",
|
||||||
|
@ -36,42 +34,6 @@ export enum InternalTable {
|
||||||
USER_METADATA = "ta_users",
|
USER_METADATA = "ta_users",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum DocumentType {
|
|
||||||
USER = "us",
|
|
||||||
GROUP = "gr",
|
|
||||||
WORKSPACE = "workspace",
|
|
||||||
CONFIG = "config",
|
|
||||||
TEMPLATE = "template",
|
|
||||||
APP = "app",
|
|
||||||
DEV = "dev",
|
|
||||||
APP_DEV = "app_dev",
|
|
||||||
APP_METADATA = "app_metadata",
|
|
||||||
ROLE = "role",
|
|
||||||
MIGRATIONS = "migrations",
|
|
||||||
DEV_INFO = "devinfo",
|
|
||||||
AUTOMATION_LOG = "log_au",
|
|
||||||
ACCOUNT_METADATA = "acc_metadata",
|
|
||||||
PLUGIN = "plg",
|
|
||||||
DATASOURCE = "datasource",
|
|
||||||
DATASOURCE_PLUS = "datasource_plus",
|
|
||||||
APP_BACKUP = "backup",
|
|
||||||
TABLE = "ta",
|
|
||||||
ROW = "ro",
|
|
||||||
AUTOMATION = "au",
|
|
||||||
LINK = "li",
|
|
||||||
WEBHOOK = "wh",
|
|
||||||
INSTANCE = "inst",
|
|
||||||
LAYOUT = "layout",
|
|
||||||
SCREEN = "screen",
|
|
||||||
QUERY = "query",
|
|
||||||
DEPLOYMENTS = "deployments",
|
|
||||||
METADATA = "metadata",
|
|
||||||
MEM_VIEW = "view",
|
|
||||||
USER_FLAG = "flag",
|
|
||||||
AUTOMATION_METADATA = "meta_au",
|
|
||||||
AUDIT_LOG = "al",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const StaticDatabases = {
|
export const StaticDatabases = {
|
||||||
GLOBAL: {
|
GLOBAL: {
|
||||||
name: "global-db",
|
name: "global-db",
|
||||||
|
@ -95,7 +57,7 @@ export const StaticDatabases = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const APP_PREFIX = DocumentType.APP + SEPARATOR
|
export const APP_PREFIX = prefixed(DocumentType.APP)
|
||||||
export const APP_DEV = DocumentType.APP_DEV + SEPARATOR
|
export const APP_DEV = prefixed(DocumentType.APP_DEV)
|
||||||
export const APP_DEV_PREFIX = APP_DEV
|
export const APP_DEV_PREFIX = APP_DEV
|
||||||
export const BUDIBASE_DATASOURCE_TYPE = "budibase"
|
export const BUDIBASE_DATASOURCE_TYPE = "budibase"
|
||||||
|
|
|
@ -105,16 +105,6 @@ export const createApiKeyView = async () => {
|
||||||
await createView(db, viewJs, ViewName.BY_API_KEY)
|
await createView(db, viewJs, ViewName.BY_API_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createUserBuildersView = async () => {
|
|
||||||
const db = getGlobalDB()
|
|
||||||
const viewJs = `function(doc) {
|
|
||||||
if (doc.builder && doc.builder.global === true) {
|
|
||||||
emit(doc._id, doc._id)
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
await createView(db, viewJs, ViewName.USER_BY_BUILDERS)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueryViewOptions {
|
export interface QueryViewOptions {
|
||||||
arrayResponse?: boolean
|
arrayResponse?: boolean
|
||||||
}
|
}
|
||||||
|
@ -223,7 +213,6 @@ export const queryPlatformView = async <T>(
|
||||||
const CreateFuncByName: any = {
|
const CreateFuncByName: any = {
|
||||||
[ViewName.USER_BY_EMAIL]: createNewUserEmailView,
|
[ViewName.USER_BY_EMAIL]: createNewUserEmailView,
|
||||||
[ViewName.BY_API_KEY]: createApiKeyView,
|
[ViewName.BY_API_KEY]: createApiKeyView,
|
||||||
[ViewName.USER_BY_BUILDERS]: createUserBuildersView,
|
|
||||||
[ViewName.USER_BY_APP]: createUserAppView,
|
[ViewName.USER_BY_APP]: createUserAppView,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { existsSync, readFileSync } from "fs"
|
import { existsSync, readFileSync } from "fs"
|
||||||
|
import { ServiceType } from "@budibase/types"
|
||||||
|
|
||||||
function isTest() {
|
function isTest() {
|
||||||
return isCypress() || isJest()
|
return isCypress() || isJest()
|
||||||
|
@ -83,10 +84,20 @@ function getPackageJsonFields(): {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWorker() {
|
||||||
|
return environment.SERVICE_TYPE === ServiceType.WORKER
|
||||||
|
}
|
||||||
|
|
||||||
|
function isApps() {
|
||||||
|
return environment.SERVICE_TYPE === ServiceType.APPS
|
||||||
|
}
|
||||||
|
|
||||||
const environment = {
|
const environment = {
|
||||||
isTest,
|
isTest,
|
||||||
isJest,
|
isJest,
|
||||||
isDev,
|
isDev,
|
||||||
|
isWorker,
|
||||||
|
isApps,
|
||||||
isProd: () => {
|
isProd: () => {
|
||||||
return !isDev()
|
return !isDev()
|
||||||
},
|
},
|
||||||
|
@ -153,6 +164,7 @@ const environment = {
|
||||||
SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS,
|
SMTP_FROM_ADDRESS: process.env.SMTP_FROM_ADDRESS,
|
||||||
DISABLE_JWT_WARNING: process.env.DISABLE_JWT_WARNING,
|
DISABLE_JWT_WARNING: process.env.DISABLE_JWT_WARNING,
|
||||||
BLACKLIST_IPS: process.env.BLACKLIST_IPS,
|
BLACKLIST_IPS: process.env.BLACKLIST_IPS,
|
||||||
|
SERVICE_TYPE: "unknown",
|
||||||
/**
|
/**
|
||||||
* Enable to allow an admin user to login using a password.
|
* Enable to allow an admin user to login using a password.
|
||||||
* This can be useful to prevent lockout when configuring SSO.
|
* This can be useful to prevent lockout when configuring SSO.
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { processors } from "./processors"
|
||||||
import { newid } from "../utils"
|
import { newid } from "../utils"
|
||||||
import * as installation from "../installation"
|
import * as installation from "../installation"
|
||||||
import * as configs from "../configs"
|
import * as configs from "../configs"
|
||||||
|
import * as users from "../users"
|
||||||
import { withCache, TTL, CacheKey } from "../cache/generic"
|
import { withCache, TTL, CacheKey } from "../cache/generic"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -164,8 +165,8 @@ const identifyUser = async (
|
||||||
const id = user._id as string
|
const id = user._id as string
|
||||||
const tenantId = await getEventTenantId(user.tenantId)
|
const tenantId = await getEventTenantId(user.tenantId)
|
||||||
const type = IdentityType.USER
|
const type = IdentityType.USER
|
||||||
let builder = user.builder?.global || false
|
let builder = users.hasBuilderPermissions(user)
|
||||||
let admin = user.admin?.global || false
|
let admin = users.hasAdminPermissions(user)
|
||||||
let providerType
|
let providerType
|
||||||
if (isSSOUser(user)) {
|
if (isSSOUser(user)) {
|
||||||
providerType = user.providerType
|
providerType = user.providerType
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { BBContext } from "@budibase/types"
|
import { UserCtx } from "@budibase/types"
|
||||||
|
import { isAdmin } from "../users"
|
||||||
|
|
||||||
export default async (ctx: BBContext, next: any) => {
|
export default async (ctx: UserCtx, next: any) => {
|
||||||
if (
|
if (!ctx.internal && !isAdmin(ctx.user)) {
|
||||||
!ctx.internal &&
|
|
||||||
(!ctx.user || !ctx.user.admin || !ctx.user.admin.global)
|
|
||||||
) {
|
|
||||||
ctx.throw(403, "Admin user only endpoint.")
|
ctx.throw(403, "Admin user only endpoint.")
|
||||||
}
|
}
|
||||||
return next()
|
return next()
|
||||||
|
|
|
@ -1,10 +1,19 @@
|
||||||
import { BBContext } from "@budibase/types"
|
import { UserCtx } from "@budibase/types"
|
||||||
|
import { isBuilder, hasBuilderPermissions } from "../users"
|
||||||
|
import { getAppId } from "../context"
|
||||||
|
import env from "../environment"
|
||||||
|
|
||||||
export default async (ctx: BBContext, next: any) => {
|
export default async (ctx: UserCtx, next: any) => {
|
||||||
if (
|
const appId = getAppId()
|
||||||
!ctx.internal &&
|
const builderFn = env.isWorker()
|
||||||
(!ctx.user || !ctx.user.builder || !ctx.user.builder.global)
|
? hasBuilderPermissions
|
||||||
) {
|
: env.isApps()
|
||||||
|
? isBuilder
|
||||||
|
: undefined
|
||||||
|
if (!builderFn) {
|
||||||
|
throw new Error("Service name unknown - middleware inactive.")
|
||||||
|
}
|
||||||
|
if (!ctx.internal && !builderFn(ctx.user, appId)) {
|
||||||
ctx.throw(403, "Builder user only endpoint.")
|
ctx.throw(403, "Builder user only endpoint.")
|
||||||
}
|
}
|
||||||
return next()
|
return next()
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
import { BBContext } from "@budibase/types"
|
import { UserCtx } from "@budibase/types"
|
||||||
|
import { isBuilder, isAdmin, hasBuilderPermissions } from "../users"
|
||||||
|
import { getAppId } from "../context"
|
||||||
|
import env from "../environment"
|
||||||
|
|
||||||
export default async (ctx: BBContext, next: any) => {
|
export default async (ctx: UserCtx, next: any) => {
|
||||||
if (
|
const appId = getAppId()
|
||||||
!ctx.internal &&
|
const builderFn = env.isWorker()
|
||||||
(!ctx.user || !ctx.user.builder || !ctx.user.builder.global) &&
|
? hasBuilderPermissions
|
||||||
(!ctx.user || !ctx.user.admin || !ctx.user.admin.global)
|
: env.isApps()
|
||||||
) {
|
? isBuilder
|
||||||
ctx.throw(403, "Builder user only endpoint.")
|
: undefined
|
||||||
|
if (!builderFn) {
|
||||||
|
throw new Error("Service name unknown - middleware inactive.")
|
||||||
|
}
|
||||||
|
if (!ctx.internal && !builderFn(ctx.user, appId) && !isAdmin(ctx.user)) {
|
||||||
|
ctx.throw(403, "Admin/Builder user only endpoint.")
|
||||||
}
|
}
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
import adminOnly from "../adminOnly"
|
||||||
|
import builderOnly from "../builderOnly"
|
||||||
|
import builderOrAdmin from "../builderOrAdmin"
|
||||||
|
import { structures } from "../../../tests"
|
||||||
|
import { ContextUser, ServiceType } from "@budibase/types"
|
||||||
|
import { doInAppContext } from "../../context"
|
||||||
|
import env from "../../environment"
|
||||||
|
env._set("SERVICE_TYPE", ServiceType.APPS)
|
||||||
|
|
||||||
|
const appId = "app_aaa"
|
||||||
|
const basicUser = structures.users.user()
|
||||||
|
const adminUser = structures.users.adminUser()
|
||||||
|
const adminOnlyUser = structures.users.adminOnlyUser()
|
||||||
|
const builderUser = structures.users.builderUser()
|
||||||
|
const appBuilderUser = structures.users.appBuilderUser(appId)
|
||||||
|
|
||||||
|
function buildUserCtx(user: ContextUser) {
|
||||||
|
return {
|
||||||
|
internal: false,
|
||||||
|
user,
|
||||||
|
throw: jest.fn(),
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
function passed(throwFn: jest.Func, nextFn: jest.Func) {
|
||||||
|
expect(throwFn).not.toBeCalled()
|
||||||
|
expect(nextFn).toBeCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
function threw(throwFn: jest.Func) {
|
||||||
|
// cant check next, the throw function doesn't actually throw - so it still continues
|
||||||
|
expect(throwFn).toBeCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("adminOnly middleware", () => {
|
||||||
|
it("should allow admin user", () => {
|
||||||
|
const ctx = buildUserCtx(adminUser),
|
||||||
|
next = jest.fn()
|
||||||
|
adminOnly(ctx, next)
|
||||||
|
passed(ctx.throw, next)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not allow basic user", () => {
|
||||||
|
const ctx = buildUserCtx(basicUser),
|
||||||
|
next = jest.fn()
|
||||||
|
adminOnly(ctx, next)
|
||||||
|
threw(ctx.throw)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not allow builder user", () => {
|
||||||
|
const ctx = buildUserCtx(builderUser),
|
||||||
|
next = jest.fn()
|
||||||
|
adminOnly(ctx, next)
|
||||||
|
threw(ctx.throw)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("builderOnly middleware", () => {
|
||||||
|
it("should allow builder user", () => {
|
||||||
|
const ctx = buildUserCtx(builderUser),
|
||||||
|
next = jest.fn()
|
||||||
|
builderOnly(ctx, next)
|
||||||
|
passed(ctx.throw, next)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow app builder user", () => {
|
||||||
|
const ctx = buildUserCtx(appBuilderUser),
|
||||||
|
next = jest.fn()
|
||||||
|
doInAppContext(appId, () => {
|
||||||
|
builderOnly(ctx, next)
|
||||||
|
})
|
||||||
|
passed(ctx.throw, next)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow admin and builder user", () => {
|
||||||
|
const ctx = buildUserCtx(adminUser),
|
||||||
|
next = jest.fn()
|
||||||
|
builderOnly(ctx, next)
|
||||||
|
passed(ctx.throw, next)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not allow admin user", () => {
|
||||||
|
const ctx = buildUserCtx(adminOnlyUser),
|
||||||
|
next = jest.fn()
|
||||||
|
builderOnly(ctx, next)
|
||||||
|
threw(ctx.throw)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not allow app builder user to different app", () => {
|
||||||
|
const ctx = buildUserCtx(appBuilderUser),
|
||||||
|
next = jest.fn()
|
||||||
|
doInAppContext("app_bbb", () => {
|
||||||
|
builderOnly(ctx, next)
|
||||||
|
})
|
||||||
|
threw(ctx.throw)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not allow basic user", () => {
|
||||||
|
const ctx = buildUserCtx(basicUser),
|
||||||
|
next = jest.fn()
|
||||||
|
builderOnly(ctx, next)
|
||||||
|
threw(ctx.throw)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("builderOrAdmin middleware", () => {
|
||||||
|
it("should allow builder user", () => {
|
||||||
|
const ctx = buildUserCtx(builderUser),
|
||||||
|
next = jest.fn()
|
||||||
|
builderOrAdmin(ctx, next)
|
||||||
|
passed(ctx.throw, next)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow builder and admin user", () => {
|
||||||
|
const ctx = buildUserCtx(adminUser),
|
||||||
|
next = jest.fn()
|
||||||
|
builderOrAdmin(ctx, next)
|
||||||
|
passed(ctx.throw, next)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow admin user", () => {
|
||||||
|
const ctx = buildUserCtx(adminOnlyUser),
|
||||||
|
next = jest.fn()
|
||||||
|
builderOrAdmin(ctx, next)
|
||||||
|
passed(ctx.throw, next)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow app builder user", () => {
|
||||||
|
const ctx = buildUserCtx(appBuilderUser),
|
||||||
|
next = jest.fn()
|
||||||
|
doInAppContext(appId, () => {
|
||||||
|
builderOrAdmin(ctx, next)
|
||||||
|
})
|
||||||
|
passed(ctx.throw, next)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not allow basic user", () => {
|
||||||
|
const ctx = buildUserCtx(basicUser),
|
||||||
|
next = jest.fn()
|
||||||
|
builderOrAdmin(ctx, next)
|
||||||
|
threw(ctx.throw)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("check service difference", () => {
|
||||||
|
it("should not allow without app ID in apps", () => {
|
||||||
|
env._set("SERVICE_TYPE", ServiceType.APPS)
|
||||||
|
const appId = "app_a"
|
||||||
|
const ctx = buildUserCtx({
|
||||||
|
...basicUser,
|
||||||
|
builder: {
|
||||||
|
apps: [appId],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const next = jest.fn()
|
||||||
|
doInAppContext(appId, () => {
|
||||||
|
builderOnly(ctx, next)
|
||||||
|
})
|
||||||
|
passed(ctx.throw, next)
|
||||||
|
doInAppContext("app_b", () => {
|
||||||
|
builderOnly(ctx, next)
|
||||||
|
})
|
||||||
|
threw(ctx.throw)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should allow without app ID in worker", () => {
|
||||||
|
env._set("SERVICE_TYPE", ServiceType.WORKER)
|
||||||
|
const ctx = buildUserCtx({
|
||||||
|
...basicUser,
|
||||||
|
builder: {
|
||||||
|
apps: ["app_a"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const next = jest.fn()
|
||||||
|
doInAppContext("app_b", () => {
|
||||||
|
builderOnly(ctx, next)
|
||||||
|
})
|
||||||
|
passed(ctx.throw, next)
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { PermissionType, PermissionLevel } from "@budibase/types"
|
||||||
|
export { PermissionType, PermissionLevel } from "@budibase/types"
|
||||||
import flatten from "lodash/flatten"
|
import flatten from "lodash/flatten"
|
||||||
import cloneDeep from "lodash/fp/cloneDeep"
|
import cloneDeep from "lodash/fp/cloneDeep"
|
||||||
|
|
||||||
|
@ -5,25 +7,6 @@ export type RoleHierarchy = {
|
||||||
permissionId: string
|
permissionId: string
|
||||||
}[]
|
}[]
|
||||||
|
|
||||||
export enum PermissionLevel {
|
|
||||||
READ = "read",
|
|
||||||
WRITE = "write",
|
|
||||||
EXECUTE = "execute",
|
|
||||||
ADMIN = "admin",
|
|
||||||
}
|
|
||||||
|
|
||||||
// these are the global types, that govern the underlying default behaviour
|
|
||||||
export enum PermissionType {
|
|
||||||
APP = "app",
|
|
||||||
TABLE = "table",
|
|
||||||
USER = "user",
|
|
||||||
AUTOMATION = "automation",
|
|
||||||
WEBHOOK = "webhook",
|
|
||||||
BUILDER = "builder",
|
|
||||||
VIEW = "view",
|
|
||||||
QUERY = "query",
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Permission {
|
export class Permission {
|
||||||
type: PermissionType
|
type: PermissionType
|
||||||
level: PermissionLevel
|
level: PermissionLevel
|
||||||
|
@ -173,3 +156,4 @@ export function isPermissionLevelHigherThanRead(level: PermissionLevel) {
|
||||||
|
|
||||||
// utility as a lot of things need simply the builder permission
|
// utility as a lot of things need simply the builder permission
|
||||||
export const BUILDER = PermissionType.BUILDER
|
export const BUILDER = PermissionType.BUILDER
|
||||||
|
export const GLOBAL_BUILDER = PermissionType.GLOBAL_BUILDER
|
||||||
|
|
|
@ -0,0 +1,460 @@
|
||||||
|
import env from "../environment"
|
||||||
|
import * as eventHelpers from "./events"
|
||||||
|
import * as accounts from "../accounts"
|
||||||
|
import * as cache from "../cache"
|
||||||
|
import { getIdentity, getTenantId, getGlobalDB } from "../context"
|
||||||
|
import * as dbUtils from "../db"
|
||||||
|
import { EmailUnavailableError, HTTPError } from "../errors"
|
||||||
|
import * as platform from "../platform"
|
||||||
|
import * as sessions from "../security/sessions"
|
||||||
|
import * as usersCore from "./users"
|
||||||
|
import {
|
||||||
|
AllDocsResponse,
|
||||||
|
BulkUserCreated,
|
||||||
|
BulkUserDeleted,
|
||||||
|
RowResponse,
|
||||||
|
SaveUserOpts,
|
||||||
|
User,
|
||||||
|
Account,
|
||||||
|
isSSOUser,
|
||||||
|
isSSOAccount,
|
||||||
|
UserStatus,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import * as accountSdk from "../accounts"
|
||||||
|
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 FeatureFns = { isSSOEnforced: FeatureFn; isAppBuildersEnabled: FeatureFn }
|
||||||
|
|
||||||
|
const bulkDeleteProcessing = async (dbUser: User) => {
|
||||||
|
const userId = dbUser._id as string
|
||||||
|
await platform.users.removeUser(dbUser)
|
||||||
|
await eventHelpers.handleDeleteEvents(dbUser)
|
||||||
|
await cache.user.invalidateUser(userId)
|
||||||
|
await sessions.invalidateSessions(userId, { reason: "bulk-deletion" })
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserDB {
|
||||||
|
static quotas: QuotaFns
|
||||||
|
static groups: GroupFns
|
||||||
|
static features: FeatureFns
|
||||||
|
|
||||||
|
static init(quotaFns: QuotaFns, groupFns: GroupFns, featureFns: FeatureFns) {
|
||||||
|
UserDB.quotas = quotaFns
|
||||||
|
UserDB.groups = groupFns
|
||||||
|
UserDB.features = featureFns
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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 UserDB.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))
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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 UserDB.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 UserDB.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
|
||||||
|
}
|
||||||
|
|
||||||
|
static async allUsers() {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
const response = await db.allDocs(
|
||||||
|
dbUtils.getGlobalUserParams(null, {
|
||||||
|
include_docs: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return response.rows.map((row: any) => row.doc)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async countUsersByApp(appId: string) {
|
||||||
|
let response: any = await usersCore.searchGlobalUsersByApp(appId, {})
|
||||||
|
return {
|
||||||
|
userCount: response.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getUsersByAppAccess(appId?: string) {
|
||||||
|
const opts: any = {
|
||||||
|
include_docs: true,
|
||||||
|
limit: 50,
|
||||||
|
}
|
||||||
|
let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
|
||||||
|
appId,
|
||||||
|
opts
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getUserByEmail(email: string) {
|
||||||
|
return usersCore.getGlobalUserByEmail(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a user by ID from the global database, based on the current tenancy.
|
||||||
|
*/
|
||||||
|
static async getUser(userId: string) {
|
||||||
|
const user = await usersCore.getById(userId)
|
||||||
|
if (user) {
|
||||||
|
delete user.password
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
static async save(user: User, opts: SaveUserOpts = {}): Promise<User> {
|
||||||
|
// default booleans to true
|
||||||
|
if (opts.hashPassword == null) {
|
||||||
|
opts.hashPassword = true
|
||||||
|
}
|
||||||
|
if (opts.requirePassword == null) {
|
||||||
|
opts.requirePassword = true
|
||||||
|
}
|
||||||
|
const tenantId = getTenantId()
|
||||||
|
const db = getGlobalDB()
|
||||||
|
|
||||||
|
let { email, _id, userGroups = [], roles } = user
|
||||||
|
|
||||||
|
if (!email && !_id) {
|
||||||
|
throw new Error("_id or email is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
user.builder?.apps?.length &&
|
||||||
|
!(await UserDB.features.isAppBuildersEnabled())
|
||||||
|
) {
|
||||||
|
throw new Error("Unable to update app builders, please check license")
|
||||||
|
}
|
||||||
|
|
||||||
|
let dbUser: User | undefined
|
||||||
|
if (_id) {
|
||||||
|
// try to get existing user from db
|
||||||
|
try {
|
||||||
|
dbUser = (await db.get(_id)) as User
|
||||||
|
if (email && dbUser.email !== email) {
|
||||||
|
throw "Email address cannot be changed"
|
||||||
|
}
|
||||||
|
email = dbUser.email
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.status === 404) {
|
||||||
|
// do nothing, save this new user with the id specified - required for SSO auth
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dbUser && email) {
|
||||||
|
// no id was specified - load from email instead
|
||||||
|
dbUser = await usersCore.getGlobalUserByEmail(email)
|
||||||
|
if (dbUser && dbUser._id !== _id) {
|
||||||
|
throw new EmailUnavailableError(email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const change = dbUser ? 0 : 1 // no change if there is existing user
|
||||||
|
return UserDB.quotas.addUsers(change, async () => {
|
||||||
|
await validateUniqueUser(email, tenantId)
|
||||||
|
|
||||||
|
let builtUser = await UserDB.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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dbUser && roles?.length) {
|
||||||
|
builtUser.roles = { ...roles }
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we set the _id field for a new user
|
||||||
|
// Also if this is a new user, associate groups with them
|
||||||
|
let groupPromises = []
|
||||||
|
if (!_id) {
|
||||||
|
_id = builtUser._id!
|
||||||
|
|
||||||
|
if (userGroups.length > 0) {
|
||||||
|
for (let groupId of userGroups) {
|
||||||
|
groupPromises.push(UserDB.groups.addUsers(groupId, [_id!]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// save the user to db
|
||||||
|
let response = await db.put(builtUser)
|
||||||
|
builtUser._rev = response.rev
|
||||||
|
|
||||||
|
await eventHelpers.handleSaveEvents(builtUser, dbUser)
|
||||||
|
await platform.users.addUser(tenantId, builtUser._id!, builtUser.email)
|
||||||
|
await cache.user.invalidateUser(response.id)
|
||||||
|
|
||||||
|
await Promise.all(groupPromises)
|
||||||
|
|
||||||
|
// finally returned the saved user from the db
|
||||||
|
return db.get(builtUser._id!)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.status === 409) {
|
||||||
|
throw "User exists already"
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async bulkCreate(
|
||||||
|
newUsersRequested: User[],
|
||||||
|
groups: string[]
|
||||||
|
): Promise<BulkUserCreated> {
|
||||||
|
const tenantId = getTenantId()
|
||||||
|
|
||||||
|
let usersToSave: any[] = []
|
||||||
|
let newUsers: any[] = []
|
||||||
|
|
||||||
|
const emails = newUsersRequested.map((user: User) => user.email)
|
||||||
|
const existingEmails = await searchExistingEmails(emails)
|
||||||
|
const unsuccessful: { email: string; reason: string }[] = []
|
||||||
|
|
||||||
|
for (const newUser of newUsersRequested) {
|
||||||
|
if (
|
||||||
|
newUsers.find(
|
||||||
|
(x: User) => x.email.toLowerCase() === newUser.email.toLowerCase()
|
||||||
|
) ||
|
||||||
|
existingEmails.includes(newUser.email.toLowerCase())
|
||||||
|
) {
|
||||||
|
unsuccessful.push({
|
||||||
|
email: newUser.email,
|
||||||
|
reason: `Unavailable`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
newUser.userGroups = groups
|
||||||
|
newUsers.push(newUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await accountSdk.getAccountByTenantId(tenantId)
|
||||||
|
return UserDB.quotas.addUsers(newUsers.length, async () => {
|
||||||
|
// create the promises array that will be called by bulkDocs
|
||||||
|
newUsers.forEach((user: any) => {
|
||||||
|
usersToSave.push(
|
||||||
|
UserDB.buildUser(
|
||||||
|
user,
|
||||||
|
{
|
||||||
|
hashPassword: true,
|
||||||
|
requirePassword: user.requirePassword,
|
||||||
|
},
|
||||||
|
tenantId,
|
||||||
|
undefined, // no dbUser
|
||||||
|
account
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const usersToBulkSave = await Promise.all(usersToSave)
|
||||||
|
await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
|
||||||
|
|
||||||
|
// Post-processing of bulk added users, e.g. events and cache operations
|
||||||
|
for (const user of usersToBulkSave) {
|
||||||
|
// TODO: Refactor to bulk insert users into the info db
|
||||||
|
// instead of relying on looping tenant creation
|
||||||
|
await platform.users.addUser(tenantId, user._id, user.email)
|
||||||
|
await eventHelpers.handleSaveEvents(user, undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saved = usersToBulkSave.map(user => {
|
||||||
|
return {
|
||||||
|
_id: user._id,
|
||||||
|
email: user.email,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// now update the groups
|
||||||
|
if (Array.isArray(saved) && groups) {
|
||||||
|
const groupPromises = []
|
||||||
|
const createdUserIds = saved.map(user => user._id)
|
||||||
|
for (let groupId of groups) {
|
||||||
|
groupPromises.push(UserDB.groups.addUsers(groupId, createdUserIds))
|
||||||
|
}
|
||||||
|
await Promise.all(groupPromises)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
successful: saved,
|
||||||
|
unsuccessful,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static async bulkDelete(userIds: string[]): Promise<BulkUserDeleted> {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
|
||||||
|
const response: BulkUserDeleted = {
|
||||||
|
successful: [],
|
||||||
|
unsuccessful: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the account holder from the delete request if present
|
||||||
|
const account = await getAccountHolderFromUserIds(userIds)
|
||||||
|
if (account) {
|
||||||
|
userIds = userIds.filter(u => u !== account.budibaseUserId)
|
||||||
|
// mark user as unsuccessful
|
||||||
|
response.unsuccessful.push({
|
||||||
|
_id: account.budibaseUserId,
|
||||||
|
email: account.email,
|
||||||
|
reason: "Account holder cannot be deleted",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get users and delete
|
||||||
|
const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
|
||||||
|
include_docs: true,
|
||||||
|
keys: userIds,
|
||||||
|
})
|
||||||
|
const usersToDelete: User[] = allDocsResponse.rows.map(
|
||||||
|
(user: RowResponse<User>) => {
|
||||||
|
return user.doc
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete from DB
|
||||||
|
const toDelete = usersToDelete.map(user => ({
|
||||||
|
...user,
|
||||||
|
_deleted: true,
|
||||||
|
}))
|
||||||
|
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
||||||
|
|
||||||
|
await UserDB.quotas.removeUsers(toDelete.length)
|
||||||
|
for (let user of usersToDelete) {
|
||||||
|
await bulkDeleteProcessing(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build Response
|
||||||
|
// index users by id
|
||||||
|
const userIndex: { [key: string]: User } = {}
|
||||||
|
usersToDelete.reduce((prev, current) => {
|
||||||
|
prev[current._id!] = current
|
||||||
|
return prev
|
||||||
|
}, userIndex)
|
||||||
|
|
||||||
|
// add the successful and unsuccessful users to response
|
||||||
|
dbResponse.forEach(item => {
|
||||||
|
const email = userIndex[item.id].email
|
||||||
|
if (item.ok) {
|
||||||
|
response.successful.push({ _id: item.id, email })
|
||||||
|
} else {
|
||||||
|
response.unsuccessful.push({
|
||||||
|
_id: item.id,
|
||||||
|
email,
|
||||||
|
reason: "Database error",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
static async destroy(id: string) {
|
||||||
|
const db = getGlobalDB()
|
||||||
|
const dbUser = (await db.get(id)) as User
|
||||||
|
const userId = dbUser._id as string
|
||||||
|
|
||||||
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
|
// root account holder can't be deleted from inside budibase
|
||||||
|
const email = dbUser.email
|
||||||
|
const account = await accounts.getAccount(email)
|
||||||
|
if (account) {
|
||||||
|
if (dbUser.userId === getIdentity()!._id) {
|
||||||
|
throw new HTTPError('Please visit "Account" to delete this user', 400)
|
||||||
|
} else {
|
||||||
|
throw new HTTPError("Account holder cannot be deleted", 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await platform.users.removeUser(dbUser)
|
||||||
|
|
||||||
|
await db.remove(userId, dbUser._rev)
|
||||||
|
|
||||||
|
await UserDB.quotas.removeUsers(1)
|
||||||
|
await eventHelpers.handleDeleteEvents(dbUser)
|
||||||
|
await cache.user.invalidateUser(userId)
|
||||||
|
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,18 @@
|
||||||
import env from "../../environment"
|
import env from "../environment"
|
||||||
import { events, accounts, tenancy } from "@budibase/backend-core"
|
import * as events from "../events"
|
||||||
|
import * as accounts from "../accounts"
|
||||||
|
import { getTenantId } from "../context"
|
||||||
import { User, UserRoles, CloudAccount } from "@budibase/types"
|
import { User, UserRoles, CloudAccount } from "@budibase/types"
|
||||||
|
import { hasBuilderPermissions, hasAdminPermissions } from "./utils"
|
||||||
|
|
||||||
export const handleDeleteEvents = async (user: any) => {
|
export const handleDeleteEvents = async (user: any) => {
|
||||||
await events.user.deleted(user)
|
await events.user.deleted(user)
|
||||||
|
|
||||||
if (isBuilder(user)) {
|
if (hasBuilderPermissions(user)) {
|
||||||
await events.user.permissionBuilderRemoved(user)
|
await events.user.permissionBuilderRemoved(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAdmin(user)) {
|
if (hasAdminPermissions(user)) {
|
||||||
await events.user.permissionAdminRemoved(user)
|
await events.user.permissionAdminRemoved(user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,7 +58,7 @@ export const handleSaveEvents = async (
|
||||||
user: User,
|
user: User,
|
||||||
existingUser: User | undefined
|
existingUser: User | undefined
|
||||||
) => {
|
) => {
|
||||||
const tenantId = tenancy.getTenantId()
|
const tenantId = getTenantId()
|
||||||
let tenantAccount: CloudAccount | undefined
|
let tenantAccount: CloudAccount | undefined
|
||||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
tenantAccount = await accounts.getAccountByTenantId(tenantId)
|
tenantAccount = await accounts.getAccountByTenantId(tenantId)
|
||||||
|
@ -103,23 +106,20 @@ export const handleSaveEvents = async (
|
||||||
await handleAppRoleEvents(user, existingUser)
|
await handleAppRoleEvents(user, existingUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isBuilder = (user: any) => user.builder && user.builder.global
|
|
||||||
const isAdmin = (user: any) => user.admin && user.admin.global
|
|
||||||
|
|
||||||
export const isAddingBuilder = (user: any, existingUser: any) => {
|
export const isAddingBuilder = (user: any, existingUser: any) => {
|
||||||
return isAddingPermission(user, existingUser, isBuilder)
|
return isAddingPermission(user, existingUser, hasBuilderPermissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isRemovingBuilder = (user: any, existingUser: any) => {
|
export const isRemovingBuilder = (user: any, existingUser: any) => {
|
||||||
return isRemovingPermission(user, existingUser, isBuilder)
|
return isRemovingPermission(user, existingUser, hasBuilderPermissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAddingAdmin = (user: any, existingUser: any) => {
|
const isAddingAdmin = (user: any, existingUser: any) => {
|
||||||
return isAddingPermission(user, existingUser, isAdmin)
|
return isAddingPermission(user, existingUser, hasAdminPermissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isRemovingAdmin = (user: any, existingUser: any) => {
|
const isRemovingAdmin = (user: any, existingUser: any) => {
|
||||||
return isRemovingPermission(user, existingUser, isAdmin)
|
return isRemovingPermission(user, existingUser, hasAdminPermissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOnboardingComplete = (user: any, existingUser: any) => {
|
const isOnboardingComplete = (user: any, existingUser: any) => {
|
|
@ -0,0 +1,4 @@
|
||||||
|
export * from "./users"
|
||||||
|
export * from "./utils"
|
||||||
|
export * from "./lookup"
|
||||||
|
export { UserDB } from "./db"
|
|
@ -0,0 +1,102 @@
|
||||||
|
import {
|
||||||
|
AccountMetadata,
|
||||||
|
PlatformUser,
|
||||||
|
PlatformUserByEmail,
|
||||||
|
User,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import * as dbUtils from "../db"
|
||||||
|
import { ViewName } from "../constants"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a system-wide search on emails:
|
||||||
|
* - in tenant
|
||||||
|
* - cross tenant
|
||||||
|
* - accounts
|
||||||
|
* return an array of emails that match the supplied emails.
|
||||||
|
*/
|
||||||
|
export async function searchExistingEmails(emails: string[]) {
|
||||||
|
let matchedEmails: string[] = []
|
||||||
|
|
||||||
|
const existingTenantUsers = await getExistingTenantUsers(emails)
|
||||||
|
matchedEmails.push(...existingTenantUsers.map(user => user.email))
|
||||||
|
|
||||||
|
const existingPlatformUsers = await getExistingPlatformUsers(emails)
|
||||||
|
matchedEmails.push(...existingPlatformUsers.map(user => user._id!))
|
||||||
|
|
||||||
|
const existingAccounts = await getExistingAccounts(emails)
|
||||||
|
matchedEmails.push(...existingAccounts.map(account => account.email))
|
||||||
|
|
||||||
|
return [...new Set(matchedEmails.map(email => email.toLowerCase()))]
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup, could be email or userId, either will return a doc
|
||||||
|
export async function getPlatformUser(
|
||||||
|
identifier: string
|
||||||
|
): Promise<PlatformUser | null> {
|
||||||
|
// use the view here and allow to find anyone regardless of casing
|
||||||
|
// Use lowercase to ensure email login is case insensitive
|
||||||
|
return (await dbUtils.queryPlatformView(ViewName.PLATFORM_USERS_LOWERCASE, {
|
||||||
|
keys: [identifier.toLowerCase()],
|
||||||
|
include_docs: true,
|
||||||
|
})) as PlatformUser
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getExistingTenantUsers(
|
||||||
|
emails: string[]
|
||||||
|
): Promise<User[]> {
|
||||||
|
const lcEmails = emails.map(email => email.toLowerCase())
|
||||||
|
const params = {
|
||||||
|
keys: lcEmails,
|
||||||
|
include_docs: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
arrayResponse: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await dbUtils.queryGlobalView(
|
||||||
|
ViewName.USER_BY_EMAIL,
|
||||||
|
params,
|
||||||
|
undefined,
|
||||||
|
opts
|
||||||
|
)) as User[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getExistingPlatformUsers(
|
||||||
|
emails: string[]
|
||||||
|
): Promise<PlatformUserByEmail[]> {
|
||||||
|
const lcEmails = emails.map(email => email.toLowerCase())
|
||||||
|
const params = {
|
||||||
|
keys: lcEmails,
|
||||||
|
include_docs: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
arrayResponse: true,
|
||||||
|
}
|
||||||
|
return (await dbUtils.queryPlatformView(
|
||||||
|
ViewName.PLATFORM_USERS_LOWERCASE,
|
||||||
|
params,
|
||||||
|
opts
|
||||||
|
)) as PlatformUserByEmail[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getExistingAccounts(
|
||||||
|
emails: string[]
|
||||||
|
): Promise<AccountMetadata[]> {
|
||||||
|
const lcEmails = emails.map(email => email.toLowerCase())
|
||||||
|
const params = {
|
||||||
|
keys: lcEmails,
|
||||||
|
include_docs: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
arrayResponse: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await dbUtils.queryPlatformView(
|
||||||
|
ViewName.ACCOUNT_BY_EMAIL,
|
||||||
|
params,
|
||||||
|
opts
|
||||||
|
)) as AccountMetadata[]
|
||||||
|
}
|
|
@ -11,10 +11,16 @@ import {
|
||||||
SEPARATOR,
|
SEPARATOR,
|
||||||
UNICODE_MAX,
|
UNICODE_MAX,
|
||||||
ViewName,
|
ViewName,
|
||||||
} from "./db"
|
} from "../db"
|
||||||
import { BulkDocsResponse, SearchUsersRequest, User } from "@budibase/types"
|
import {
|
||||||
import { getGlobalDB } from "./context"
|
BulkDocsResponse,
|
||||||
import * as context from "./context"
|
SearchUsersRequest,
|
||||||
|
User,
|
||||||
|
ContextUser,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { getGlobalDB } from "../context"
|
||||||
|
import * as context from "../context"
|
||||||
|
import { user as userCache } from "../cache"
|
||||||
|
|
||||||
type GetOpts = { cleanup?: boolean }
|
type GetOpts = { cleanup?: boolean }
|
||||||
|
|
||||||
|
@ -178,7 +184,7 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
||||||
* Performs a starts with search on the global email view.
|
* Performs a starts with search on the global email view.
|
||||||
*/
|
*/
|
||||||
export const searchGlobalUsersByEmail = async (
|
export const searchGlobalUsersByEmail = async (
|
||||||
email: string,
|
email: string | unknown,
|
||||||
opts: any,
|
opts: any,
|
||||||
getOpts?: GetOpts
|
getOpts?: GetOpts
|
||||||
) => {
|
) => {
|
||||||
|
@ -248,3 +254,23 @@ export async function getUserCount() {
|
||||||
})
|
})
|
||||||
return response.total_rows
|
return response.total_rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// used to remove the builder/admin permissions, for processing the
|
||||||
|
// user as an app user (they may have some specific role/group
|
||||||
|
export function removePortalUserPermissions(user: User | ContextUser) {
|
||||||
|
delete user.admin
|
||||||
|
delete user.builder
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanseUserObject(user: User | ContextUser, base?: User) {
|
||||||
|
delete user.admin
|
||||||
|
delete user.builder
|
||||||
|
delete user.roles
|
||||||
|
if (base) {
|
||||||
|
user.admin = base.admin
|
||||||
|
user.builder = base.builder
|
||||||
|
user.roles = base.roles
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { CloudAccount } from "@budibase/types"
|
||||||
|
import * as accountSdk from "../accounts"
|
||||||
|
import env from "../environment"
|
||||||
|
import { getPlatformUser } from "./lookup"
|
||||||
|
import { EmailUnavailableError } from "../errors"
|
||||||
|
import { getTenantId } from "../context"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
import { getAccountByTenantId } from "../accounts"
|
||||||
|
|
||||||
|
// extract from shared-core to make easily accessible from backend-core
|
||||||
|
export const isBuilder = sdk.users.isBuilder
|
||||||
|
export const isAdmin = sdk.users.isAdmin
|
||||||
|
export const isGlobalBuilder = sdk.users.isGlobalBuilder
|
||||||
|
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
|
||||||
|
export const hasAdminPermissions = sdk.users.hasAdminPermissions
|
||||||
|
export const hasBuilderPermissions = sdk.users.hasBuilderPermissions
|
||||||
|
export const hasAppBuilderPermissions = sdk.users.hasAppBuilderPermissions
|
||||||
|
|
||||||
|
export async function validateUniqueUser(email: string, tenantId: string) {
|
||||||
|
// check budibase users in other tenants
|
||||||
|
if (env.MULTI_TENANCY) {
|
||||||
|
const tenantUser = await getPlatformUser(email)
|
||||||
|
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
|
||||||
|
throw new EmailUnavailableError(email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check root account users in account portal
|
||||||
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
|
const account = await accountSdk.getAccount(email)
|
||||||
|
if (account && account.verified && account.tenantId !== tenantId) {
|
||||||
|
throw new EmailUnavailableError(email)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For the given user id's, return the account holder if it is in the ids.
|
||||||
|
*/
|
||||||
|
export async function getAccountHolderFromUserIds(
|
||||||
|
userIds: string[]
|
||||||
|
): Promise<CloudAccount | undefined> {
|
||||||
|
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
||||||
|
const tenantId = getTenantId()
|
||||||
|
const account = await getAccountByTenantId(tenantId)
|
||||||
|
if (!account) {
|
||||||
|
throw new Error(`Account not found for tenantId=${tenantId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const budibaseUserId = account.budibaseUserId
|
||||||
|
if (userIds.includes(budibaseUserId)) {
|
||||||
|
return account
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -94,6 +94,10 @@ export const useSyncAutomations = () => {
|
||||||
return useFeature(Feature.SYNC_AUTOMATIONS)
|
return useFeature(Feature.SYNC_AUTOMATIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useAppBuilders = () => {
|
||||||
|
return useFeature(Feature.APP_BUILDERS)
|
||||||
|
}
|
||||||
|
|
||||||
// QUOTAS
|
// QUOTAS
|
||||||
|
|
||||||
export const setAutomationLogsQuota = (value: number) => {
|
export const setAutomationLogsQuota = (value: number) => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import {
|
import {
|
||||||
AdminUser,
|
AdminUser,
|
||||||
|
AdminOnlyUser,
|
||||||
BuilderUser,
|
BuilderUser,
|
||||||
SSOAuthDetails,
|
SSOAuthDetails,
|
||||||
SSOUser,
|
SSOUser,
|
||||||
|
@ -21,6 +22,15 @@ export const adminUser = (userProps?: any): AdminUser => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const adminOnlyUser = (userProps?: any): AdminOnlyUser => {
|
||||||
|
return {
|
||||||
|
...user(userProps),
|
||||||
|
admin: {
|
||||||
|
global: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const builderUser = (userProps?: any): BuilderUser => {
|
export const builderUser = (userProps?: any): BuilderUser => {
|
||||||
return {
|
return {
|
||||||
...user(userProps),
|
...user(userProps),
|
||||||
|
@ -30,6 +40,15 @@ export const builderUser = (userProps?: any): BuilderUser => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const appBuilderUser = (appId: string, userProps?: any): BuilderUser => {
|
||||||
|
return {
|
||||||
|
...user(userProps),
|
||||||
|
builder: {
|
||||||
|
apps: [appId],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function ssoUser(
|
export function ssoUser(
|
||||||
opts: { user?: any; details?: SSOAuthDetails } = {}
|
opts: { user?: any; details?: SSOAuthDetails } = {}
|
||||||
): SSOUser {
|
): SSOUser {
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
<script>
|
<script>
|
||||||
import { Heading, Body, Button, Icon } from "@budibase/bbui"
|
import { Heading, Body, Button, Icon } from "@budibase/bbui"
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { UserAvatars } from "@budibase/frontend-core"
|
import { UserAvatars } from "@budibase/frontend-core"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let lockedAction
|
export let lockedAction
|
||||||
|
|
||||||
$: editing = app.sessions?.length
|
$: editing = app.sessions?.length
|
||||||
|
$: isBuilder = sdk.users.isBuilder($auth.user, app?.devId)
|
||||||
|
|
||||||
const handleDefaultClick = () => {
|
const handleDefaultClick = () => {
|
||||||
if (window.innerWidth < 640) {
|
if (!isBuilder) {
|
||||||
|
goToApp()
|
||||||
|
} else if (window.innerWidth < 640) {
|
||||||
goToOverview()
|
goToOverview()
|
||||||
} else {
|
} else {
|
||||||
goToBuilder()
|
goToBuilder()
|
||||||
|
@ -24,6 +29,10 @@
|
||||||
const goToOverview = () => {
|
const goToOverview = () => {
|
||||||
$goto(`../../app/${app.devId}/settings`)
|
$goto(`../../app/${app.devId}/settings`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goToApp = () => {
|
||||||
|
window.open(`/app/${app.name}`, "_blank")
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="app-row" on:click={lockedAction || handleDefaultClick}>
|
<div class="app-row" on:click={lockedAction || handleDefaultClick}>
|
||||||
|
@ -39,7 +48,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="updated">
|
<div class="updated">
|
||||||
{#if editing}
|
{#if editing && isBuilder}
|
||||||
Currently editing
|
Currently editing
|
||||||
<UserAvatars users={app.sessions} />
|
<UserAvatars users={app.sessions} />
|
||||||
{:else if app.updatedAt}
|
{:else if app.updatedAt}
|
||||||
|
@ -56,14 +65,21 @@
|
||||||
<Body size="S">{app.deployed ? "Published" : "Unpublished"}</Body>
|
<Body size="S">{app.deployed ? "Published" : "Unpublished"}</Body>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="app-row-actions">
|
{#if isBuilder}
|
||||||
<Button size="S" secondary on:click={lockedAction || goToOverview}>
|
<div class="app-row-actions">
|
||||||
Manage
|
<Button size="S" secondary on:click={lockedAction || goToOverview}>
|
||||||
</Button>
|
Manage
|
||||||
<Button size="S" primary on:click={lockedAction || goToBuilder}>
|
</Button>
|
||||||
Edit
|
<Button size="S" primary on:click={lockedAction || goToBuilder}>
|
||||||
</Button>
|
Edit
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- this can happen if an app builder has app user access to an app -->
|
||||||
|
<div class="app-row-actions">
|
||||||
|
<Button size="S" secondary>View</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -12,12 +12,12 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { groups, licensing, apps, users, auth, admin } from "stores/portal"
|
import { groups, licensing, apps, users, auth, admin } from "stores/portal"
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
import { fetchData, Constants, Utils } from "@budibase/frontend-core"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import GroupIcon from "../../../portal/users/groups/_components/GroupIcon.svelte"
|
import GroupIcon from "../../../portal/users/groups/_components/GroupIcon.svelte"
|
||||||
import RoleSelect from "components/common/RoleSelect.svelte"
|
import RoleSelect from "components/common/RoleSelect.svelte"
|
||||||
import UpgradeModal from "components/common/users/UpgradeModal.svelte"
|
import UpgradeModal from "components/common/users/UpgradeModal.svelte"
|
||||||
import { Constants, Utils } from "@budibase/frontend-core"
|
|
||||||
import { emailValidator } from "helpers/validation"
|
import { emailValidator } from "helpers/validation"
|
||||||
import { roles } from "stores/backend"
|
import { roles } from "stores/backend"
|
||||||
import { fly } from "svelte/transition"
|
import { fly } from "svelte/transition"
|
||||||
|
@ -108,9 +108,9 @@
|
||||||
await usersFetch.refresh()
|
await usersFetch.refresh()
|
||||||
|
|
||||||
filteredUsers = $usersFetch.rows.map(user => {
|
filteredUsers = $usersFetch.rows.map(user => {
|
||||||
const isBuilderOrAdmin = user.admin?.global || user.builder?.global
|
const isAdminOrBuilder = sdk.users.isAdminOrBuilder(user, prodAppId)
|
||||||
let role = undefined
|
let role = undefined
|
||||||
if (isBuilderOrAdmin) {
|
if (isAdminOrBuilder) {
|
||||||
role = Constants.Roles.ADMIN
|
role = Constants.Roles.ADMIN
|
||||||
} else {
|
} else {
|
||||||
const appRole = Object.keys(user.roles).find(x => x === prodAppId)
|
const appRole = Object.keys(user.roles).find(x => x === prodAppId)
|
||||||
|
@ -122,7 +122,7 @@
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
role,
|
role,
|
||||||
isBuilderOrAdmin,
|
isAdminOrBuilder,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -258,7 +258,7 @@
|
||||||
}
|
}
|
||||||
// Must exclude users who have explicit privileges
|
// Must exclude users who have explicit privileges
|
||||||
const userByEmail = filteredUsers.reduce((acc, user) => {
|
const userByEmail = filteredUsers.reduce((acc, user) => {
|
||||||
if (user.role || user.admin?.global || user.builder?.global) {
|
if (user.role || sdk.users.isAdminOrBuilder(user, prodAppId)) {
|
||||||
acc.push(user.email)
|
acc.push(user.email)
|
||||||
}
|
}
|
||||||
return acc
|
return acc
|
||||||
|
@ -389,9 +389,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const userTitle = user => {
|
const userTitle = user => {
|
||||||
if (user.admin?.global) {
|
if (sdk.users.isAdmin(user)) {
|
||||||
return "Admin"
|
return "Admin"
|
||||||
} else if (user.builder?.global) {
|
} else if (sdk.users.isBuilder(user, prodAppId)) {
|
||||||
return "Developer"
|
return "Developer"
|
||||||
} else {
|
} else {
|
||||||
return "App user"
|
return "App user"
|
||||||
|
@ -403,7 +403,7 @@
|
||||||
const role = $roles.find(role => role._id === user.role)
|
const role = $roles.find(role => role._id === user.role)
|
||||||
return `This user has been given ${role?.name} access from the ${user.group} group`
|
return `This user has been given ${role?.name} access from the ${user.group} group`
|
||||||
}
|
}
|
||||||
if (user.isBuilderOrAdmin) {
|
if (user.isAdminOrBuilder) {
|
||||||
return "This user's role grants admin access to all apps"
|
return "This user's role grants admin access to all apps"
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
@ -614,7 +614,7 @@
|
||||||
}}
|
}}
|
||||||
autoWidth
|
autoWidth
|
||||||
align="right"
|
align="right"
|
||||||
allowedRoles={user.isBuilderOrAdmin
|
allowedRoles={user.isAdminOrBuilder
|
||||||
? [Constants.Roles.ADMIN]
|
? [Constants.Roles.ADMIN]
|
||||||
: null}
|
: null}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
import { url, isActive } from "@roxi/routify"
|
import { url, isActive } from "@roxi/routify"
|
||||||
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
||||||
import { isOnlyUser } from "builderStore"
|
import { isOnlyUser } from "builderStore"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
let deleteModal
|
let deleteModal
|
||||||
</script>
|
</script>
|
||||||
|
@ -44,22 +46,24 @@
|
||||||
url={$url("./version")}
|
url={$url("./version")}
|
||||||
active={$isActive("./version")}
|
active={$isActive("./version")}
|
||||||
/>
|
/>
|
||||||
<div class="delete-action">
|
{#if sdk.users.isGlobalBuilder($auth.user)}
|
||||||
<AbsTooltip
|
<div class="delete-action">
|
||||||
position={TooltipPosition.Bottom}
|
<AbsTooltip
|
||||||
text={$isOnlyUser
|
position={TooltipPosition.Bottom}
|
||||||
? null
|
text={$isOnlyUser
|
||||||
: "Unavailable - another user is editing this app"}
|
? null
|
||||||
>
|
: "Unavailable - another user is editing this app"}
|
||||||
<SideNavItem
|
>
|
||||||
text="Delete app"
|
<SideNavItem
|
||||||
disabled={!$isOnlyUser}
|
text="Delete app"
|
||||||
on:click={() => {
|
disabled={!$isOnlyUser}
|
||||||
deleteModal.show()
|
on:click={() => {
|
||||||
}}
|
deleteModal.show()
|
||||||
/>
|
}}
|
||||||
</AbsTooltip>
|
/>
|
||||||
</div>
|
</AbsTooltip>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</SideNav>
|
</SideNav>
|
||||||
<slot />
|
<slot />
|
||||||
</Content>
|
</Content>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
import Spaceman from "assets/bb-space-man.svg"
|
import Spaceman from "assets/bb-space-man.svg"
|
||||||
import Logo from "assets/bb-emblem.svg"
|
import Logo from "assets/bb-emblem.svg"
|
||||||
import { UserAvatar } from "@budibase/frontend-core"
|
import { UserAvatar } from "@budibase/frontend-core"
|
||||||
import { helpers } from "@budibase/shared-core"
|
import { helpers, sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let userInfoModal
|
let userInfoModal
|
||||||
|
@ -43,32 +43,30 @@
|
||||||
$: userGroups = $groups.filter(group =>
|
$: userGroups = $groups.filter(group =>
|
||||||
group.users.find(user => user._id === $auth.user?._id)
|
group.users.find(user => user._id === $auth.user?._id)
|
||||||
)
|
)
|
||||||
let userApps = []
|
|
||||||
$: publishedApps = $apps.filter(publishedAppsOnly)
|
$: publishedApps = $apps.filter(publishedAppsOnly)
|
||||||
|
$: userApps = getUserApps($auth.user)
|
||||||
|
|
||||||
$: {
|
function getUserApps(user) {
|
||||||
if (!Object.keys($auth.user?.roles).length && $auth.user?.userGroups) {
|
if (sdk.users.isAdmin(user)) {
|
||||||
userApps =
|
return publishedApps
|
||||||
$auth.user?.builder?.global || $auth.user?.admin?.global
|
|
||||||
? publishedApps
|
|
||||||
: publishedApps.filter(app => {
|
|
||||||
return userGroups.find(group => {
|
|
||||||
return groups.actions
|
|
||||||
.getGroupAppIds(group)
|
|
||||||
.map(role => apps.extractAppId(role))
|
|
||||||
.includes(app.appId)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
userApps =
|
|
||||||
$auth.user?.builder?.global || $auth.user?.admin?.global
|
|
||||||
? publishedApps
|
|
||||||
: publishedApps.filter(app =>
|
|
||||||
Object.keys($auth.user?.roles)
|
|
||||||
.map(x => apps.extractAppId(x))
|
|
||||||
.includes(app.appId)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return publishedApps.filter(app => {
|
||||||
|
if (sdk.users.isBuilder(user, app.appId)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!Object.keys(user?.roles).length && user?.userGroups) {
|
||||||
|
return userGroups.find(group => {
|
||||||
|
return groups.actions
|
||||||
|
.getGroupAppIds(group)
|
||||||
|
.map(role => apps.extractAppId(role))
|
||||||
|
.includes(app.appId)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return Object.keys($auth.user?.roles)
|
||||||
|
.map(x => apps.extractAppId(x))
|
||||||
|
.includes(app.appId)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUrl(app) {
|
function getUrl(app) {
|
||||||
|
@ -109,7 +107,7 @@
|
||||||
>
|
>
|
||||||
Update password
|
Update password
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{#if $auth.isBuilder}
|
{#if sdk.users.hasBuilderPermissions($auth.user)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="UserDeveloper"
|
icon="UserDeveloper"
|
||||||
on:click={() => $goto("../portal")}
|
on:click={() => $goto("../portal")}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
auth.checkQueryString()
|
auth.checkQueryString()
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if ($auth.user?.builder?.global) {
|
if (sdk.users.hasBuilderPermissions($auth.user)) {
|
||||||
$redirect(`./portal`)
|
$redirect(`./portal`)
|
||||||
} else if ($auth.user) {
|
} else if ($auth.user) {
|
||||||
$redirect(`./apps`)
|
$redirect(`./apps`)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { auth, admin, licensing } from "stores/portal"
|
import { auth, admin, licensing } from "stores/portal"
|
||||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING) && !$licensing.isEnterprisePlan}
|
{#if isEnabled(TENANT_FEATURE_FLAGS.LICENSING) && !$licensing.isEnterprisePlan}
|
||||||
|
@ -17,7 +18,7 @@
|
||||||
>
|
>
|
||||||
Upgrade
|
Upgrade
|
||||||
</Button>
|
</Button>
|
||||||
{:else if !$admin.cloud && $auth.isAdmin}
|
{:else if !$admin.cloud && sdk.users.isAdmin($auth.user)}
|
||||||
<Button
|
<Button
|
||||||
cta
|
cta
|
||||||
size="S"
|
size="S"
|
||||||
|
|
|
@ -8,13 +8,14 @@
|
||||||
import Logo from "./_components/Logo.svelte"
|
import Logo from "./_components/Logo.svelte"
|
||||||
import UserDropdown from "./_components/UserDropdown.svelte"
|
import UserDropdown from "./_components/UserDropdown.svelte"
|
||||||
import HelpMenu from "components/common/HelpMenu.svelte"
|
import HelpMenu from "components/common/HelpMenu.svelte"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
let loaded = false
|
let loaded = false
|
||||||
let mobileMenuVisible = false
|
let mobileMenuVisible = false
|
||||||
let activeTab = "Apps"
|
let activeTab = "Apps"
|
||||||
|
|
||||||
$: $url(), updateActiveTab($menu)
|
$: $url(), updateActiveTab($menu)
|
||||||
$: fullscreen = !$apps.length
|
$: isOnboarding = !$apps.length && sdk.users.isGlobalBuilder($auth.user)
|
||||||
|
|
||||||
const updateActiveTab = menu => {
|
const updateActiveTab = menu => {
|
||||||
for (let entry of menu) {
|
for (let entry of menu) {
|
||||||
|
@ -33,7 +34,7 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Prevent non-builders from accessing the portal
|
// Prevent non-builders from accessing the portal
|
||||||
if ($auth.user) {
|
if ($auth.user) {
|
||||||
if (!$auth.user?.builder?.global) {
|
if (!sdk.users.hasBuilderPermissions($auth.user)) {
|
||||||
$redirect("../")
|
$redirect("../")
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
@ -49,7 +50,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $auth.user && loaded}
|
{#if $auth.user && loaded}
|
||||||
{#if fullscreen}
|
{#if isOnboarding}
|
||||||
<slot />
|
<slot />
|
||||||
{:else}
|
{:else}
|
||||||
<HelpMenu />
|
<HelpMenu />
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
import DeleteLicenseKeyModal from "../../../../components/portal/licensing/DeleteLicenseKeyModal.svelte"
|
import DeleteLicenseKeyModal from "../../../../components/portal/licensing/DeleteLicenseKeyModal.svelte"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
$: license = $auth.user.license
|
$: license = $auth.user.license
|
||||||
$: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
$: upgradeUrl = `${$admin.accountPortalUrl}/portal/upgrade`
|
||||||
|
@ -176,7 +177,7 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $auth.isAdmin}
|
{#if sdk.users.isAdmin($auth.user)}
|
||||||
<DeleteLicenseKeyModal
|
<DeleteLicenseKeyModal
|
||||||
bind:this={deleteLicenseKeyModal}
|
bind:this={deleteLicenseKeyModal}
|
||||||
onConfirm={deleteLicenseKey}
|
onConfirm={deleteLicenseKey}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import { DashCard, Usage } from "components/usage"
|
import { DashCard, Usage } from "components/usage"
|
||||||
import { PlanModel } from "constants"
|
import { PlanModel } from "constants"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
let staticUsage = []
|
let staticUsage = []
|
||||||
let monthlyUsage = []
|
let monthlyUsage = []
|
||||||
|
@ -51,7 +52,8 @@
|
||||||
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
$: accountPortalAccess = $auth?.user?.accountPortalAccess
|
||||||
$: quotaReset = quotaUsage?.quotaReset
|
$: quotaReset = quotaUsage?.quotaReset
|
||||||
$: canManagePlan =
|
$: canManagePlan =
|
||||||
($admin.cloud && accountPortalAccess) || (!$admin.cloud && $auth.isAdmin)
|
($admin.cloud && accountPortalAccess) ||
|
||||||
|
(!$admin.cloud && sdk.users.isAdmin($auth.user))
|
||||||
|
|
||||||
$: showButton = !usesInvoicing && accountPortalAccess
|
$: showButton = !usesInvoicing && accountPortalAccess
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
<script>
|
<script>
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import { admin, apps, templates, licensing, groups } from "stores/portal"
|
import {
|
||||||
|
admin,
|
||||||
|
apps,
|
||||||
|
templates,
|
||||||
|
licensing,
|
||||||
|
groups,
|
||||||
|
auth,
|
||||||
|
} from "stores/portal"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
// Don't block loading if we've already hydrated state
|
// Don't block loading if we've already hydrated state
|
||||||
let loaded = $apps.length > 0
|
let loaded = $apps.length != null
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -25,7 +33,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go to new app page if no apps exists
|
// Go to new app page if no apps exists
|
||||||
if (!$apps.length) {
|
if (!$apps.length && sdk.users.isGlobalBuilder($auth.user)) {
|
||||||
$redirect("./onboarding")
|
$redirect("./onboarding")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||||
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
|
||||||
import AccountLockedModal from "components/portal/licensing/AccountLockedModal.svelte"
|
import AccountLockedModal from "components/portal/licensing/AccountLockedModal.svelte"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
import { store, automationStore } from "builderStore"
|
import { store, automationStore } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
@ -203,40 +204,40 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $apps.length}
|
<Page>
|
||||||
<Page>
|
<Layout noPadding gap="L">
|
||||||
<Layout noPadding gap="L">
|
{#each Object.keys(automationErrors || {}) as appId}
|
||||||
{#each Object.keys(automationErrors || {}) as appId}
|
<Notification
|
||||||
<Notification
|
wide
|
||||||
wide
|
dismissable
|
||||||
dismissable
|
action={() => goToAutomationError(appId)}
|
||||||
action={() => goToAutomationError(appId)}
|
type="error"
|
||||||
type="error"
|
icon="Alert"
|
||||||
icon="Alert"
|
actionMessage={errorCount(automationErrors[appId]) > 1
|
||||||
actionMessage={errorCount(automationErrors[appId]) > 1
|
? "View errors"
|
||||||
? "View errors"
|
: "View error"}
|
||||||
: "View error"}
|
on:dismiss={async () => {
|
||||||
on:dismiss={async () => {
|
await automationStore.actions.clearLogErrors({ appId })
|
||||||
await automationStore.actions.clearLogErrors({ appId })
|
await apps.load()
|
||||||
await apps.load()
|
}}
|
||||||
}}
|
message={automationErrorMessage(appId)}
|
||||||
message={automationErrorMessage(appId)}
|
/>
|
||||||
/>
|
{/each}
|
||||||
{/each}
|
<div class="title">
|
||||||
<div class="title">
|
<div class="welcome">
|
||||||
<div class="welcome">
|
<Layout noPadding gap="XS">
|
||||||
<Layout noPadding gap="XS">
|
<Heading size="L">{welcomeHeader}</Heading>
|
||||||
<Heading size="L">{welcomeHeader}</Heading>
|
<Body size="M">
|
||||||
<Body size="M">
|
Below you'll find the list of apps that you have access to
|
||||||
Manage your apps and get a head start with templates
|
</Body>
|
||||||
</Body>
|
</Layout>
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if enrichedApps.length}
|
{#if enrichedApps.length}
|
||||||
<Layout noPadding gap="L">
|
<Layout noPadding gap="L">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
{#if $auth.user && sdk.users.isGlobalBuilder($auth.user)}
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<Button
|
<Button
|
||||||
size="M"
|
size="M"
|
||||||
|
@ -266,41 +267,46 @@
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if enrichedApps.length > 1}
|
{/if}
|
||||||
<div class="app-actions">
|
{#if enrichedApps.length > 1}
|
||||||
<Select
|
<div class="app-actions">
|
||||||
autoWidth
|
<Select
|
||||||
bind:value={sortBy}
|
autoWidth
|
||||||
placeholder={null}
|
bind:value={sortBy}
|
||||||
options={[
|
placeholder={null}
|
||||||
{ label: "Sort by name", value: "name" },
|
options={[
|
||||||
{ label: "Sort by recently updated", value: "updated" },
|
{ label: "Sort by name", value: "name" },
|
||||||
{ label: "Sort by status", value: "status" },
|
{ label: "Sort by recently updated", value: "updated" },
|
||||||
]}
|
{ label: "Sort by status", value: "status" },
|
||||||
/>
|
]}
|
||||||
<Search placeholder="Search" bind:value={searchTerm} />
|
/>
|
||||||
</div>
|
<Search placeholder="Search" bind:value={searchTerm} />
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<div class="app-table">
|
|
||||||
{#each filteredApps as app (app.appId)}
|
|
||||||
<AppRow {app} lockedAction={usersLimitLockAction} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if creatingFromTemplate}
|
|
||||||
<div class="empty-wrapper">
|
|
||||||
<img class="img-logo img-size" alt="logo" src={Logo} />
|
|
||||||
<p>Creating your Budibase app from your selected template...</p>
|
|
||||||
<Spinner size="10" />
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</Layout>
|
<div class="app-table">
|
||||||
</Page>
|
{#each filteredApps as app (app.appId)}
|
||||||
{/if}
|
<AppRow {app} lockedAction={usersLimitLockAction} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{:else}
|
||||||
|
<div class="no-apps">
|
||||||
|
<img class="spaceman" alt="spaceman" src={Logo} width="100px" />
|
||||||
|
<Body weight="700">You haven't been given access to any apps yet</Body>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if creatingFromTemplate}
|
||||||
|
<div class="empty-wrapper">
|
||||||
|
<img class="img-logo img-size" alt="logo" src={Logo} />
|
||||||
|
<p>Creating your Budibase app from your selected template...</p>
|
||||||
|
<Spinner size="10" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
bind:this={creationModal}
|
bind:this={creationModal}
|
||||||
|
@ -368,6 +374,16 @@
|
||||||
height: 160px;
|
height: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-apps {
|
||||||
|
background-color: var(--spectrum-global-color-gray-100);
|
||||||
|
padding: calc(var(--spacing-xl) * 2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
.img-logo {
|
.img-logo {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
const imageExtensions = [
|
const imageExtensions = [
|
||||||
".png",
|
".png",
|
||||||
|
@ -206,7 +207,7 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $auth.isAdmin && mounted}
|
{#if sdk.users.isAdmin($auth.user) && mounted}
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
@ -400,7 +401,7 @@
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (isCloud && $auth?.user?.accountPortalAccess) {
|
if (isCloud && $auth?.user?.accountPortalAccess) {
|
||||||
window.open($admin.accountPortalUrl + "/portal/upgrade", "_blank")
|
window.open($admin.accountPortalUrl + "/portal/upgrade", "_blank")
|
||||||
} else if ($auth.isAdmin) {
|
} else if (sdk.users.isAdmin($auth.user)) {
|
||||||
$goto("/builder/portal/account/upgrade")
|
$goto("/builder/portal/account/upgrade")
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
let diagnosticInfo = ""
|
let diagnosticInfo = ""
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $auth.isAdmin && diagnosticInfo}
|
{#if sdk.users.isAdmin($auth.user) && diagnosticInfo}
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<Layout gap="XS">
|
<Layout gap="XS">
|
||||||
<Heading size="M">Diagnostics</Heading>
|
<Heading size="M">Diagnostics</Heading>
|
||||||
|
|
|
@ -13,10 +13,11 @@
|
||||||
import { auth, organisation, admin } from "stores/portal"
|
import { auth, organisation, admin } from "stores/portal"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
// Only admins allowed here
|
// Only admins allowed here
|
||||||
$: {
|
$: {
|
||||||
if (!$auth.isAdmin) {
|
if (!sdk.users.isAdmin($auth.user)) {
|
||||||
$redirect("../../portal")
|
$redirect("../../portal")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +51,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $auth.isAdmin}
|
{#if sdk.users.isAdmin($auth.user)}
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="M">Organisation</Heading>
|
<Heading size="M">Organisation</Heading>
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { auth, admin } from "stores/portal"
|
import { auth, admin } from "stores/portal"
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
let version
|
let version
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
@ -25,7 +26,7 @@
|
||||||
|
|
||||||
// Only admins allowed here
|
// Only admins allowed here
|
||||||
$: {
|
$: {
|
||||||
if (!$auth.isAdmin || $admin.cloud) {
|
if (!sdk.users.isAdmin($auth.user) || $admin.cloud) {
|
||||||
$redirect("../../portal")
|
$redirect("../../portal")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,7 +90,7 @@
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $auth.isAdmin}
|
{#if sdk.users.isAdmin($auth.user)}
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="M">Version</Heading>
|
<Heading size="M">Version</Heading>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
|
import CreateEditGroupModal from "./_components/CreateEditGroupModal.svelte"
|
||||||
import GroupIcon from "./_components/GroupIcon.svelte"
|
import GroupIcon from "./_components/GroupIcon.svelte"
|
||||||
import GroupUsers from "./_components/GroupUsers.svelte"
|
import GroupUsers from "./_components/GroupUsers.svelte"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let groupId
|
export let groupId
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@
|
||||||
let editModal, deleteModal
|
let editModal, deleteModal
|
||||||
|
|
||||||
$: scimEnabled = $features.isScimEnabled
|
$: scimEnabled = $features.isScimEnabled
|
||||||
$: readonly = !$auth.isAdmin || scimEnabled
|
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
||||||
$: group = $groups.find(x => x._id === groupId)
|
$: group = $groups.find(x => x._id === groupId)
|
||||||
$: groupApps = $apps
|
$: groupApps = $apps
|
||||||
.filter(app =>
|
.filter(app =>
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
import UserGroupPicker from "components/settings/UserGroupPicker.svelte"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
import { auth, groups, users } from "stores/portal"
|
import { auth, groups, users } from "stores/portal"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let groupId
|
export let groupId
|
||||||
export let onUsersUpdated
|
export let onUsersUpdated
|
||||||
|
@ -13,7 +14,7 @@
|
||||||
let prevSearch = undefined
|
let prevSearch = undefined
|
||||||
let pageInfo = createPaginationStore()
|
let pageInfo = createPaginationStore()
|
||||||
|
|
||||||
$: readonly = !$auth.isAdmin
|
$: readonly = !sdk.users.isAdmin($auth.user)
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: searchUsers(page, searchTerm)
|
$: searchUsers(page, searchTerm)
|
||||||
$: group = $groups.find(x => x._id === groupId)
|
$: group = $groups.find(x => x._id === groupId)
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import { setContext } from "svelte"
|
import { setContext } from "svelte"
|
||||||
import ScimBanner from "../../_components/SCIMBanner.svelte"
|
import ScimBanner from "../../_components/SCIMBanner.svelte"
|
||||||
import RemoveUserTableRenderer from "../_components/RemoveUserTableRenderer.svelte"
|
import RemoveUserTableRenderer from "../_components/RemoveUserTableRenderer.svelte"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let groupId
|
export let groupId
|
||||||
|
|
||||||
|
@ -49,7 +50,7 @@
|
||||||
]
|
]
|
||||||
|
|
||||||
$: scimEnabled = $features.isScimEnabled
|
$: scimEnabled = $features.isScimEnabled
|
||||||
$: readonly = !$auth.isAdmin || scimEnabled
|
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
||||||
|
|
||||||
const removeUser = async id => {
|
const removeUser = async id => {
|
||||||
await groups.actions.removeUser(groupId, id)
|
await groups.actions.removeUser(groupId, id)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { ActionButton } from "@budibase/bbui"
|
import { ActionButton } from "@budibase/bbui"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
|
@ -13,6 +14,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton disabled={!$auth.isAdmin} size="S" on:click={onClick}>
|
<ActionButton
|
||||||
|
disabled={!sdk.users.isAdmin($auth.user)}
|
||||||
|
size="S"
|
||||||
|
on:click={onClick}
|
||||||
|
>
|
||||||
Remove
|
Remove
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
import GroupNameTableRenderer from "./_components/GroupNameTableRenderer.svelte"
|
import GroupNameTableRenderer from "./_components/GroupNameTableRenderer.svelte"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import ScimBanner from "../_components/SCIMBanner.svelte"
|
import ScimBanner from "../_components/SCIMBanner.svelte"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
const DefaultGroup = {
|
const DefaultGroup = {
|
||||||
name: "",
|
name: "",
|
||||||
|
@ -40,7 +41,7 @@
|
||||||
{ column: "roles", component: GroupAppsTableRenderer },
|
{ column: "roles", component: GroupAppsTableRenderer },
|
||||||
]
|
]
|
||||||
|
|
||||||
$: readonly = !$auth.isAdmin
|
$: readonly = !sdk.users.isAdmin($auth.user)
|
||||||
$: schema = {
|
$: schema = {
|
||||||
name: { displayName: "Group", width: "2fr", minWidth: "200px" },
|
name: { displayName: "Group", width: "2fr", minWidth: "200px" },
|
||||||
users: { sortable: false, width: "1fr" },
|
users: { sortable: false, width: "1fr" },
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
import AppNameTableRenderer from "./_components/AppNameTableRenderer.svelte"
|
import AppNameTableRenderer from "./_components/AppNameTableRenderer.svelte"
|
||||||
import AppRoleTableRenderer from "./_components/AppRoleTableRenderer.svelte"
|
import AppRoleTableRenderer from "./_components/AppRoleTableRenderer.svelte"
|
||||||
import ScimBanner from "../_components/SCIMBanner.svelte"
|
import ScimBanner from "../_components/SCIMBanner.svelte"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let userId
|
export let userId
|
||||||
|
|
||||||
|
@ -87,8 +88,8 @@
|
||||||
|
|
||||||
$: scimEnabled = $features.isScimEnabled
|
$: scimEnabled = $features.isScimEnabled
|
||||||
$: isSSO = !!user?.provider
|
$: isSSO = !!user?.provider
|
||||||
$: readonly = !$auth.isAdmin || scimEnabled
|
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
||||||
$: privileged = user?.admin?.global || user?.builder?.global
|
$: privileged = sdk.users.isAdminOrBuilder(user)
|
||||||
$: nameLabel = getNameLabel(user)
|
$: nameLabel = getNameLabel(user)
|
||||||
$: filteredGroups = getFilteredGroups($groups, searchTerm)
|
$: filteredGroups = getFilteredGroups($groups, searchTerm)
|
||||||
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
|
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
|
||||||
|
@ -97,9 +98,9 @@
|
||||||
return y._id === userId
|
return y._id === userId
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
$: globalRole = user?.admin?.global
|
$: globalRole = sdk.users.isAdmin(user)
|
||||||
? "admin"
|
? "admin"
|
||||||
: user?.builder?.global
|
: sdk.users.isBuilder(user)
|
||||||
? "developer"
|
? "developer"
|
||||||
: "appUser"
|
: "appUser"
|
||||||
|
|
||||||
|
@ -285,7 +286,7 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Role</Label>
|
<Label size="L">Role</Label>
|
||||||
<Select
|
<Select
|
||||||
disabled={!$auth.isAdmin}
|
disabled={!sdk.users.isAdmin($auth.user)}
|
||||||
value={globalRole}
|
value={globalRole}
|
||||||
options={Constants.BudibaseRoleOptions}
|
options={Constants.BudibaseRoleOptions}
|
||||||
on:change={updateUserRole}
|
on:change={updateUserRole}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon } from "@budibase/bbui"
|
import { Icon } from "@budibase/bbui"
|
||||||
import { apps } from "stores/portal"
|
import { apps } from "stores/portal"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
export let row
|
export let row
|
||||||
|
|
||||||
$: priviliged = row?.admin?.global || row?.builder?.global
|
$: priviliged = sdk.users.isAdminOrBuilder(row)
|
||||||
$: count = priviliged ? $apps.length : value?.length || 0
|
$: count = priviliged ? $apps.length : value?.length || 0
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { ActionButton } from "@budibase/bbui"
|
import { ActionButton } from "@budibase/bbui"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let value
|
export let value
|
||||||
|
|
||||||
|
@ -13,6 +14,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionButton disabled={!$auth.isAdmin} size="S" on:click={onClick}>
|
<ActionButton
|
||||||
|
disabled={!sdk.users.isAdmin($auth.user)}
|
||||||
|
size="S"
|
||||||
|
on:click={onClick}
|
||||||
|
>
|
||||||
Remove
|
Remove
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
import { Body, Select, ModalContent, notifications } from "@budibase/bbui"
|
import { Body, Select, ModalContent, notifications } from "@budibase/bbui"
|
||||||
import { users } from "stores/portal"
|
import { users } from "stores/portal"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let user
|
export let user
|
||||||
|
@ -15,7 +16,7 @@
|
||||||
.filter(role => role._id !== "PUBLIC")
|
.filter(role => role._id !== "PUBLIC")
|
||||||
.map(role => ({ value: role._id, label: role.name }))
|
.map(role => ({ value: role._id, label: role.name }))
|
||||||
|
|
||||||
if (!user?.builder?.global) {
|
if (!sdk.users.isBuilder(user, app?.appId)) {
|
||||||
options.push({ value: NO_ACCESS, label: "No Access" })
|
options.push({ value: NO_ACCESS, label: "No Access" })
|
||||||
}
|
}
|
||||||
let selectedRole = user?.roles?.[app?._id]
|
let selectedRole = user?.roles?.[app?._id]
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { OnboardingType } from "../../../../../constants"
|
import { OnboardingType } from "../../../../../constants"
|
||||||
import ScimBanner from "../_components/SCIMBanner.svelte"
|
import ScimBanner from "../_components/SCIMBanner.svelte"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
const fetch = fetchData({
|
const fetch = fetchData({
|
||||||
API,
|
API,
|
||||||
|
@ -66,7 +67,7 @@
|
||||||
let userData = []
|
let userData = []
|
||||||
|
|
||||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||||
$: readonly = !$auth.isAdmin || $features.isScimEnabled
|
$: readonly = !sdk.users.isAdmin($auth.user) || $features.isScimEnabled
|
||||||
|
|
||||||
$: debouncedUpdateFetch(searchEmail)
|
$: debouncedUpdateFetch(searchEmail)
|
||||||
$: schema = {
|
$: schema = {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { derived, writable, get } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { admin } from "stores/portal"
|
import { admin } from "stores/portal"
|
||||||
import analytics from "analytics"
|
import analytics from "analytics"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export function createAuthStore() {
|
export function createAuthStore() {
|
||||||
const auth = writable({
|
const auth = writable({
|
||||||
|
@ -13,13 +14,6 @@ export function createAuthStore() {
|
||||||
postLogout: false,
|
postLogout: false,
|
||||||
})
|
})
|
||||||
const store = derived(auth, $store => {
|
const store = derived(auth, $store => {
|
||||||
let isAdmin = false
|
|
||||||
let isBuilder = false
|
|
||||||
if ($store.user) {
|
|
||||||
const user = $store.user
|
|
||||||
isAdmin = !!user.admin?.global
|
|
||||||
isBuilder = !!user.builder?.global
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
user: $store.user,
|
user: $store.user,
|
||||||
accountPortalAccess: $store.accountPortalAccess,
|
accountPortalAccess: $store.accountPortalAccess,
|
||||||
|
@ -27,8 +21,6 @@ export function createAuthStore() {
|
||||||
tenantSet: $store.tenantSet,
|
tenantSet: $store.tenantSet,
|
||||||
loaded: $store.loaded,
|
loaded: $store.loaded,
|
||||||
postLogout: $store.postLogout,
|
postLogout: $store.postLogout,
|
||||||
isAdmin,
|
|
||||||
isBuilder,
|
|
||||||
isSSO: !!$store.user?.provider,
|
isSSO: !!$store.user?.provider,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -57,8 +49,8 @@ export function createAuthStore() {
|
||||||
name: user.account?.name,
|
name: user.account?.name,
|
||||||
user_id: user._id,
|
user_id: user._id,
|
||||||
tenant: user.tenantId,
|
tenant: user.tenantId,
|
||||||
admin: user?.admin?.global,
|
admin: sdk.users.isAdmin(user),
|
||||||
builder: user?.builder?.global,
|
builder: sdk.users.isBuilder(user),
|
||||||
"Company size": user.account?.size,
|
"Company size": user.account?.size,
|
||||||
"Job role": user.account?.profession,
|
"Job role": user.account?.profession,
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,8 +2,12 @@ import { derived } from "svelte/store"
|
||||||
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
import { isEnabled, TENANT_FEATURE_FLAGS } from "helpers/featureFlags"
|
||||||
import { admin } from "./admin"
|
import { admin } from "./admin"
|
||||||
import { auth } from "./auth"
|
import { auth } from "./auth"
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
|
const user = $auth?.user
|
||||||
|
const isAdmin = sdk.users.isAdmin(user)
|
||||||
|
const cloud = $admin?.cloud
|
||||||
// Determine user sub pages
|
// Determine user sub pages
|
||||||
let userSubPages = [
|
let userSubPages = [
|
||||||
{
|
{
|
||||||
|
@ -24,19 +28,21 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
title: "Apps",
|
title: "Apps",
|
||||||
href: "/builder/portal/apps",
|
href: "/builder/portal/apps",
|
||||||
},
|
},
|
||||||
{
|
]
|
||||||
|
if (sdk.users.isGlobalBuilder(user)) {
|
||||||
|
menu.push({
|
||||||
title: "Users",
|
title: "Users",
|
||||||
href: "/builder/portal/users",
|
href: "/builder/portal/users",
|
||||||
subPages: userSubPages,
|
subPages: userSubPages,
|
||||||
},
|
})
|
||||||
{
|
menu.push({
|
||||||
title: "Plugins",
|
title: "Plugins",
|
||||||
href: "/builder/portal/plugins",
|
href: "/builder/portal/plugins",
|
||||||
},
|
})
|
||||||
]
|
}
|
||||||
|
|
||||||
// Add settings page for admins
|
// Add settings page for admins
|
||||||
if ($auth.isAdmin) {
|
if (isAdmin) {
|
||||||
let settingsSubPages = [
|
let settingsSubPages = [
|
||||||
{
|
{
|
||||||
title: "Auth",
|
title: "Auth",
|
||||||
|
@ -59,7 +65,7 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
href: "/builder/portal/settings/environment",
|
href: "/builder/portal/settings/environment",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
if (!$admin.cloud) {
|
if (!cloud) {
|
||||||
settingsSubPages.push({
|
settingsSubPages.push({
|
||||||
title: "Version",
|
title: "Version",
|
||||||
href: "/builder/portal/settings/version",
|
href: "/builder/portal/settings/version",
|
||||||
|
@ -84,38 +90,35 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
||||||
href: "/builder/portal/account/usage",
|
href: "/builder/portal/account/usage",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
if ($auth.isAdmin) {
|
if (isAdmin) {
|
||||||
accountSubPages.push({
|
accountSubPages.push({
|
||||||
title: "Audit Logs",
|
title: "Audit Logs",
|
||||||
href: "/builder/portal/account/auditLogs",
|
href: "/builder/portal/account/auditLogs",
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!$admin.cloud) {
|
if (!cloud) {
|
||||||
accountSubPages.push({
|
accountSubPages.push({
|
||||||
title: "System Logs",
|
title: "System Logs",
|
||||||
href: "/builder/portal/account/systemLogs",
|
href: "/builder/portal/account/systemLogs",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($admin.cloud && $auth?.user?.accountPortalAccess) {
|
if (cloud && user?.accountPortalAccess) {
|
||||||
accountSubPages.push({
|
accountSubPages.push({
|
||||||
title: "Upgrade",
|
title: "Upgrade",
|
||||||
href: $admin.accountPortalUrl + "/portal/upgrade",
|
href: $admin?.accountPortalUrl + "/portal/upgrade",
|
||||||
})
|
})
|
||||||
} else if (!$admin.cloud && $auth.isAdmin) {
|
} else if (!cloud && isAdmin) {
|
||||||
accountSubPages.push({
|
accountSubPages.push({
|
||||||
title: "Upgrade",
|
title: "Upgrade",
|
||||||
href: "/builder/portal/account/upgrade",
|
href: "/builder/portal/account/upgrade",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// add license check here
|
// add license check here
|
||||||
if (
|
if (user?.accountPortalAccess && user.account.stripeCustomerId) {
|
||||||
$auth?.user?.accountPortalAccess &&
|
|
||||||
$auth.user.account.stripeCustomerId
|
|
||||||
) {
|
|
||||||
accountSubPages.push({
|
accountSubPages.push({
|
||||||
title: "Billing",
|
title: "Billing",
|
||||||
href: $admin.accountPortalUrl + "/portal/billing",
|
href: $admin?.accountPortalUrl + "/portal/billing",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
menu.push({
|
menu.push({
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { writable } from "svelte/store"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { update } from "lodash"
|
import { update } from "lodash"
|
||||||
import { licensing } from "."
|
import { licensing } from "."
|
||||||
|
import { sdk } from "@budibase/shared-core"
|
||||||
|
|
||||||
export function createUsersStore() {
|
export function createUsersStore() {
|
||||||
const { subscribe, set } = writable({})
|
const { subscribe, set } = writable({})
|
||||||
|
@ -111,8 +112,12 @@ export function createUsersStore() {
|
||||||
return await API.saveUser(user)
|
return await API.saveUser(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserRole = ({ admin, builder }) =>
|
const getUserRole = user =>
|
||||||
admin?.global ? "admin" : builder?.global ? "developer" : "appUser"
|
sdk.users.isAdmin(user)
|
||||||
|
? "admin"
|
||||||
|
: sdk.users.isBuilder(user)
|
||||||
|
? "developer"
|
||||||
|
: "appUser"
|
||||||
|
|
||||||
const refreshUsage =
|
const refreshUsage =
|
||||||
fn =>
|
fn =>
|
||||||
|
|
|
@ -30,6 +30,7 @@ import {
|
||||||
objectStore,
|
objectStore,
|
||||||
roles,
|
roles,
|
||||||
tenancy,
|
tenancy,
|
||||||
|
users,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { USERS_TABLE_SCHEMA } from "../../constants"
|
import { USERS_TABLE_SCHEMA } from "../../constants"
|
||||||
import {
|
import {
|
||||||
|
@ -49,8 +50,8 @@ import {
|
||||||
MigrationType,
|
MigrationType,
|
||||||
PlanType,
|
PlanType,
|
||||||
Screen,
|
Screen,
|
||||||
SocketSession,
|
|
||||||
UserCtx,
|
UserCtx,
|
||||||
|
ContextUser,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||||
import sdk from "../../sdk"
|
import sdk from "../../sdk"
|
||||||
|
@ -177,32 +178,10 @@ export const addSampleData = async (ctx: UserCtx) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetch(ctx: UserCtx) {
|
export async function fetch(ctx: UserCtx) {
|
||||||
const dev = ctx.query && ctx.query.status === AppStatus.DEV
|
ctx.body = await sdk.applications.fetch(
|
||||||
const all = ctx.query && ctx.query.status === AppStatus.ALL
|
ctx.query.status as AppStatus,
|
||||||
const apps = (await dbCore.getAllApps({ dev, all })) as App[]
|
ctx.user
|
||||||
|
)
|
||||||
const appIds = apps
|
|
||||||
.filter((app: any) => app.status === "development")
|
|
||||||
.map((app: any) => app.appId)
|
|
||||||
|
|
||||||
// get the locks for all the dev apps
|
|
||||||
if (dev || all) {
|
|
||||||
const locks = await getLocksById(appIds)
|
|
||||||
for (let app of apps) {
|
|
||||||
const lock = locks[app.appId]
|
|
||||||
if (lock) {
|
|
||||||
app.lockedBy = lock
|
|
||||||
} else {
|
|
||||||
// make sure its definitely not present
|
|
||||||
delete app.lockedBy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enrich apps with all builder user sessions
|
|
||||||
const enrichedApps = await sdk.users.sessions.enrichApps(apps)
|
|
||||||
|
|
||||||
ctx.body = await checkAppMetadata(enrichedApps)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAppDefinition(ctx: UserCtx) {
|
export async function fetchAppDefinition(ctx: UserCtx) {
|
||||||
|
@ -222,6 +201,7 @@ export async function fetchAppDefinition(ctx: UserCtx) {
|
||||||
|
|
||||||
export async function fetchAppPackage(ctx: UserCtx) {
|
export async function fetchAppPackage(ctx: UserCtx) {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
const appId = context.getAppId()
|
||||||
let application = await db.get<any>(DocumentType.APP_METADATA)
|
let application = await db.get<any>(DocumentType.APP_METADATA)
|
||||||
const layouts = await getLayouts()
|
const layouts = await getLayouts()
|
||||||
let screens = await getScreens()
|
let screens = await getScreens()
|
||||||
|
@ -233,7 +213,7 @@ export async function fetchAppPackage(ctx: UserCtx) {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Only filter screens if the user is not a builder
|
// Only filter screens if the user is not a builder
|
||||||
if (!(ctx.user.builder && ctx.user.builder.global)) {
|
if (!users.isBuilder(ctx.user, appId)) {
|
||||||
const userRoleId = getUserRoleId(ctx)
|
const userRoleId = getUserRoleId(ctx)
|
||||||
const accessController = new roles.AccessController()
|
const accessController = new roles.AccessController()
|
||||||
screens = await accessController.checkScreensAccess(screens, userRoleId)
|
screens = await accessController.checkScreensAccess(screens, userRoleId)
|
||||||
|
|
|
@ -15,7 +15,7 @@ router
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/applications",
|
"/api/applications",
|
||||||
authorized(permissions.BUILDER),
|
authorized(permissions.GLOBAL_BUILDER),
|
||||||
applicationValidator(),
|
applicationValidator(),
|
||||||
controller.create
|
controller.create
|
||||||
)
|
)
|
||||||
|
@ -55,7 +55,7 @@ router
|
||||||
)
|
)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/applications/:appId",
|
"/api/applications/:appId",
|
||||||
authorized(permissions.BUILDER),
|
authorized(permissions.GLOBAL_BUILDER),
|
||||||
controller.destroy
|
controller.destroy
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -8,14 +8,18 @@ const router: Router = new Router()
|
||||||
router
|
router
|
||||||
.post(
|
.post(
|
||||||
"/api/plugin/upload",
|
"/api/plugin/upload",
|
||||||
authorized(permissions.BUILDER),
|
authorized(permissions.GLOBAL_BUILDER),
|
||||||
controller.upload
|
controller.upload
|
||||||
)
|
)
|
||||||
.post("/api/plugin", authorized(permissions.BUILDER), controller.create)
|
.post(
|
||||||
.get("/api/plugin", authorized(permissions.BUILDER), controller.fetch)
|
"/api/plugin",
|
||||||
|
authorized(permissions.GLOBAL_BUILDER),
|
||||||
|
controller.create
|
||||||
|
)
|
||||||
|
.get("/api/plugin", authorized(permissions.GLOBAL_BUILDER), controller.fetch)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/plugin/:pluginId",
|
"/api/plugin/:pluginId",
|
||||||
authorized(permissions.BUILDER),
|
authorized(permissions.GLOBAL_BUILDER),
|
||||||
controller.destroy
|
controller.destroy
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -7,14 +7,14 @@ import userEndpoints from "./users"
|
||||||
import authorized from "../../../middleware/authorized"
|
import authorized from "../../../middleware/authorized"
|
||||||
import publicApi from "../../../middleware/publicApi"
|
import publicApi from "../../../middleware/publicApi"
|
||||||
import { paramResource, paramSubResource } from "../../../middleware/resourceId"
|
import { paramResource, paramSubResource } from "../../../middleware/resourceId"
|
||||||
|
import { PermissionType, PermissionLevel } from "@budibase/types"
|
||||||
import { CtxFn } from "./utils/Endpoint"
|
import { CtxFn } from "./utils/Endpoint"
|
||||||
import mapperMiddleware from "./middleware/mapper"
|
import mapperMiddleware from "./middleware/mapper"
|
||||||
import env from "../../../environment"
|
import env from "../../../environment"
|
||||||
// below imports don't have declaration files
|
// below imports don't have declaration files
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const { RateLimit, Stores } = require("koa2-ratelimit")
|
const { RateLimit, Stores } = require("koa2-ratelimit")
|
||||||
import { middleware, redis, permissions } from "@budibase/backend-core"
|
import { middleware, redis } from "@budibase/backend-core"
|
||||||
const { PermissionType, PermissionLevel } = permissions
|
|
||||||
|
|
||||||
const PREFIX = "/api/public/v1"
|
const PREFIX = "/api/public/v1"
|
||||||
// allow a lot more requests when in test
|
// allow a lot more requests when in test
|
||||||
|
@ -111,7 +111,7 @@ function applyAdminRoutes(endpoints: any) {
|
||||||
|
|
||||||
function applyRoutes(
|
function applyRoutes(
|
||||||
endpoints: any,
|
endpoints: any,
|
||||||
permType: string,
|
permType: PermissionType,
|
||||||
resource: string,
|
resource: string,
|
||||||
subResource?: string
|
subResource?: string
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -36,7 +36,10 @@ export const clearAllApps = async (
|
||||||
exceptions: Array<string> = []
|
exceptions: Array<string> = []
|
||||||
) => {
|
) => {
|
||||||
await tenancy.doInTenant(tenantId, async () => {
|
await tenancy.doInTenant(tenantId, async () => {
|
||||||
const req: any = { query: { status: AppStatus.DEV }, user: { tenantId } }
|
const req: any = {
|
||||||
|
query: { status: AppStatus.DEV },
|
||||||
|
user: { tenantId, builder: { global: true } },
|
||||||
|
}
|
||||||
await appController.fetch(req)
|
await appController.fetch(req)
|
||||||
const apps = req.body
|
const apps = req.body
|
||||||
if (!apps || apps.length <= 0) {
|
if (!apps || apps.length <= 0) {
|
||||||
|
|
|
@ -15,7 +15,15 @@ import * as api from "./api"
|
||||||
import * as automations from "./automations"
|
import * as automations from "./automations"
|
||||||
import { Thread } from "./threads"
|
import { Thread } from "./threads"
|
||||||
import * as redis from "./utilities/redis"
|
import * as redis from "./utilities/redis"
|
||||||
import { events, logging, middleware, timers } from "@budibase/backend-core"
|
import { ServiceType } from "@budibase/types"
|
||||||
|
import {
|
||||||
|
events,
|
||||||
|
logging,
|
||||||
|
middleware,
|
||||||
|
timers,
|
||||||
|
env as coreEnv,
|
||||||
|
} from "@budibase/backend-core"
|
||||||
|
coreEnv._set("SERVICE_TYPE", ServiceType.APPS)
|
||||||
import { startup } from "./startup"
|
import { startup } from "./startup"
|
||||||
const Sentry = require("@sentry/node")
|
const Sentry = require("@sentry/node")
|
||||||
const destroyable = require("server-destroy")
|
const destroyable = require("server-destroy")
|
||||||
|
|
|
@ -3,10 +3,10 @@ import { db as dbCore } from "@budibase/backend-core"
|
||||||
|
|
||||||
type Optional = string | null
|
type Optional = string | null
|
||||||
|
|
||||||
export const AppStatus = {
|
export const enum AppStatus {
|
||||||
DEV: "development",
|
DEV = "development",
|
||||||
ALL: "all",
|
ALL = "all",
|
||||||
DEPLOYED: "published",
|
DEPLOYED = "published",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BudibaseInternalDB = {
|
export const BudibaseInternalDB = {
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { env as coreEnv } from "@budibase/backend-core"
|
||||||
|
import { ServiceType } from "@budibase/types"
|
||||||
|
coreEnv._set("SERVICE_TYPE", ServiceType.APPS)
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
|
|
||||||
function isTest() {
|
function isTest() {
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
import { roles, permissions, auth, context } from "@budibase/backend-core"
|
import {
|
||||||
import { Role } from "@budibase/types"
|
auth,
|
||||||
|
context,
|
||||||
|
permissions,
|
||||||
|
roles,
|
||||||
|
users,
|
||||||
|
} from "@budibase/backend-core"
|
||||||
|
import { PermissionLevel, PermissionType, Role, UserCtx } from "@budibase/types"
|
||||||
import builderMiddleware from "./builder"
|
import builderMiddleware from "./builder"
|
||||||
import { isWebhookEndpoint } from "./utils"
|
import { isWebhookEndpoint } from "./utils"
|
||||||
|
|
||||||
|
@ -16,15 +22,20 @@ const csrf = auth.buildCsrfMiddleware()
|
||||||
* - Otherwise the user must have the required role.
|
* - Otherwise the user must have the required role.
|
||||||
*/
|
*/
|
||||||
const checkAuthorized = async (
|
const checkAuthorized = async (
|
||||||
ctx: any,
|
ctx: UserCtx,
|
||||||
resourceRoles: any,
|
resourceRoles: any,
|
||||||
permType: any,
|
permType: PermissionType,
|
||||||
permLevel: any
|
permLevel: PermissionLevel
|
||||||
) => {
|
) => {
|
||||||
|
const appId = context.getAppId()
|
||||||
|
const isGlobalBuilderApi = permType === PermissionType.GLOBAL_BUILDER
|
||||||
|
const isBuilderApi = permType === PermissionType.BUILDER
|
||||||
|
const globalBuilder = users.isGlobalBuilder(ctx.user)
|
||||||
|
let isBuilder = appId
|
||||||
|
? users.isBuilder(ctx.user, appId)
|
||||||
|
: users.hasBuilderPermissions(ctx.user)
|
||||||
// check if this is a builder api and the user is not a builder
|
// check if this is a builder api and the user is not a builder
|
||||||
const isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
|
if ((isGlobalBuilderApi && !globalBuilder) || (isBuilderApi && !isBuilder)) {
|
||||||
const isBuilderApi = permType === permissions.PermissionType.BUILDER
|
|
||||||
if (isBuilderApi && !isBuilder) {
|
|
||||||
return ctx.throw(403, "Not Authorized")
|
return ctx.throw(403, "Not Authorized")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,10 +46,10 @@ const checkAuthorized = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkAuthorizedResource = async (
|
const checkAuthorizedResource = async (
|
||||||
ctx: any,
|
ctx: UserCtx,
|
||||||
resourceRoles: any,
|
resourceRoles: any,
|
||||||
permType: any,
|
permType: PermissionType,
|
||||||
permLevel: any
|
permLevel: PermissionLevel
|
||||||
) => {
|
) => {
|
||||||
// get the user's roles
|
// get the user's roles
|
||||||
const roleId = ctx.roleId || roles.BUILTIN_ROLE_IDS.PUBLIC
|
const roleId = ctx.roleId || roles.BUILTIN_ROLE_IDS.PUBLIC
|
||||||
|
@ -64,8 +75,8 @@ const checkAuthorizedResource = async (
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
permType: any,
|
permType: PermissionType,
|
||||||
permLevel: any = null,
|
permLevel?: PermissionLevel,
|
||||||
opts = { schema: false }
|
opts = { schema: false }
|
||||||
) =>
|
) =>
|
||||||
async (ctx: any, next: any) => {
|
async (ctx: any, next: any) => {
|
||||||
|
@ -83,12 +94,12 @@ export default (
|
||||||
let resourceRoles: any = []
|
let resourceRoles: any = []
|
||||||
let otherLevelRoles: any = []
|
let otherLevelRoles: any = []
|
||||||
const otherLevel =
|
const otherLevel =
|
||||||
permLevel === permissions.PermissionLevel.READ
|
permLevel === PermissionLevel.READ
|
||||||
? permissions.PermissionLevel.WRITE
|
? PermissionLevel.WRITE
|
||||||
: permissions.PermissionLevel.READ
|
: PermissionLevel.READ
|
||||||
const appId = context.getAppId()
|
const appId = context.getAppId()
|
||||||
if (appId && hasResource(ctx)) {
|
if (appId && hasResource(ctx)) {
|
||||||
resourceRoles = await roles.getRequiredResourceRole(permLevel, ctx)
|
resourceRoles = await roles.getRequiredResourceRole(permLevel!, ctx)
|
||||||
if (opts && opts.schema) {
|
if (opts && opts.schema) {
|
||||||
otherLevelRoles = await roles.getRequiredResourceRole(otherLevel, ctx)
|
otherLevelRoles = await roles.getRequiredResourceRole(otherLevel, ctx)
|
||||||
}
|
}
|
||||||
|
@ -110,13 +121,16 @@ export default (
|
||||||
|
|
||||||
// check general builder stuff, this middleware is a good way
|
// check general builder stuff, this middleware is a good way
|
||||||
// to find API endpoints which are builder focused
|
// to find API endpoints which are builder focused
|
||||||
if (permType === permissions.PermissionType.BUILDER) {
|
if (
|
||||||
|
permType === PermissionType.BUILDER ||
|
||||||
|
permType === PermissionType.GLOBAL_BUILDER
|
||||||
|
) {
|
||||||
await builderMiddleware(ctx)
|
await builderMiddleware(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// check authorized
|
// check authorized
|
||||||
await checkAuthorized(ctx, resourceRoles, permType, permLevel)
|
await checkAuthorized(ctx, resourceRoles, permType, permLevel!)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// this is a schema, check if
|
// this is a schema, check if
|
||||||
if (opts && opts.schema && permLevel) {
|
if (opts && opts.schema && permLevel) {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
setDebounce,
|
setDebounce,
|
||||||
} from "../utilities/redis"
|
} from "../utilities/redis"
|
||||||
import { db as dbCore, cache } from "@budibase/backend-core"
|
import { db as dbCore, cache } from "@budibase/backend-core"
|
||||||
import { UserCtx, Database, App } from "@budibase/types"
|
import { UserCtx, Database } from "@budibase/types"
|
||||||
|
|
||||||
const DEBOUNCE_TIME_SEC = 30
|
const DEBOUNCE_TIME_SEC = 30
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,13 @@ import {
|
||||||
roles,
|
roles,
|
||||||
tenancy,
|
tenancy,
|
||||||
context,
|
context,
|
||||||
|
users,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { generateUserMetadataID, isDevAppID } from "../db/utils"
|
import { generateUserMetadataID, isDevAppID } from "../db/utils"
|
||||||
import { getCachedSelf } from "../utilities/global"
|
import { getCachedSelf } from "../utilities/global"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { isWebhookEndpoint } from "./utils"
|
import { isWebhookEndpoint } from "./utils"
|
||||||
import { UserCtx } from "@budibase/types"
|
import { UserCtx, ContextUser } from "@budibase/types"
|
||||||
|
|
||||||
export default async (ctx: UserCtx, next: any) => {
|
export default async (ctx: UserCtx, next: any) => {
|
||||||
// try to get the appID from the request
|
// try to get the appID from the request
|
||||||
|
@ -23,7 +24,7 @@ export default async (ctx: UserCtx, next: any) => {
|
||||||
if (
|
if (
|
||||||
isDevAppID(requestAppId) &&
|
isDevAppID(requestAppId) &&
|
||||||
!isWebhookEndpoint(ctx) &&
|
!isWebhookEndpoint(ctx) &&
|
||||||
(!ctx.user || !ctx.user.builder || !ctx.user.builder.global)
|
!users.isBuilder(ctx.user, requestAppId)
|
||||||
) {
|
) {
|
||||||
return ctx.redirect("/")
|
return ctx.redirect("/")
|
||||||
}
|
}
|
||||||
|
@ -42,8 +43,7 @@ export default async (ctx: UserCtx, next: any) => {
|
||||||
roleId = globalUser.roleId || roleId
|
roleId = globalUser.roleId || roleId
|
||||||
|
|
||||||
// Allow builders to specify their role via a header
|
// Allow builders to specify their role via a header
|
||||||
const isBuilder =
|
const isBuilder = users.isBuilder(globalUser, appId)
|
||||||
globalUser && globalUser.builder && globalUser.builder.global
|
|
||||||
const isDevApp = appId && isDevAppID(appId)
|
const isDevApp = appId && isDevAppID(appId)
|
||||||
const roleHeader =
|
const roleHeader =
|
||||||
ctx.request &&
|
ctx.request &&
|
||||||
|
@ -56,8 +56,7 @@ export default async (ctx: UserCtx, next: any) => {
|
||||||
roleId = roleHeader
|
roleId = roleHeader
|
||||||
|
|
||||||
// Delete admin and builder flags so that the specified role is honoured
|
// Delete admin and builder flags so that the specified role is honoured
|
||||||
delete ctx.user.builder
|
ctx.user = users.removePortalUserPermissions(ctx.user) as ContextUser
|
||||||
delete ctx.user.admin
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Swallow error and do nothing
|
// Swallow error and do nothing
|
||||||
|
@ -71,7 +70,6 @@ export default async (ctx: UserCtx, next: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return context.doInAppContext(appId, async () => {
|
return context.doInAppContext(appId, async () => {
|
||||||
let skipCookie = false
|
|
||||||
// if the user not in the right tenant then make sure they have no permissions
|
// if the user not in the right tenant then make sure they have no permissions
|
||||||
// need to judge this only based on the request app ID,
|
// need to judge this only based on the request app ID,
|
||||||
if (
|
if (
|
||||||
|
@ -81,12 +79,9 @@ export default async (ctx: UserCtx, next: any) => {
|
||||||
!tenancy.isUserInAppTenant(requestAppId, ctx.user)
|
!tenancy.isUserInAppTenant(requestAppId, ctx.user)
|
||||||
) {
|
) {
|
||||||
// don't error, simply remove the users rights (they are a public user)
|
// don't error, simply remove the users rights (they are a public user)
|
||||||
delete ctx.user.builder
|
ctx.user = users.cleanseUserObject(ctx.user) as ContextUser
|
||||||
delete ctx.user.admin
|
|
||||||
delete ctx.user.roles
|
|
||||||
ctx.isAuthenticated = false
|
ctx.isAuthenticated = false
|
||||||
roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
|
roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
|
||||||
skipCookie = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.appId = appId
|
ctx.appId = appId
|
||||||
|
|
|
@ -9,7 +9,7 @@ jest.mock("../../environment", () => ({
|
||||||
)
|
)
|
||||||
const authorizedMiddleware = require("../authorized").default
|
const authorizedMiddleware = require("../authorized").default
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
const { permissions } = require("@budibase/backend-core")
|
const { PermissionType, PermissionLevel } = require("@budibase/types")
|
||||||
|
|
||||||
const APP_ID = ""
|
const APP_ID = ""
|
||||||
|
|
||||||
|
@ -112,7 +112,7 @@ describe("Authorization middleware", () => {
|
||||||
|
|
||||||
it("throws if the user does not have builder permissions", async () => {
|
it("throws if the user does not have builder permissions", async () => {
|
||||||
config.setEnvironment(false)
|
config.setEnvironment(false)
|
||||||
config.setMiddlewareRequiredPermission(permissions.PermissionType.BUILDER)
|
config.setMiddlewareRequiredPermission(PermissionType.BUILDER)
|
||||||
config.setUser({
|
config.setUser({
|
||||||
role: {
|
role: {
|
||||||
_id: ""
|
_id: ""
|
||||||
|
@ -124,13 +124,13 @@ describe("Authorization middleware", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("passes on to next() middleware if the user has resource permission", async () => {
|
it("passes on to next() middleware if the user has resource permission", async () => {
|
||||||
config.setResourceId(permissions.PermissionType.QUERY)
|
config.setResourceId(PermissionType.QUERY)
|
||||||
config.setUser({
|
config.setUser({
|
||||||
role: {
|
role: {
|
||||||
_id: ""
|
_id: ""
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
config.setMiddlewareRequiredPermission(permissions.PermissionType.QUERY)
|
config.setMiddlewareRequiredPermission(PermissionType.QUERY)
|
||||||
|
|
||||||
await config.executeMiddleware()
|
await config.executeMiddleware()
|
||||||
expect(config.next).toHaveBeenCalled()
|
expect(config.next).toHaveBeenCalled()
|
||||||
|
@ -154,7 +154,7 @@ describe("Authorization middleware", () => {
|
||||||
_id: ""
|
_id: ""
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
config.setMiddlewareRequiredPermission(permissions.PermissionType.ADMIN, permissions.PermissionLevel.BASIC)
|
config.setMiddlewareRequiredPermission(PermissionType.ADMIN, PermissionLevel.BASIC)
|
||||||
|
|
||||||
await config.executeMiddleware()
|
await config.executeMiddleware()
|
||||||
expect(config.throw).toHaveBeenCalledWith(403, "User does not have permission")
|
expect(config.throw).toHaveBeenCalledWith(403, "User does not have permission")
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { events, db as dbUtils } from "@budibase/backend-core"
|
import {
|
||||||
|
events,
|
||||||
|
db as dbUtils,
|
||||||
|
users as usersCore,
|
||||||
|
} from "@budibase/backend-core"
|
||||||
import { User, CloudAccount } from "@budibase/types"
|
import { User, CloudAccount } from "@budibase/types"
|
||||||
import { DEFAULT_TIMESTAMP } from ".."
|
import { DEFAULT_TIMESTAMP } from ".."
|
||||||
|
|
||||||
|
@ -30,11 +34,11 @@ export const backfill = async (
|
||||||
await events.identification.identifyUser(user, account, timestamp)
|
await events.identification.identifyUser(user, account, timestamp)
|
||||||
await events.user.created(user, timestamp)
|
await events.user.created(user, timestamp)
|
||||||
|
|
||||||
if (user.admin?.global) {
|
if (usersCore.hasAdminPermissions(user)) {
|
||||||
await events.user.permissionAdminAssigned(user, timestamp)
|
await events.user.permissionAdminAssigned(user, timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.builder?.global) {
|
if (usersCore.hasBuilderPermissions(user)) {
|
||||||
await events.user.permissionBuilderAssigned(user, timestamp)
|
await events.user.permissionBuilderAssigned(user, timestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { AppStatus } from "../../../db/utils"
|
||||||
|
import { App, ContextUser } from "@budibase/types"
|
||||||
|
import { getLocksById } from "../../../utilities/redis"
|
||||||
|
import { enrichApps } from "../../users/sessions"
|
||||||
|
import { checkAppMetadata } from "../../../automations/logging"
|
||||||
|
import { db as dbCore, users } from "@budibase/backend-core"
|
||||||
|
|
||||||
|
export function filterAppList(user: ContextUser, apps: App[]) {
|
||||||
|
let appList: string[] = []
|
||||||
|
const roleApps = Object.keys(user.roles || {})
|
||||||
|
if (users.hasAppBuilderPermissions(user)) {
|
||||||
|
appList = user.builder?.apps || []
|
||||||
|
appList = appList.concat(roleApps)
|
||||||
|
} else if (!users.isAdminOrBuilder(user)) {
|
||||||
|
appList = roleApps
|
||||||
|
} else {
|
||||||
|
return apps
|
||||||
|
}
|
||||||
|
return apps.filter(app => appList.includes(dbCore.getProdAppID(app.appId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetch(status: AppStatus, user: ContextUser) {
|
||||||
|
const dev = status === AppStatus.DEV
|
||||||
|
const all = status === AppStatus.ALL
|
||||||
|
let apps = (await dbCore.getAllApps({ dev, all })) as App[]
|
||||||
|
apps = filterAppList(user, apps)
|
||||||
|
|
||||||
|
const appIds = apps
|
||||||
|
.filter((app: any) => app.status === "development")
|
||||||
|
.map((app: any) => app.appId)
|
||||||
|
|
||||||
|
// get the locks for all the dev apps
|
||||||
|
if (dev || all) {
|
||||||
|
const locks = await getLocksById(appIds)
|
||||||
|
for (let app of apps) {
|
||||||
|
const lock = locks[app.appId]
|
||||||
|
if (lock) {
|
||||||
|
app.lockedBy = lock
|
||||||
|
} else {
|
||||||
|
// make sure its definitely not present
|
||||||
|
delete app.lockedBy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrich apps with all builder user sessions
|
||||||
|
const enrichedApps = await enrichApps(apps)
|
||||||
|
|
||||||
|
return await checkAppMetadata(enrichedApps)
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
import * as sync from "./sync"
|
import * as sync from "./sync"
|
||||||
import * as utils from "./utils"
|
import * as utils from "./utils"
|
||||||
|
import * as applications from "./applications"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...sync,
|
...sync,
|
||||||
...utils,
|
...utils,
|
||||||
|
...applications,
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
cache,
|
cache,
|
||||||
tenancy,
|
tenancy,
|
||||||
context,
|
context,
|
||||||
|
users,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { groups } from "@budibase/pro"
|
import { groups } from "@budibase/pro"
|
||||||
|
@ -22,8 +23,7 @@ export function updateAppRole(
|
||||||
}
|
}
|
||||||
// if in an multi-tenancy environment make sure roles are never updated
|
// if in an multi-tenancy environment make sure roles are never updated
|
||||||
if (env.MULTI_TENANCY && appId && !tenancy.isUserInAppTenant(appId, user)) {
|
if (env.MULTI_TENANCY && appId && !tenancy.isUserInAppTenant(appId, user)) {
|
||||||
delete user.builder
|
user = users.removePortalUserPermissions(user)
|
||||||
delete user.admin
|
|
||||||
user.roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
|
user.roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ export function updateAppRole(
|
||||||
user.roleId = user.roles[dbCore.getProdAppID(appId)]
|
user.roleId = user.roles[dbCore.getProdAppID(appId)]
|
||||||
}
|
}
|
||||||
// if a role wasn't found then either set as admin (builder) or public (everyone else)
|
// if a role wasn't found then either set as admin (builder) or public (everyone else)
|
||||||
if (!user.roleId && user.builder && user.builder.global) {
|
if (!user.roleId && users.isBuilder(user, appId)) {
|
||||||
user.roleId = roles.BUILTIN_ROLE_IDS.ADMIN
|
user.roleId = roles.BUILTIN_ROLE_IDS.ADMIN
|
||||||
} else if (!user.roleId && !user?.userGroups?.length) {
|
} else if (!user.roleId && !user?.userGroups?.length) {
|
||||||
user.roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
|
user.roleId = roles.BUILTIN_ROLE_IDS.PUBLIC
|
||||||
|
|
|
@ -2,3 +2,4 @@ export * from "./constants"
|
||||||
export * as dataFilters from "./filters"
|
export * as dataFilters from "./filters"
|
||||||
export * as helpers from "./helpers"
|
export * as helpers from "./helpers"
|
||||||
export * as utils from "./utils"
|
export * as utils from "./utils"
|
||||||
|
export * as sdk from "./sdk"
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { DocumentType, prefixed } from "@budibase/types"
|
||||||
|
|
||||||
|
const APP_PREFIX = prefixed(DocumentType.APP)
|
||||||
|
const APP_DEV_PREFIX = prefixed(DocumentType.APP_DEV)
|
||||||
|
|
||||||
|
export function getDevAppID(appId: string) {
|
||||||
|
if (!appId) {
|
||||||
|
throw new Error("No app ID provided")
|
||||||
|
}
|
||||||
|
if (appId.startsWith(APP_DEV_PREFIX)) {
|
||||||
|
return appId
|
||||||
|
}
|
||||||
|
// split to take off the app_ element, then join it together incase any other app_ exist
|
||||||
|
const split = appId.split(APP_PREFIX)
|
||||||
|
split.shift()
|
||||||
|
const rest = split.join(APP_PREFIX)
|
||||||
|
return `${APP_DEV_PREFIX}${rest}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a development app ID to a deployed app ID.
|
||||||
|
*/
|
||||||
|
export function getProdAppID(appId: string) {
|
||||||
|
if (!appId) {
|
||||||
|
throw new Error("No app ID provided")
|
||||||
|
}
|
||||||
|
if (!appId.startsWith(APP_DEV_PREFIX)) {
|
||||||
|
return appId
|
||||||
|
}
|
||||||
|
// split to take off the app_dev element, then join it together incase any other app_ exist
|
||||||
|
const split = appId.split(APP_DEV_PREFIX)
|
||||||
|
split.shift()
|
||||||
|
const rest = split.join(APP_DEV_PREFIX)
|
||||||
|
return `${APP_PREFIX}${rest}`
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
export * as applications from "./applications"
|
||||||
|
export * as users from "./users"
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { ContextUser, User } from "@budibase/types"
|
||||||
|
import { getProdAppID } from "./applications"
|
||||||
|
|
||||||
|
// checks if a user is specifically a builder, given an app ID
|
||||||
|
export function isBuilder(user: User | ContextUser, appId?: string): boolean {
|
||||||
|
if (!user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (user.builder?.global) {
|
||||||
|
return true
|
||||||
|
} else if (appId && user.builder?.apps?.includes(getProdAppID(appId))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isGlobalBuilder(user: User | ContextUser): boolean {
|
||||||
|
return (isBuilder(user) && !hasAppBuilderPermissions(user)) || isAdmin(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// alias for hasAdminPermission, currently do the same thing
|
||||||
|
// in future whether someone has admin permissions and whether they are
|
||||||
|
// an admin for a specific resource could be separated
|
||||||
|
export function isAdmin(user: User | ContextUser): boolean {
|
||||||
|
if (!user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return hasAdminPermissions(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAdminOrBuilder(
|
||||||
|
user: User | ContextUser,
|
||||||
|
appId?: string
|
||||||
|
): boolean {
|
||||||
|
return isBuilder(user, appId) || isAdmin(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if they are a builder within an app (not necessarily a global builder)
|
||||||
|
export function hasAppBuilderPermissions(user?: User | ContextUser): boolean {
|
||||||
|
if (!user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const appLength = user.builder?.apps?.length
|
||||||
|
const isGlobalBuilder = !!user.builder?.global
|
||||||
|
return !isGlobalBuilder && appLength != null && appLength > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks if a user is capable of building any app
|
||||||
|
export function hasBuilderPermissions(user?: User | ContextUser): boolean {
|
||||||
|
if (!user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return user.builder?.global || hasAppBuilderPermissions(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks if a user is capable of being an admin
|
||||||
|
export function hasAdminPermissions(user?: User | ContextUser): boolean {
|
||||||
|
if (!user) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !!user.admin?.global
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./documents"
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./installation"
|
|
@ -0,0 +1,4 @@
|
||||||
|
export enum ServiceType {
|
||||||
|
WORKER = "worker",
|
||||||
|
APPS = "apps",
|
||||||
|
}
|
|
@ -1,3 +1,44 @@
|
||||||
|
export const SEPARATOR = "_"
|
||||||
|
export const UNICODE_MAX = "\ufff0"
|
||||||
|
|
||||||
|
export const prefixed = (type: DocumentType) => `${type}${SEPARATOR}`
|
||||||
|
|
||||||
|
export enum DocumentType {
|
||||||
|
USER = "us",
|
||||||
|
GROUP = "gr",
|
||||||
|
WORKSPACE = "workspace",
|
||||||
|
CONFIG = "config",
|
||||||
|
TEMPLATE = "template",
|
||||||
|
APP = "app",
|
||||||
|
DEV = "dev",
|
||||||
|
APP_DEV = "app_dev",
|
||||||
|
APP_METADATA = "app_metadata",
|
||||||
|
ROLE = "role",
|
||||||
|
MIGRATIONS = "migrations",
|
||||||
|
DEV_INFO = "devinfo",
|
||||||
|
AUTOMATION_LOG = "log_au",
|
||||||
|
ACCOUNT_METADATA = "acc_metadata",
|
||||||
|
PLUGIN = "plg",
|
||||||
|
DATASOURCE = "datasource",
|
||||||
|
DATASOURCE_PLUS = "datasource_plus",
|
||||||
|
APP_BACKUP = "backup",
|
||||||
|
TABLE = "ta",
|
||||||
|
ROW = "ro",
|
||||||
|
AUTOMATION = "au",
|
||||||
|
LINK = "li",
|
||||||
|
WEBHOOK = "wh",
|
||||||
|
INSTANCE = "inst",
|
||||||
|
LAYOUT = "layout",
|
||||||
|
SCREEN = "screen",
|
||||||
|
QUERY = "query",
|
||||||
|
DEPLOYMENTS = "deployments",
|
||||||
|
METADATA = "metadata",
|
||||||
|
MEM_VIEW = "view",
|
||||||
|
USER_FLAG = "flag",
|
||||||
|
AUTOMATION_METADATA = "meta_au",
|
||||||
|
AUDIT_LOG = "al",
|
||||||
|
}
|
||||||
|
|
||||||
export interface Document {
|
export interface Document {
|
||||||
_id?: string
|
_id?: string
|
||||||
_rev?: string
|
_rev?: string
|
||||||
|
|
|
@ -42,7 +42,8 @@ export interface User extends Document {
|
||||||
forceResetPassword?: boolean
|
forceResetPassword?: boolean
|
||||||
roles: UserRoles
|
roles: UserRoles
|
||||||
builder?: {
|
builder?: {
|
||||||
global: boolean
|
global?: boolean
|
||||||
|
apps?: string[]
|
||||||
}
|
}
|
||||||
admin?: {
|
admin?: {
|
||||||
global: boolean
|
global: boolean
|
||||||
|
@ -69,7 +70,8 @@ export interface UserRoles {
|
||||||
|
|
||||||
export interface BuilderUser extends User {
|
export interface BuilderUser extends User {
|
||||||
builder: {
|
builder: {
|
||||||
global: boolean
|
global?: boolean
|
||||||
|
apps?: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +84,12 @@ export interface AdminUser extends User {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminOnlyUser extends User {
|
||||||
|
admin: {
|
||||||
|
global: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function isUser(user: object): user is User {
|
export function isUser(user: object): user is User {
|
||||||
return !!(user as User).roles
|
return !!(user as User).roles
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from "./documents"
|
export * from "./documents"
|
||||||
export * from "./sdk"
|
export * from "./sdk"
|
||||||
export * from "./api"
|
export * from "./api"
|
||||||
|
export * from "./core"
|
||||||
export * from "./shared"
|
export * from "./shared"
|
||||||
|
|
|
@ -18,3 +18,4 @@ export * from "./sso"
|
||||||
export * from "./user"
|
export * from "./user"
|
||||||
export * from "./cli"
|
export * from "./cli"
|
||||||
export * from "./websocket"
|
export * from "./websocket"
|
||||||
|
export * from "./permissions"
|
||||||
|
|
|
@ -38,6 +38,7 @@ export interface Ctx<RequestBody = any, ResponseBody = any> extends Context {
|
||||||
export interface UserCtx<RequestBody = any, ResponseBody = any>
|
export interface UserCtx<RequestBody = any, ResponseBody = any>
|
||||||
extends Ctx<RequestBody, ResponseBody> {
|
extends Ctx<RequestBody, ResponseBody> {
|
||||||
user: ContextUser
|
user: ContextUser
|
||||||
|
roleId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
export enum PermissionLevel {
|
||||||
|
READ = "read",
|
||||||
|
WRITE = "write",
|
||||||
|
EXECUTE = "execute",
|
||||||
|
ADMIN = "admin",
|
||||||
|
}
|
||||||
|
|
||||||
|
// these are the global types, that govern the underlying default behaviour
|
||||||
|
export enum PermissionType {
|
||||||
|
APP = "app",
|
||||||
|
TABLE = "table",
|
||||||
|
USER = "user",
|
||||||
|
AUTOMATION = "automation",
|
||||||
|
WEBHOOK = "webhook",
|
||||||
|
BUILDER = "builder",
|
||||||
|
GLOBAL_BUILDER = "globalBuilder",
|
||||||
|
VIEW = "view",
|
||||||
|
QUERY = "query",
|
||||||
|
}
|
|
@ -55,8 +55,8 @@ async function passportCallback(
|
||||||
export const login = async (ctx: Ctx<LoginRequest>, next: any) => {
|
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.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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ export const googlePreAuth = async (ctx: any, next: any) => {
|
||||||
const strategy = await google.strategyFactory(
|
const strategy = await google.strategyFactory(
|
||||||
config,
|
config,
|
||||||
callbackUrl,
|
callbackUrl,
|
||||||
userSdk.save
|
userSdk.db.save
|
||||||
)
|
)
|
||||||
|
|
||||||
return passport.authenticate(strategy, {
|
return passport.authenticate(strategy, {
|
||||||
|
@ -193,7 +193,7 @@ export const googleCallback = async (ctx: any, next: any) => {
|
||||||
const strategy = await google.strategyFactory(
|
const strategy = await google.strategyFactory(
|
||||||
config,
|
config,
|
||||||
callbackUrl,
|
callbackUrl,
|
||||||
userSdk.save
|
userSdk.db.save
|
||||||
)
|
)
|
||||||
|
|
||||||
return passport.authenticate(
|
return passport.authenticate(
|
||||||
|
@ -228,7 +228,7 @@ export const oidcStrategyFactory = async (ctx: any, configId: any) => {
|
||||||
|
|
||||||
//Remote Config
|
//Remote Config
|
||||||
const enrichedConfig = await oidc.fetchStrategyConfig(config, callbackUrl)
|
const enrichedConfig = await oidc.fetchStrategyConfig(config, callbackUrl)
|
||||||
return oidc.strategyFactory(enrichedConfig, userSdk.save)
|
return oidc.strategyFactory(enrichedConfig, userSdk.db.save)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,10 +5,10 @@ import {
|
||||||
cache,
|
cache,
|
||||||
tenancy,
|
tenancy,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { BBContext, App } from "@budibase/types"
|
import sdk from "../../../sdk"
|
||||||
import { allUsers } from "../../../sdk/users"
|
import { Ctx, App } from "@budibase/types"
|
||||||
|
|
||||||
export async function fetch(ctx: BBContext) {
|
export async function fetch(ctx: Ctx) {
|
||||||
const tenantId = ctx.user!.tenantId
|
const tenantId = ctx.user!.tenantId
|
||||||
// always use the dev apps as they'll be most up to date (true)
|
// always use the dev apps as they'll be most up to date (true)
|
||||||
const apps = (await dbCore.getAllApps({ tenantId, all: true })) as App[]
|
const apps = (await dbCore.getAllApps({ tenantId, all: true })) as App[]
|
||||||
|
@ -31,7 +31,7 @@ export async function fetch(ctx: BBContext) {
|
||||||
ctx.body = response
|
ctx.body = response
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function find(ctx: BBContext) {
|
export async function find(ctx: Ctx) {
|
||||||
const appId = ctx.params.appId
|
const appId = ctx.params.appId
|
||||||
await context.doInAppContext(dbCore.getDevAppID(appId), async () => {
|
await context.doInAppContext(dbCore.getDevAppID(appId), async () => {
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
|
@ -45,10 +45,10 @@ export async function find(ctx: BBContext) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeAppRole(ctx: BBContext) {
|
export async function removeAppRole(ctx: Ctx) {
|
||||||
const { appId } = ctx.params
|
const { appId } = ctx.params
|
||||||
const db = tenancy.getGlobalDB()
|
const db = tenancy.getGlobalDB()
|
||||||
const users = await allUsers()
|
const users = await sdk.users.db.allUsers()
|
||||||
const bulk = []
|
const bulk = []
|
||||||
const cacheInvalidations = []
|
const cacheInvalidations = []
|
||||||
for (let user of users) {
|
for (let user of users) {
|
||||||
|
|
|
@ -91,7 +91,7 @@ export async function getSelf(ctx: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the main body of the user
|
// get the main body of the user
|
||||||
const user = await userSdk.getUser(userId)
|
const user = await userSdk.db.getUser(userId)
|
||||||
ctx.body = await groups.enrichUserRolesFromGroups(user)
|
ctx.body = await groups.enrichUserRolesFromGroups(user)
|
||||||
|
|
||||||
// add the feature flags for this tenant
|
// add the feature flags for this tenant
|
||||||
|
@ -106,12 +106,12 @@ export async function updateSelf(
|
||||||
) {
|
) {
|
||||||
const update = ctx.request.body
|
const update = ctx.request.body
|
||||||
|
|
||||||
let user = await userSdk.getUser(ctx.user._id!)
|
let user = await userSdk.db.getUser(ctx.user._id!)
|
||||||
user = {
|
user = {
|
||||||
...user,
|
...user,
|
||||||
...update,
|
...update,
|
||||||
}
|
}
|
||||||
user = await userSdk.save(user, { requirePassword: false })
|
user = await userSdk.db.save(user, { requirePassword: false })
|
||||||
|
|
||||||
if (update.password) {
|
if (update.password) {
|
||||||
// Log all other sessions out apart from the current one
|
// Log all other sessions out apart from the current one
|
||||||
|
|
|
@ -41,7 +41,7 @@ export const save = async (ctx: UserCtx<User, SaveUserResponse>) => {
|
||||||
const currentUserId = ctx.user?._id
|
const currentUserId = ctx.user?._id
|
||||||
const requestUser = ctx.request.body
|
const requestUser = ctx.request.body
|
||||||
|
|
||||||
const user = await userSdk.save(requestUser, { currentUserId })
|
const user = await userSdk.db.save(requestUser, { currentUserId })
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
_id: user._id!,
|
_id: user._id!,
|
||||||
|
@ -57,7 +57,7 @@ const bulkDelete = async (userIds: string[], currentUserId: string) => {
|
||||||
if (userIds?.indexOf(currentUserId) !== -1) {
|
if (userIds?.indexOf(currentUserId) !== -1) {
|
||||||
throw new Error("Unable to delete self.")
|
throw new Error("Unable to delete self.")
|
||||||
}
|
}
|
||||||
return await userSdk.bulkDelete(userIds)
|
return await userSdk.db.bulkDelete(userIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
const bulkCreate = async (users: User[], groupIds: string[]) => {
|
const bulkCreate = async (users: User[], groupIds: string[]) => {
|
||||||
|
@ -66,7 +66,7 @@ const bulkCreate = async (users: User[], groupIds: string[]) => {
|
||||||
"Max limit for upload is 1000 users. Please reduce file size and try again."
|
"Max limit for upload is 1000 users. Please reduce file size and try again."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return await userSdk.bulkCreate(users, groupIds)
|
return await userSdk.db.bulkCreate(users, groupIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bulkUpdate = async (
|
export const bulkUpdate = async (
|
||||||
|
@ -141,7 +141,7 @@ export const adminUser = async (
|
||||||
// always bust checklist beforehand, if an error occurs but can proceed, don't get
|
// always bust checklist beforehand, if an error occurs but can proceed, don't get
|
||||||
// stuck in a cycle
|
// stuck in a cycle
|
||||||
await cache.bustCache(cache.CacheKey.CHECKLIST)
|
await cache.bustCache(cache.CacheKey.CHECKLIST)
|
||||||
const finalUser = await userSdk.save(user, {
|
const finalUser = await userSdk.db.save(user, {
|
||||||
hashPassword,
|
hashPassword,
|
||||||
requirePassword,
|
requirePassword,
|
||||||
})
|
})
|
||||||
|
@ -167,7 +167,7 @@ export const adminUser = async (
|
||||||
export const countByApp = async (ctx: any) => {
|
export const countByApp = async (ctx: any) => {
|
||||||
const appId = ctx.params.appId
|
const appId = ctx.params.appId
|
||||||
try {
|
try {
|
||||||
ctx.body = await userSdk.countUsersByApp(appId)
|
ctx.body = await userSdk.db.countUsersByApp(appId)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.throw(err.status || 400, err)
|
ctx.throw(err.status || 400, err)
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,7 @@ export const destroy = async (ctx: any) => {
|
||||||
ctx.throw(400, "Unable to delete self.")
|
ctx.throw(400, "Unable to delete self.")
|
||||||
}
|
}
|
||||||
|
|
||||||
await userSdk.destroy(id)
|
await userSdk.db.destroy(id)
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
message: `User ${id} deleted.`,
|
message: `User ${id} deleted.`,
|
||||||
|
@ -188,7 +188,7 @@ export const destroy = async (ctx: any) => {
|
||||||
|
|
||||||
export const getAppUsers = async (ctx: Ctx<SearchUsersRequest>) => {
|
export const getAppUsers = async (ctx: Ctx<SearchUsersRequest>) => {
|
||||||
const body = ctx.request.body
|
const body = ctx.request.body
|
||||||
const users = await userSdk.getUsersByAppAccess(body?.appId)
|
const users = await userSdk.db.getUsersByAppAccess(body?.appId)
|
||||||
|
|
||||||
ctx.body = { data: users }
|
ctx.body = { data: users }
|
||||||
}
|
}
|
||||||
|
@ -212,7 +212,7 @@ export const search = async (ctx: Ctx<SearchUsersRequest>) => {
|
||||||
|
|
||||||
// called internally by app server user fetch
|
// called internally by app server user fetch
|
||||||
export const fetch = async (ctx: any) => {
|
export const fetch = async (ctx: any) => {
|
||||||
const all = await userSdk.allUsers()
|
const all = await userSdk.db.allUsers()
|
||||||
// user hashed password shouldn't ever be returned
|
// user hashed password shouldn't ever be returned
|
||||||
for (let user of all) {
|
for (let user of all) {
|
||||||
if (user) {
|
if (user) {
|
||||||
|
@ -224,12 +224,12 @@ export const fetch = async (ctx: any) => {
|
||||||
|
|
||||||
// called internally by app server user find
|
// called internally by app server user find
|
||||||
export const find = async (ctx: any) => {
|
export const find = async (ctx: any) => {
|
||||||
ctx.body = await userSdk.getUser(ctx.params.id)
|
ctx.body = await userSdk.db.getUser(ctx.params.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tenantUserLookup = async (ctx: any) => {
|
export const tenantUserLookup = async (ctx: any) => {
|
||||||
const id = ctx.params.id
|
const id = ctx.params.id
|
||||||
const user = await userSdk.getPlatformUser(id)
|
const user = await userSdk.core.getPlatformUser(id)
|
||||||
if (user) {
|
if (user) {
|
||||||
ctx.body = user
|
ctx.body = user
|
||||||
} else {
|
} else {
|
||||||
|
@ -252,7 +252,7 @@ export const onboardUsers = async (ctx: Ctx<InviteUsersRequest>) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const { users, groups, roles } = request.create
|
const { users, groups, roles } = request.create
|
||||||
const assignUsers = users.map((user: User) => (user.roles = roles))
|
const assignUsers = users.map((user: User) => (user.roles = roles))
|
||||||
onboardingResponse = await userSdk.bulkCreate(assignUsers, groups)
|
onboardingResponse = await userSdk.db.bulkCreate(assignUsers, groups)
|
||||||
ctx.body = onboardingResponse
|
ctx.body = onboardingResponse
|
||||||
} else if (emailConfigured) {
|
} else if (emailConfigured) {
|
||||||
onboardingResponse = await inviteMultiple(ctx)
|
onboardingResponse = await inviteMultiple(ctx)
|
||||||
|
@ -277,7 +277,7 @@ export const onboardUsers = async (ctx: Ctx<InviteUsersRequest>) => {
|
||||||
tenantId: tenancy.getTenantId(),
|
tenantId: tenancy.getTenantId(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
let bulkCreateReponse = await userSdk.bulkCreate(users, [])
|
let bulkCreateReponse = await userSdk.db.bulkCreate(users, [])
|
||||||
|
|
||||||
// Apply temporary credentials
|
// Apply temporary credentials
|
||||||
let createWithCredentials = {
|
let createWithCredentials = {
|
||||||
|
@ -410,7 +410,7 @@ export const inviteAccept = async (
|
||||||
...info,
|
...info,
|
||||||
}
|
}
|
||||||
|
|
||||||
const saved = await userSdk.save(request)
|
const saved = await userSdk.db.save(request)
|
||||||
const db = tenancy.getGlobalDB()
|
const db = tenancy.getGlobalDB()
|
||||||
const user = await db.get<User>(saved._id)
|
const user = await db.get<User>(saved._id)
|
||||||
await events.user.inviteAccepted(user)
|
await events.user.inviteAccepted(user)
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { TestConfiguration, structures } from "../../../../tests"
|
||||||
|
import { mocks } from "@budibase/backend-core/tests"
|
||||||
|
import { User } from "@budibase/types"
|
||||||
|
|
||||||
|
const MOCK_APP_ID = "app_a"
|
||||||
|
|
||||||
|
describe("/api/global/users/:userId/app/builder", () => {
|
||||||
|
const config = new TestConfiguration()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await config.beforeAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await config.afterAll()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function newUser() {
|
||||||
|
const base = structures.users.user()
|
||||||
|
return await config.createUser(base)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUser(userId: string) {
|
||||||
|
const response = await config.api.users.getUser(userId)
|
||||||
|
return response.body as User
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Confirm pro license", () => {
|
||||||
|
it("should 400 with licensing", async () => {
|
||||||
|
const user = await newUser()
|
||||||
|
const resp = await config.api.users.grantBuilderToApp(
|
||||||
|
user._id!,
|
||||||
|
MOCK_APP_ID,
|
||||||
|
400
|
||||||
|
)
|
||||||
|
expect(resp.body.message).toContain("Feature not enabled")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("PATCH /api/global/users/:userId/app/:appId/builder", () => {
|
||||||
|
it("should be able to grant a user access to a particular app", async () => {
|
||||||
|
mocks.licenses.useAppBuilders()
|
||||||
|
const user = await newUser()
|
||||||
|
await config.api.users.grantBuilderToApp(user._id!, MOCK_APP_ID)
|
||||||
|
const updated = await getUser(user._id!)
|
||||||
|
expect(updated.builder?.apps![0]).toBe(MOCK_APP_ID)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("DELETE /api/global/users/:userId/app/:appId/builder", () => {
|
||||||
|
it("should allow revoking access", async () => {
|
||||||
|
mocks.licenses.useAppBuilders()
|
||||||
|
const user = await newUser()
|
||||||
|
await config.api.users.grantBuilderToApp(user._id!, MOCK_APP_ID)
|
||||||
|
let updated = await getUser(user._id!)
|
||||||
|
expect(updated.builder?.apps![0]).toBe(MOCK_APP_ID)
|
||||||
|
await config.api.users.revokeBuilderFromApp(user._id!, MOCK_APP_ID)
|
||||||
|
updated = await getUser(user._id!)
|
||||||
|
expect(updated.builder?.apps!.length).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -41,7 +41,7 @@ describe("/api/global/auth", () => {
|
||||||
|
|
||||||
async function createSSOUser() {
|
async function createSSOUser() {
|
||||||
return config.doInTenant(async () => {
|
return config.doInTenant(async () => {
|
||||||
return userSdk.save(structures.users.ssoUser(), {
|
return userSdk.db.save(structures.users.ssoUser(), {
|
||||||
requirePassword: false,
|
requirePassword: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -245,7 +245,7 @@ describe("/api/global/auth", () => {
|
||||||
const ssoUser = user as SSOUser
|
const ssoUser = user as SSOUser
|
||||||
ssoUser.providerType = structures.sso.providerType()
|
ssoUser.providerType = structures.sso.providerType()
|
||||||
delete ssoUser.password
|
delete ssoUser.password
|
||||||
await config.doInTenant(() => userSdk.save(ssoUser))
|
await config.doInTenant(() => userSdk.db.save(ssoUser))
|
||||||
|
|
||||||
await testSSOUser(code!)
|
await testSSOUser(code!)
|
||||||
})
|
})
|
||||||
|
|
|
@ -480,7 +480,7 @@ describe("/api/global/users", () => {
|
||||||
function createSSOUser() {
|
function createSSOUser() {
|
||||||
return config.doInTenant(() => {
|
return config.doInTenant(() => {
|
||||||
const user = structures.users.ssoUser()
|
const user = structures.users.ssoUser()
|
||||||
return userSdk.save(user, { requirePassword: false })
|
return userSdk.db.save(user, { requirePassword: false })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
const { join } = require("path")
|
import { env as coreEnv } from "@budibase/backend-core"
|
||||||
|
import { ServiceType } from "@budibase/types"
|
||||||
|
import { join } from "path"
|
||||||
|
|
||||||
|
coreEnv._set("SERVICE_TYPE", ServiceType.WORKER)
|
||||||
|
|
||||||
function isDev() {
|
function isDev() {
|
||||||
return process.env.NODE_ENV !== "production"
|
return process.env.NODE_ENV !== "production"
|
||||||
|
|
|
@ -2,12 +2,5 @@ import { sdk as proSdk } from "@budibase/pro"
|
||||||
import * as userSdk from "./sdk/users"
|
import * as userSdk from "./sdk/users"
|
||||||
|
|
||||||
export const initPro = async () => {
|
export const initPro = async () => {
|
||||||
await proSdk.init({
|
await proSdk.init({})
|
||||||
scimUserServiceConfig: {
|
|
||||||
functions: {
|
|
||||||
saveUser: userSdk.save,
|
|
||||||
removeUser: (id: string) => userSdk.destroy(id),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { platform } from "@budibase/backend-core"
|
||||||
* Re-sync the global-db users to the global-info db users
|
* Re-sync the global-db users to the global-info db users
|
||||||
*/
|
*/
|
||||||
export const run = async (globalDb: any) => {
|
export const run = async (globalDb: any) => {
|
||||||
const users = (await usersSdk.allUsers()) as User[]
|
const users = (await usersSdk.db.allUsers()) as User[]
|
||||||
const promises = []
|
const promises = []
|
||||||
for (let user of users) {
|
for (let user of users) {
|
||||||
promises.push(
|
promises.push(
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import {
|
import {
|
||||||
auth as authCore,
|
auth as authCore,
|
||||||
tenancy,
|
env as coreEnv,
|
||||||
utils as coreUtils,
|
|
||||||
sessions,
|
|
||||||
events,
|
events,
|
||||||
HTTPError,
|
HTTPError,
|
||||||
env as coreEnv,
|
sessions,
|
||||||
|
tenancy,
|
||||||
|
utils as coreUtils,
|
||||||
} from "@budibase/backend-core"
|
} from "@budibase/backend-core"
|
||||||
import { PlatformLogoutOpts, User } from "@budibase/types"
|
import { PlatformLogoutOpts, User } from "@budibase/types"
|
||||||
import jwt from "jsonwebtoken"
|
import jwt from "jsonwebtoken"
|
||||||
|
@ -20,7 +20,7 @@ export async function loginUser(user: User) {
|
||||||
const sessionId = coreUtils.newid()
|
const sessionId = coreUtils.newid()
|
||||||
const tenantId = tenancy.getTenantId()
|
const tenantId = tenancy.getTenantId()
|
||||||
await sessions.createASession(user._id!, { sessionId, tenantId })
|
await sessions.createASession(user._id!, { sessionId, tenantId })
|
||||||
const token = jwt.sign(
|
return jwt.sign(
|
||||||
{
|
{
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
sessionId,
|
sessionId,
|
||||||
|
@ -28,7 +28,6 @@ export async function loginUser(user: User) {
|
||||||
},
|
},
|
||||||
coreEnv.JWT_SECRET!
|
coreEnv.JWT_SECRET!
|
||||||
)
|
)
|
||||||
return token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout(opts: PlatformLogoutOpts) {
|
export async function logout(opts: PlatformLogoutOpts) {
|
||||||
|
@ -58,7 +57,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,9 +75,9 @@ export const reset = async (email: string) => {
|
||||||
export const resetUpdate = async (resetCode: string, password: string) => {
|
export const resetUpdate = async (resetCode: string, password: string) => {
|
||||||
const { userId } = await redis.checkResetPasswordCode(resetCode)
|
const { userId } = await redis.checkResetPasswordCode(resetCode)
|
||||||
|
|
||||||
let user = await userSdk.getUser(userId)
|
let user = await userSdk.db.getUser(userId)
|
||||||
user.password = password
|
user.password = password
|
||||||
user = await userSdk.save(user)
|
user = await userSdk.db.save(user)
|
||||||
|
|
||||||
// remove password from the user before sending events
|
// remove password from the user before sending events
|
||||||
delete user.password
|
delete user.password
|
||||||
|
|
|
@ -1,2 +1,7 @@
|
||||||
export * from "./users"
|
export * 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
|
||||||
|
users.UserDB.init(pro.quotas, pro.groups, pro.features)
|
||||||
|
export const db = users.UserDB
|
||||||
export { users as core } from "@budibase/backend-core"
|
export { users as core } from "@budibase/backend-core"
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
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"
|
||||||
import { isPreventPasswordActions } from "../users"
|
|
||||||
|
|
||||||
describe("users", () => {
|
describe("users", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -13,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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -24,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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -34,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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -42,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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -52,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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -70,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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -80,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,602 +1,19 @@
|
||||||
import env from "../../environment"
|
import { events, tenancy, users as usersCore } from "@budibase/backend-core"
|
||||||
import * as eventHelpers from "./events"
|
import { InviteUsersRequest, InviteUsersResponse } from "@budibase/types"
|
||||||
import {
|
|
||||||
accounts,
|
|
||||||
cache,
|
|
||||||
constants,
|
|
||||||
db as dbUtils,
|
|
||||||
events,
|
|
||||||
HTTPError,
|
|
||||||
sessions,
|
|
||||||
tenancy,
|
|
||||||
platform,
|
|
||||||
users as usersCore,
|
|
||||||
utils,
|
|
||||||
ViewName,
|
|
||||||
env as coreEnv,
|
|
||||||
context,
|
|
||||||
EmailUnavailableError,
|
|
||||||
} from "@budibase/backend-core"
|
|
||||||
import {
|
|
||||||
AccountMetadata,
|
|
||||||
AllDocsResponse,
|
|
||||||
CloudAccount,
|
|
||||||
InviteUsersRequest,
|
|
||||||
InviteUsersResponse,
|
|
||||||
isSSOAccount,
|
|
||||||
isSSOUser,
|
|
||||||
PlatformUser,
|
|
||||||
PlatformUserByEmail,
|
|
||||||
RowResponse,
|
|
||||||
User,
|
|
||||||
SaveUserOpts,
|
|
||||||
BulkUserCreated,
|
|
||||||
BulkUserDeleted,
|
|
||||||
Account,
|
|
||||||
} 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"
|
|
||||||
import * as accountSdk from "../accounts"
|
|
||||||
|
|
||||||
export const allUsers = async () => {
|
export async function invite(
|
||||||
const db = tenancy.getGlobalDB()
|
|
||||||
const response = await db.allDocs(
|
|
||||||
dbUtils.getGlobalUserParams(null, {
|
|
||||||
include_docs: true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return response.rows.map((row: any) => row.doc)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const countUsersByApp = async (appId: string) => {
|
|
||||||
let response: any = await usersCore.searchGlobalUsersByApp(appId, {})
|
|
||||||
return {
|
|
||||||
userCount: response.length,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getUsersByAppAccess = async (appId?: string) => {
|
|
||||||
const opts: any = {
|
|
||||||
include_docs: true,
|
|
||||||
limit: 50,
|
|
||||||
}
|
|
||||||
let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
|
|
||||||
appId,
|
|
||||||
opts
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUserByEmail(email: string) {
|
|
||||||
return usersCore.getGlobalUserByEmail(email)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a user by ID from the global database, based on the current tenancy.
|
|
||||||
*/
|
|
||||||
export const getUser = async (userId: string) => {
|
|
||||||
const user = await usersCore.getById(userId)
|
|
||||||
if (user) {
|
|
||||||
delete user.password
|
|
||||||
}
|
|
||||||
return user
|
|
||||||
}
|
|
||||||
|
|
||||||
const buildUser = async (
|
|
||||||
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 = constants.UserStatus.ACTIVE
|
|
||||||
}
|
|
||||||
|
|
||||||
return fullUser
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookup, could be email or userId, either will return a doc
|
|
||||||
export const getPlatformUser = async (
|
|
||||||
identifier: string
|
|
||||||
): Promise<PlatformUser | null> => {
|
|
||||||
// use the view here and allow to find anyone regardless of casing
|
|
||||||
// Use lowercase to ensure email login is case insensitive
|
|
||||||
const response = dbUtils.queryPlatformView(
|
|
||||||
ViewName.PLATFORM_USERS_LOWERCASE,
|
|
||||||
{
|
|
||||||
keys: [identifier.toLowerCase()],
|
|
||||||
include_docs: true,
|
|
||||||
}
|
|
||||||
) as Promise<PlatformUser>
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateUniqueUser = async (email: string, tenantId: string) => {
|
|
||||||
// check budibase users in other tenants
|
|
||||||
if (env.MULTI_TENANCY) {
|
|
||||||
const tenantUser = await getPlatformUser(email)
|
|
||||||
if (tenantUser != null && tenantUser.tenantId !== tenantId) {
|
|
||||||
throw new EmailUnavailableError(email)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check root account users in account portal
|
|
||||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
||||||
const account = await accounts.getAccount(email)
|
|
||||||
if (account && account.verified && account.tenantId !== tenantId) {
|
|
||||||
throw new EmailUnavailableError(email)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 && user.admin?.global) {
|
|
||||||
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.api.getAccountByTenantId(tenancy.getTenantId())
|
|
||||||
}
|
|
||||||
return !!(account && account.email === user.email && isSSOAccount(account))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: The single save should re-use the bulk insert with a single
|
|
||||||
// user so that we don't need to duplicate logic
|
|
||||||
export const save = async (
|
|
||||||
user: User,
|
|
||||||
opts: SaveUserOpts = {}
|
|
||||||
): Promise<User> => {
|
|
||||||
// default booleans to true
|
|
||||||
if (opts.hashPassword == null) {
|
|
||||||
opts.hashPassword = true
|
|
||||||
}
|
|
||||||
if (opts.requirePassword == null) {
|
|
||||||
opts.requirePassword = true
|
|
||||||
}
|
|
||||||
const tenantId = tenancy.getTenantId()
|
|
||||||
const db = tenancy.getGlobalDB()
|
|
||||||
|
|
||||||
let { email, _id, userGroups = [], roles } = user
|
|
||||||
|
|
||||||
if (!email && !_id) {
|
|
||||||
throw new Error("_id or email is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
let dbUser: User | undefined
|
|
||||||
if (_id) {
|
|
||||||
// try to get existing user from db
|
|
||||||
try {
|
|
||||||
dbUser = (await db.get(_id)) as User
|
|
||||||
if (email && dbUser.email !== email) {
|
|
||||||
throw "Email address cannot be changed"
|
|
||||||
}
|
|
||||||
email = dbUser.email
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.status === 404) {
|
|
||||||
// do nothing, save this new user with the id specified - required for SSO auth
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dbUser && email) {
|
|
||||||
// no id was specified - load from email instead
|
|
||||||
dbUser = await usersCore.getGlobalUserByEmail(email)
|
|
||||||
if (dbUser && dbUser._id !== _id) {
|
|
||||||
throw new EmailUnavailableError(email)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const change = dbUser ? 0 : 1 // no change if there is existing user
|
|
||||||
return pro.quotas.addUsers(change, async () => {
|
|
||||||
await validateUniqueUser(email, tenantId)
|
|
||||||
|
|
||||||
let builtUser = await buildUser(user, opts, tenantId, dbUser)
|
|
||||||
// don't allow a user to update its own roles/perms
|
|
||||||
if (opts.currentUserId && opts.currentUserId === dbUser?._id) {
|
|
||||||
builtUser.builder = dbUser.builder
|
|
||||||
builtUser.admin = dbUser.admin
|
|
||||||
builtUser.roles = dbUser.roles
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dbUser && roles?.length) {
|
|
||||||
builtUser.roles = { ...roles }
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure we set the _id field for a new user
|
|
||||||
// Also if this is a new user, associate groups with them
|
|
||||||
let groupPromises = []
|
|
||||||
if (!_id) {
|
|
||||||
_id = builtUser._id!
|
|
||||||
|
|
||||||
if (userGroups.length > 0) {
|
|
||||||
for (let groupId of userGroups) {
|
|
||||||
groupPromises.push(pro.groups.addUsers(groupId, [_id]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// save the user to db
|
|
||||||
let response = await db.put(builtUser)
|
|
||||||
builtUser._rev = response.rev
|
|
||||||
|
|
||||||
await eventHelpers.handleSaveEvents(builtUser, dbUser)
|
|
||||||
await platform.users.addUser(tenantId, builtUser._id!, builtUser.email)
|
|
||||||
await cache.user.invalidateUser(response.id)
|
|
||||||
|
|
||||||
await Promise.all(groupPromises)
|
|
||||||
|
|
||||||
// finally returned the saved user from the db
|
|
||||||
return db.get(builtUser._id!)
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.status === 409) {
|
|
||||||
throw "User exists already"
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getExistingTenantUsers = async (emails: string[]): Promise<User[]> => {
|
|
||||||
const lcEmails = emails.map(email => email.toLowerCase())
|
|
||||||
const params = {
|
|
||||||
keys: lcEmails,
|
|
||||||
include_docs: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
arrayResponse: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbUtils.queryGlobalView(
|
|
||||||
ViewName.USER_BY_EMAIL,
|
|
||||||
params,
|
|
||||||
undefined,
|
|
||||||
opts
|
|
||||||
) as Promise<User[]>
|
|
||||||
}
|
|
||||||
|
|
||||||
const getExistingPlatformUsers = async (
|
|
||||||
emails: string[]
|
|
||||||
): Promise<PlatformUserByEmail[]> => {
|
|
||||||
const lcEmails = emails.map(email => email.toLowerCase())
|
|
||||||
const params = {
|
|
||||||
keys: lcEmails,
|
|
||||||
include_docs: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
arrayResponse: true,
|
|
||||||
}
|
|
||||||
return dbUtils.queryPlatformView(
|
|
||||||
ViewName.PLATFORM_USERS_LOWERCASE,
|
|
||||||
params,
|
|
||||||
opts
|
|
||||||
) as Promise<PlatformUserByEmail[]>
|
|
||||||
}
|
|
||||||
|
|
||||||
const getExistingAccounts = async (
|
|
||||||
emails: string[]
|
|
||||||
): Promise<AccountMetadata[]> => {
|
|
||||||
const lcEmails = emails.map(email => email.toLowerCase())
|
|
||||||
const params = {
|
|
||||||
keys: lcEmails,
|
|
||||||
include_docs: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
arrayResponse: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbUtils.queryPlatformView(
|
|
||||||
ViewName.ACCOUNT_BY_EMAIL,
|
|
||||||
params,
|
|
||||||
opts
|
|
||||||
) as Promise<AccountMetadata[]>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply a system-wide search on emails:
|
|
||||||
* - in tenant
|
|
||||||
* - cross tenant
|
|
||||||
* - accounts
|
|
||||||
* return an array of emails that match the supplied emails.
|
|
||||||
*/
|
|
||||||
const searchExistingEmails = async (emails: string[]) => {
|
|
||||||
let matchedEmails: string[] = []
|
|
||||||
|
|
||||||
const existingTenantUsers = await getExistingTenantUsers(emails)
|
|
||||||
matchedEmails.push(...existingTenantUsers.map(user => user.email))
|
|
||||||
|
|
||||||
const existingPlatformUsers = await getExistingPlatformUsers(emails)
|
|
||||||
matchedEmails.push(...existingPlatformUsers.map(user => user._id!))
|
|
||||||
|
|
||||||
const existingAccounts = await getExistingAccounts(emails)
|
|
||||||
matchedEmails.push(...existingAccounts.map(account => account.email))
|
|
||||||
|
|
||||||
return [...new Set(matchedEmails.map(email => email.toLowerCase()))]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const bulkCreate = async (
|
|
||||||
newUsersRequested: User[],
|
|
||||||
groups: string[]
|
|
||||||
): Promise<BulkUserCreated> => {
|
|
||||||
const tenantId = tenancy.getTenantId()
|
|
||||||
|
|
||||||
let usersToSave: any[] = []
|
|
||||||
let newUsers: any[] = []
|
|
||||||
|
|
||||||
const emails = newUsersRequested.map((user: User) => user.email)
|
|
||||||
const existingEmails = await searchExistingEmails(emails)
|
|
||||||
const unsuccessful: { email: string; reason: string }[] = []
|
|
||||||
|
|
||||||
for (const newUser of newUsersRequested) {
|
|
||||||
if (
|
|
||||||
newUsers.find(
|
|
||||||
(x: User) => x.email.toLowerCase() === newUser.email.toLowerCase()
|
|
||||||
) ||
|
|
||||||
existingEmails.includes(newUser.email.toLowerCase())
|
|
||||||
) {
|
|
||||||
unsuccessful.push({
|
|
||||||
email: newUser.email,
|
|
||||||
reason: `Unavailable`,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newUser.userGroups = groups
|
|
||||||
newUsers.push(newUser)
|
|
||||||
}
|
|
||||||
|
|
||||||
const account = await accountSdk.api.getAccountByTenantId(tenantId)
|
|
||||||
return pro.quotas.addUsers(newUsers.length, async () => {
|
|
||||||
// create the promises array that will be called by bulkDocs
|
|
||||||
newUsers.forEach((user: any) => {
|
|
||||||
usersToSave.push(
|
|
||||||
buildUser(
|
|
||||||
user,
|
|
||||||
{
|
|
||||||
hashPassword: true,
|
|
||||||
requirePassword: user.requirePassword,
|
|
||||||
},
|
|
||||||
tenantId,
|
|
||||||
undefined, // no dbUser
|
|
||||||
account
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const usersToBulkSave = await Promise.all(usersToSave)
|
|
||||||
await usersCore.bulkUpdateGlobalUsers(usersToBulkSave)
|
|
||||||
|
|
||||||
// Post-processing of bulk added users, e.g. events and cache operations
|
|
||||||
for (const user of usersToBulkSave) {
|
|
||||||
// TODO: Refactor to bulk insert users into the info db
|
|
||||||
// instead of relying on looping tenant creation
|
|
||||||
await platform.users.addUser(tenantId, user._id, user.email)
|
|
||||||
await eventHelpers.handleSaveEvents(user, undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
const saved = usersToBulkSave.map(user => {
|
|
||||||
return {
|
|
||||||
_id: user._id,
|
|
||||||
email: user.email,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// now update the groups
|
|
||||||
if (Array.isArray(saved) && groups) {
|
|
||||||
const groupPromises = []
|
|
||||||
const createdUserIds = saved.map(user => user._id)
|
|
||||||
for (let groupId of groups) {
|
|
||||||
groupPromises.push(pro.groups.addUsers(groupId, createdUserIds))
|
|
||||||
}
|
|
||||||
await Promise.all(groupPromises)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
successful: saved,
|
|
||||||
unsuccessful,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For the given user id's, return the account holder if it is in the ids.
|
|
||||||
*/
|
|
||||||
const getAccountHolderFromUserIds = async (
|
|
||||||
userIds: string[]
|
|
||||||
): Promise<CloudAccount | undefined> => {
|
|
||||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
||||||
const tenantId = tenancy.getTenantId()
|
|
||||||
const account = await accounts.getAccountByTenantId(tenantId)
|
|
||||||
if (!account) {
|
|
||||||
throw new Error(`Account not found for tenantId=${tenantId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const budibaseUserId = account.budibaseUserId
|
|
||||||
if (userIds.includes(budibaseUserId)) {
|
|
||||||
return account
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const bulkDelete = async (
|
|
||||||
userIds: string[]
|
|
||||||
): Promise<BulkUserDeleted> => {
|
|
||||||
const db = tenancy.getGlobalDB()
|
|
||||||
|
|
||||||
const response: BulkUserDeleted = {
|
|
||||||
successful: [],
|
|
||||||
unsuccessful: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove the account holder from the delete request if present
|
|
||||||
const account = await getAccountHolderFromUserIds(userIds)
|
|
||||||
if (account) {
|
|
||||||
userIds = userIds.filter(u => u !== account.budibaseUserId)
|
|
||||||
// mark user as unsuccessful
|
|
||||||
response.unsuccessful.push({
|
|
||||||
_id: account.budibaseUserId,
|
|
||||||
email: account.email,
|
|
||||||
reason: "Account holder cannot be deleted",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get users and delete
|
|
||||||
const allDocsResponse: AllDocsResponse<User> = await db.allDocs({
|
|
||||||
include_docs: true,
|
|
||||||
keys: userIds,
|
|
||||||
})
|
|
||||||
const usersToDelete: User[] = allDocsResponse.rows.map(
|
|
||||||
(user: RowResponse<User>) => {
|
|
||||||
return user.doc
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Delete from DB
|
|
||||||
const toDelete = usersToDelete.map(user => ({
|
|
||||||
...user,
|
|
||||||
_deleted: true,
|
|
||||||
}))
|
|
||||||
const dbResponse = await usersCore.bulkUpdateGlobalUsers(toDelete)
|
|
||||||
|
|
||||||
await pro.quotas.removeUsers(toDelete.length)
|
|
||||||
for (let user of usersToDelete) {
|
|
||||||
await bulkDeleteProcessing(user)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build Response
|
|
||||||
// index users by id
|
|
||||||
const userIndex: { [key: string]: User } = {}
|
|
||||||
usersToDelete.reduce((prev, current) => {
|
|
||||||
prev[current._id!] = current
|
|
||||||
return prev
|
|
||||||
}, userIndex)
|
|
||||||
|
|
||||||
// add the successful and unsuccessful users to response
|
|
||||||
dbResponse.forEach(item => {
|
|
||||||
const email = userIndex[item.id].email
|
|
||||||
if (item.ok) {
|
|
||||||
response.successful.push({ _id: item.id, email })
|
|
||||||
} else {
|
|
||||||
response.unsuccessful.push({
|
|
||||||
_id: item.id,
|
|
||||||
email,
|
|
||||||
reason: "Database error",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: The single delete should re-use the bulk delete with a single
|
|
||||||
// user so that we don't need to duplicate logic
|
|
||||||
export const destroy = async (id: string) => {
|
|
||||||
const db = tenancy.getGlobalDB()
|
|
||||||
const dbUser = (await db.get(id)) as User
|
|
||||||
const userId = dbUser._id as string
|
|
||||||
|
|
||||||
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
|
|
||||||
// root account holder can't be deleted from inside budibase
|
|
||||||
const email = dbUser.email
|
|
||||||
const account = await accounts.getAccount(email)
|
|
||||||
if (account) {
|
|
||||||
if (dbUser.userId === context.getIdentity()!._id) {
|
|
||||||
throw new HTTPError('Please visit "Account" to delete this user', 400)
|
|
||||||
} else {
|
|
||||||
throw new HTTPError("Account holder cannot be deleted", 400)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await platform.users.removeUser(dbUser)
|
|
||||||
|
|
||||||
await db.remove(userId, dbUser._rev)
|
|
||||||
|
|
||||||
await pro.quotas.removeUsers(1)
|
|
||||||
await eventHelpers.handleDeleteEvents(dbUser)
|
|
||||||
await cache.user.invalidateUser(userId)
|
|
||||||
await sessions.invalidateSessions(userId, { reason: "deletion" })
|
|
||||||
}
|
|
||||||
|
|
||||||
const bulkDeleteProcessing = async (dbUser: User) => {
|
|
||||||
const userId = dbUser._id as string
|
|
||||||
await platform.users.removeUser(dbUser)
|
|
||||||
await eventHelpers.handleDeleteEvents(dbUser)
|
|
||||||
await cache.user.invalidateUser(userId)
|
|
||||||
await sessions.invalidateSessions(userId, { reason: "bulk-deletion" })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const invite = async (
|
|
||||||
users: InviteUsersRequest
|
users: InviteUsersRequest
|
||||||
): Promise<InviteUsersResponse> => {
|
): Promise<InviteUsersResponse> {
|
||||||
const response: InviteUsersResponse = {
|
const response: InviteUsersResponse = {
|
||||||
successful: [],
|
successful: [],
|
||||||
unsuccessful: [],
|
unsuccessful: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const matchedEmails = await searchExistingEmails(users.map(u => u.email))
|
const matchedEmails = await usersCore.searchExistingEmails(
|
||||||
|
users.map(u => u.email)
|
||||||
|
)
|
||||||
const newUsers = []
|
const newUsers = []
|
||||||
|
|
||||||
// separate duplicates from new users
|
// separate duplicates from new users
|
||||||
|
|
|
@ -251,9 +251,9 @@ class TestConfiguration {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUser(email: string): Promise<User | undefined> {
|
async getUser(email: string): Promise<User> {
|
||||||
return context.doInTenant(this.getTenantId(), () => {
|
return context.doInTenant(this.getTenantId(), async () => {
|
||||||
return users.getGlobalUserByEmail(email)
|
return (await users.getGlobalUserByEmail(email)) as User
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,7 +263,7 @@ class TestConfiguration {
|
||||||
}
|
}
|
||||||
const response = await this._req(user, null, controllers.users.save)
|
const response = await this._req(user, null, controllers.users.save)
|
||||||
const body = response as SaveUserResponse
|
const body = response as SaveUserResponse
|
||||||
return this.getUser(body.email) as Promise<User>
|
return (await this.getUser(body.email)) as User
|
||||||
}
|
}
|
||||||
|
|
||||||
// CONFIGS
|
// CONFIGS
|
||||||
|
|
|
@ -140,4 +140,24 @@ export class UserAPI extends TestAPI {
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(opts?.status ? opts.status : 200)
|
.expect(opts?.status ? opts.status : 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
grantBuilderToApp = (
|
||||||
|
userId: string,
|
||||||
|
appId: string,
|
||||||
|
statusCode: number = 200
|
||||||
|
) => {
|
||||||
|
return this.request
|
||||||
|
.post(`/api/global/users/${userId}/app/${appId}/builder`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
revokeBuilderFromApp = (userId: string, appId: string) => {
|
||||||
|
return this.request
|
||||||
|
.delete(`/api/global/users/${userId}/app/${appId}/builder`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(200)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue