diff --git a/packages/backend-core/src/context/index.js b/packages/backend-core/src/context/index.js index ae9c7b92ea..7c7c5c45e6 100644 --- a/packages/backend-core/src/context/index.js +++ b/packages/backend-core/src/context/index.js @@ -76,17 +76,10 @@ exports.isMultiTenant = () => { exports.doInTenant = (tenantId, task) => { // the internal function is so that we can re-use an existing // context - don't want to close DB on a parent context - async function internal(opts = { existing: false, user: undefined }) { - // preserve the user - if (user) { - exports.setUser(user) - } + async function internal(opts = { existing: false }) { // set the tenant id if (!opts.existing) { - cls.setOnContext(ContextKeys.TENANT_ID, tenantId) - if (env.USE_COUCH) { - exports.setGlobalDB(tenantId) - } + exports.updateTenantId(tenantId) } try { @@ -102,15 +95,14 @@ exports.doInTenant = (tenantId, task) => { } } - const user = cls.getFromContext(ContextKeys.USER) const using = cls.getFromContext(ContextKeys.IN_USE) if (using && cls.getFromContext(ContextKeys.TENANT_ID) === tenantId) { cls.setOnContext(ContextKeys.IN_USE, using + 1) - return internal({ existing: true, user }) + return internal({ existing: true }) } else { return cls.run(async () => { cls.setOnContext(ContextKeys.IN_USE, 1) - return internal({ existing: false, user }) + return internal() }) } } @@ -146,19 +138,19 @@ exports.doInAppContext = (appId, task) => { throw new Error("appId is required") } + const user = exports.getUser() + // the internal function is so that we can re-use an existing // context - don't want to close DB on a parent context async function internal(opts = { existing: false, user: undefined }) { - // preserve the user - if (user) { - exports.setUser(user) - } // set the app tenant id if (!opts.existing) { setAppTenantId(appId) } // set the app ID cls.setOnContext(ContextKeys.APP_ID, appId) + // preserve the user + exports.setUser(user) try { // invoke the task return await task() @@ -171,29 +163,56 @@ exports.doInAppContext = (appId, task) => { } } } - const user = cls.getFromContext(ContextKeys.USER) const using = cls.getFromContext(ContextKeys.IN_USE) if (using && cls.getFromContext(ContextKeys.APP_ID) === appId) { cls.setOnContext(ContextKeys.IN_USE, using + 1) - return internal({ existing: true, user }) + return internal({ existing: true }) } else { return cls.run(async () => { cls.setOnContext(ContextKeys.IN_USE, 1) - return internal({ existing: false, user }) + return internal() }) } } exports.doInUserContext = (user, task) => { - return cls.run(() => { - let tenantId = user.tenantId - if (!tenantId) { - tenantId = exports.getTenantId() + if (!user) { + throw new Error("user is required") + } + + async function internal(opts = { existing: false }) { + if (!opts.existing) { + cls.setOnContext(ContextKeys.USER, user) + // set the tenant so that doInTenant will preserve user + if (user.tenantId) { + exports.updateTenantId(user.tenantId) + } } - cls.setOnContext(ContextKeys.TENANT_ID, tenantId) - exports.setUser(user) - return task() - }) + + try { + // invoke the task + return await task() + } finally { + const using = cls.getFromContext(ContextKeys.IN_USE) + if (!using || using <= 1) { + exports.setUser(null) + } else { + cls.setOnContext(using - 1) + } + } + } + + const existing = cls.getFromContext(ContextKeys.USER) + const using = cls.getFromContext(ContextKeys.IN_USE) + if (using && existing && existing._id === user._id) { + cls.setOnContext(ContextKeys.IN_USE, using + 1) + return internal({ existing: true }) + } else { + return cls.run(async () => { + cls.setOnContext(ContextKeys.IN_USE, 1) + return internal({ existing: false }) + }) + } } exports.setUser = user => { @@ -202,8 +221,7 @@ exports.setUser = user => { exports.getUser = () => { try { - const user = cls.getFromContext(ContextKeys.USER) - return user + return cls.getFromContext(ContextKeys.USER) } catch (e) { // do nothing - user is not in context } @@ -211,7 +229,9 @@ exports.getUser = () => { exports.updateTenantId = tenantId => { cls.setOnContext(ContextKeys.TENANT_ID, tenantId) - exports.setGlobalDB(tenantId) + if (env.USE_COUCH) { + exports.setGlobalDB(tenantId) + } } exports.updateAppId = async appId => { diff --git a/packages/backend-core/src/events/events.ts b/packages/backend-core/src/events/events.ts index 21dfba6090..4ae90be745 100644 --- a/packages/backend-core/src/events/events.ts +++ b/packages/backend-core/src/events/events.ts @@ -4,6 +4,6 @@ import * as identification from "./identification" export const publishEvent = async (event: Event, properties: any) => { // in future this should use async events via a distributed queue. - const identity = identification.getCurrentIdentity() + const identity = await identification.getCurrentIdentity() await processors.processEvent(event, identity, properties) } diff --git a/packages/backend-core/src/events/identification.ts b/packages/backend-core/src/events/identification.ts index 0b7e1f12f9..d22cfe7720 100644 --- a/packages/backend-core/src/events/identification.ts +++ b/packages/backend-core/src/events/identification.ts @@ -11,20 +11,25 @@ import { BudibaseIdentity, isCloudAccount, isSSOAccount, + TenantIdentity, + SettingsConfig, } from "@budibase/types" import { analyticsProcessor } from "./processors" +import * as dbUtils from "../db/utils" +import { Configs } from "../constants" +import * as hashing from "../hashing" -export const getCurrentIdentity = (): Identity => { +export const getCurrentIdentity = async (): Promise => { const user: SessionUser | undefined = context.getUser() - const tenantId = context.getTenantId() + let tenantId = context.getTenantId() let id: string if (user) { id = user._id - } else if (env.SELF_HOSTED) { - id = "installationId" // TODO } else { - id = tenantId + const global = await getGlobalIdentifiers(tenantId) + id = global.id + tenantId = global.tenantId } return { @@ -33,6 +38,55 @@ export const getCurrentIdentity = (): Identity => { } } +const getGlobalId = async (): Promise => { + const db = context.getGlobalDB() + const config: SettingsConfig = await dbUtils.getScopedFullConfig(db, { + type: Configs.SETTINGS, + }) + if (config.config.globalId) { + return config.config.globalId + } else { + const globalId = `global_${hashing.newid()}` + config.config.globalId = globalId + await db.put(config) + return globalId + } +} + +const getGlobalIdentifiers = async ( + tenantId: string +): Promise<{ id: string; tenantId: string }> => { + if (env.SELF_HOSTED) { + const globalId = await getGlobalId() + return { + id: globalId, + tenantId: `${globalId}-${tenantId}`, + } + } else { + // tenant id's in the cloud are already unique + return { + id: tenantId, + tenantId: tenantId, + } + } +} + +const getHostingFromEnv = () => { + return env.SELF_HOSTED ? Hosting.SELF : Hosting.CLOUD +} + +export const identifyTenant = async (tenantId: string) => { + const global = await getGlobalIdentifiers(tenantId) + + const identity: TenantIdentity = { + id: global.id, + tenantId: global.tenantId, + hosting: getHostingFromEnv(), + type: IdentityType.TENANT, + } + await identify(identity) +} + export const identifyUser = async (user: User) => { const id = user._id as string const tenantId = user.tenantId @@ -40,7 +94,7 @@ export const identifyUser = async (user: User) => { const type = IdentityType.USER let builder = user.builder?.global let admin = user.admin?.global - let authType = user.providerType ? user.providerType : "password" + let providerType = user.providerType const identity: BudibaseIdentity = { id, @@ -49,7 +103,7 @@ export const identifyUser = async (user: User) => { type, builder, admin, - authType, + providerType, } await identify(identity) @@ -60,9 +114,7 @@ export const identifyAccount = async (account: Account) => { const tenantId = account.tenantId const hosting = account.hosting let type = IdentityType.ACCOUNT - let authType = isSSOAccount(account) - ? (account.providerType as string) - : "password" + let providerType = isSSOAccount(account) ? account.providerType : undefined if (isCloudAccount(account)) { if (account.budibaseUserId) { @@ -77,7 +129,7 @@ export const identifyAccount = async (account: Account) => { tenantId, hosting, type, - authType, + providerType, verified: account.verified, profession: account.profession, companySize: account.size, diff --git a/packages/backend-core/src/events/index.ts b/packages/backend-core/src/events/index.ts index e850ea8b26..ae5b66023d 100644 --- a/packages/backend-core/src/events/index.ts +++ b/packages/backend-core/src/events/index.ts @@ -1,6 +1,7 @@ import { processors } from "./processors" export * from "./publishers" export * as analytics from "./analytics" +export * as identification from "./identification" export const shutdown = () => { processors.shutdown() diff --git a/packages/backend-core/src/middleware/passport/third-party-common.js b/packages/backend-core/src/middleware/passport/third-party-common.js index 44d80f7ee7..845362200f 100644 --- a/packages/backend-core/src/middleware/passport/third-party-common.js +++ b/packages/backend-core/src/middleware/passport/third-party-common.js @@ -79,14 +79,14 @@ exports.authenticateThirdParty = async function ( dbUser.forceResetPassword = false // create or sync the user - let response try { - response = await saveUserFn(dbUser, false, false) + await saveUserFn(dbUser, false, false) } catch (err) { return authError(done, err) } - dbUser._rev = response.rev + // now that we're sure user exists, load them from the db + dbUser = await users.getGlobalUserByEmail(thirdPartyUser.email) // authenticate const sessionId = newid() diff --git a/packages/backend-core/tsconfig.build.json b/packages/backend-core/tsconfig.build.json index b5f8607023..602d4b3a7d 100644 --- a/packages/backend-core/tsconfig.build.json +++ b/packages/backend-core/tsconfig.build.json @@ -3,6 +3,7 @@ "extends": "./tsconfig.json", "exclude": [ "node_modules", + "dist/**/*", "**/*.spec.js", "**/*.spec.ts" ] diff --git a/packages/backend-core/tsconfig.json b/packages/backend-core/tsconfig.json index 5f9000c18f..373b8440f7 100644 --- a/packages/backend-core/tsconfig.json +++ b/packages/backend-core/tsconfig.json @@ -29,6 +29,7 @@ ], "exclude": [ "node_modules", + "dist/**/*", "**/*.spec.js", // "**/*.spec.ts" // don't exclude spec.ts files for editor support ] diff --git a/packages/types/src/documents/global/config.ts b/packages/types/src/documents/global/config.ts index b37e09bb9d..9ad80e4eef 100644 --- a/packages/types/src/documents/global/config.ts +++ b/packages/types/src/documents/global/config.ts @@ -19,6 +19,7 @@ export interface SettingsConfig extends Config { company: string logoUrl: string platformUrl: string + globalId?: string } } diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index 1cf3025ed9..61a942fdb8 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -10,6 +10,9 @@ export interface User extends Document { } providerType?: string tenantId: string + email: string + password?: string + status?: string } export interface UserRoles { diff --git a/packages/types/src/events/identification.ts b/packages/types/src/events/identification.ts index 0429882ecd..e35167d00f 100644 --- a/packages/types/src/events/identification.ts +++ b/packages/types/src/events/identification.ts @@ -11,10 +11,15 @@ export interface Identity { tenantId: string } -export interface UserIdentity extends Identity { +export interface TenantIdentity extends Identity { hosting: Hosting type: IdentityType - authType: string +} + +export interface UserIdentity extends TenantIdentity { + hosting: Hosting + type: IdentityType + providerType?: string } export interface BudibaseIdentity extends UserIdentity { diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index acd5bf8c89..d0a96b5362 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -2,13 +2,15 @@ import { EmailTemplatePurpose } from "../../../constants" import { checkInviteCode } from "../../../utilities/redis" import { sendEmail } from "../../../utilities/email" import { users } from "../../../sdk" +import { User } from "@budibase/types" +import { events } from "@budibase/backend-core" +import { getGlobalDB } from "@budibase/backend-core/dist/src/context" const { errors, users: usersCore, tenancy, db: dbUtils, - events, } = require("@budibase/backend-core") export const save = async (ctx: any) => { @@ -48,10 +50,9 @@ export const adminUser = async (ctx: any) => { ctx.throw(403, "You cannot initialise once a global user has been created.") } - const user = { + const user: User = { email: email, password: password, - createdAt: Date.now(), roles: {}, builder: { global: true, @@ -65,6 +66,7 @@ export const adminUser = async (ctx: any) => { ctx.body = await tenancy.doInTenant(tenantId, async () => { return users.save(user, hashPassword, requirePassword) }) + await events.identification.identifyTenant(tenantId) } catch (err: any) { ctx.throw(err.status || 400, err) } @@ -132,15 +134,17 @@ export const inviteAccept = async (ctx: any) => { // info is an extension of the user object that was stored by global const { email, info }: any = await checkInviteCode(inviteCode) ctx.body = await tenancy.doInTenant(info.tenantId, async () => { - const user = await users.save({ + const saved = await users.save({ firstName, lastName, password, email, ...info, }) + const db = getGlobalDB() + const user = await db.get(saved._id) await events.user.inviteAccepted(user) - return user + return saved }) } catch (err: any) { if (err.code === errors.codes.USAGE_LIMIT_EXCEEDED) { diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 0d29dfd190..b9218b1dea 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -2,6 +2,7 @@ import env from "../../environment" import { quotas } from "@budibase/pro" import * as apps from "../../utilities/appService" import * as eventHelpers from "./events" +import { User } from "@budibase/types" const { tenancy, @@ -16,6 +17,8 @@ const { HTTPError, } = require("@budibase/backend-core") +import { events } from "@budibase/backend-core" + /** * Retrieves all users from the current tenancy. */ @@ -51,7 +54,7 @@ export const getUser = async (userId: string) => { } export const save = async ( - user: any, + user: User, hashPassword = true, requirePassword = true ) => { @@ -97,7 +100,7 @@ export const save = async ( } if (!_id) { - _id = dbUtils.generateGlobalUserID(email) + _id = dbUtils.generateGlobalUserID() } user = { @@ -130,7 +133,7 @@ export const save = async ( user._rev = response.rev await eventHelpers.handleSaveEvents(user, dbUser) - + await events.identification.identifyUser(user) await tenancy.tryAddTenant(tenantId, _id, email) await cache.user.invalidateUser(response.id) // let server know to sync user