Identity updates

This commit is contained in:
Rory Powell 2022-05-24 20:01:13 +01:00
parent ff48aaec6e
commit b3f04e7e8f
12 changed files with 146 additions and 55 deletions

View File

@ -76,17 +76,10 @@ exports.isMultiTenant = () => {
exports.doInTenant = (tenantId, task) => { exports.doInTenant = (tenantId, task) => {
// the internal function is so that we can re-use an existing // the internal function is so that we can re-use an existing
// context - don't want to close DB on a parent context // context - don't want to close DB on a parent context
async function internal(opts = { existing: false, user: undefined }) { async function internal(opts = { existing: false }) {
// preserve the user
if (user) {
exports.setUser(user)
}
// set the tenant id // set the tenant id
if (!opts.existing) { if (!opts.existing) {
cls.setOnContext(ContextKeys.TENANT_ID, tenantId) exports.updateTenantId(tenantId)
if (env.USE_COUCH) {
exports.setGlobalDB(tenantId)
}
} }
try { try {
@ -102,15 +95,14 @@ exports.doInTenant = (tenantId, task) => {
} }
} }
const user = cls.getFromContext(ContextKeys.USER)
const using = cls.getFromContext(ContextKeys.IN_USE) const using = cls.getFromContext(ContextKeys.IN_USE)
if (using && cls.getFromContext(ContextKeys.TENANT_ID) === tenantId) { if (using && cls.getFromContext(ContextKeys.TENANT_ID) === tenantId) {
cls.setOnContext(ContextKeys.IN_USE, using + 1) cls.setOnContext(ContextKeys.IN_USE, using + 1)
return internal({ existing: true, user }) return internal({ existing: true })
} else { } else {
return cls.run(async () => { return cls.run(async () => {
cls.setOnContext(ContextKeys.IN_USE, 1) 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") throw new Error("appId is required")
} }
const user = exports.getUser()
// the internal function is so that we can re-use an existing // the internal function is so that we can re-use an existing
// context - don't want to close DB on a parent context // context - don't want to close DB on a parent context
async function internal(opts = { existing: false, user: undefined }) { async function internal(opts = { existing: false, user: undefined }) {
// preserve the user
if (user) {
exports.setUser(user)
}
// set the app tenant id // set the app tenant id
if (!opts.existing) { if (!opts.existing) {
setAppTenantId(appId) setAppTenantId(appId)
} }
// set the app ID // set the app ID
cls.setOnContext(ContextKeys.APP_ID, appId) cls.setOnContext(ContextKeys.APP_ID, appId)
// preserve the user
exports.setUser(user)
try { try {
// invoke the task // invoke the task
return await 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) const using = cls.getFromContext(ContextKeys.IN_USE)
if (using && cls.getFromContext(ContextKeys.APP_ID) === appId) { if (using && cls.getFromContext(ContextKeys.APP_ID) === appId) {
cls.setOnContext(ContextKeys.IN_USE, using + 1) cls.setOnContext(ContextKeys.IN_USE, using + 1)
return internal({ existing: true, user }) return internal({ existing: true })
} else { } else {
return cls.run(async () => { return cls.run(async () => {
cls.setOnContext(ContextKeys.IN_USE, 1) cls.setOnContext(ContextKeys.IN_USE, 1)
return internal({ existing: false, user }) return internal()
}) })
} }
} }
exports.doInUserContext = (user, task) => { exports.doInUserContext = (user, task) => {
return cls.run(() => { if (!user) {
let tenantId = user.tenantId throw new Error("user is required")
if (!tenantId) { }
tenantId = exports.getTenantId()
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) try {
return task() // 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 => { exports.setUser = user => {
@ -202,8 +221,7 @@ exports.setUser = user => {
exports.getUser = () => { exports.getUser = () => {
try { try {
const user = cls.getFromContext(ContextKeys.USER) return cls.getFromContext(ContextKeys.USER)
return user
} catch (e) { } catch (e) {
// do nothing - user is not in context // do nothing - user is not in context
} }
@ -211,7 +229,9 @@ exports.getUser = () => {
exports.updateTenantId = tenantId => { exports.updateTenantId = tenantId => {
cls.setOnContext(ContextKeys.TENANT_ID, tenantId) cls.setOnContext(ContextKeys.TENANT_ID, tenantId)
exports.setGlobalDB(tenantId) if (env.USE_COUCH) {
exports.setGlobalDB(tenantId)
}
} }
exports.updateAppId = async appId => { exports.updateAppId = async appId => {

View File

@ -4,6 +4,6 @@ import * as identification from "./identification"
export const publishEvent = async (event: Event, properties: any) => { export const publishEvent = async (event: Event, properties: any) => {
// in future this should use async events via a distributed queue. // 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) await processors.processEvent(event, identity, properties)
} }

View File

@ -11,20 +11,25 @@ import {
BudibaseIdentity, BudibaseIdentity,
isCloudAccount, isCloudAccount,
isSSOAccount, isSSOAccount,
TenantIdentity,
SettingsConfig,
} from "@budibase/types" } from "@budibase/types"
import { analyticsProcessor } from "./processors" 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<Identity> => {
const user: SessionUser | undefined = context.getUser() const user: SessionUser | undefined = context.getUser()
const tenantId = context.getTenantId() let tenantId = context.getTenantId()
let id: string let id: string
if (user) { if (user) {
id = user._id id = user._id
} else if (env.SELF_HOSTED) {
id = "installationId" // TODO
} else { } else {
id = tenantId const global = await getGlobalIdentifiers(tenantId)
id = global.id
tenantId = global.tenantId
} }
return { return {
@ -33,6 +38,55 @@ export const getCurrentIdentity = (): Identity => {
} }
} }
const getGlobalId = async (): Promise<string> => {
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) => { export const identifyUser = async (user: User) => {
const id = user._id as string const id = user._id as string
const tenantId = user.tenantId const tenantId = user.tenantId
@ -40,7 +94,7 @@ export const identifyUser = async (user: User) => {
const type = IdentityType.USER const type = IdentityType.USER
let builder = user.builder?.global let builder = user.builder?.global
let admin = user.admin?.global let admin = user.admin?.global
let authType = user.providerType ? user.providerType : "password" let providerType = user.providerType
const identity: BudibaseIdentity = { const identity: BudibaseIdentity = {
id, id,
@ -49,7 +103,7 @@ export const identifyUser = async (user: User) => {
type, type,
builder, builder,
admin, admin,
authType, providerType,
} }
await identify(identity) await identify(identity)
@ -60,9 +114,7 @@ export const identifyAccount = async (account: Account) => {
const tenantId = account.tenantId const tenantId = account.tenantId
const hosting = account.hosting const hosting = account.hosting
let type = IdentityType.ACCOUNT let type = IdentityType.ACCOUNT
let authType = isSSOAccount(account) let providerType = isSSOAccount(account) ? account.providerType : undefined
? (account.providerType as string)
: "password"
if (isCloudAccount(account)) { if (isCloudAccount(account)) {
if (account.budibaseUserId) { if (account.budibaseUserId) {
@ -77,7 +129,7 @@ export const identifyAccount = async (account: Account) => {
tenantId, tenantId,
hosting, hosting,
type, type,
authType, providerType,
verified: account.verified, verified: account.verified,
profession: account.profession, profession: account.profession,
companySize: account.size, companySize: account.size,

View File

@ -1,6 +1,7 @@
import { processors } from "./processors" import { processors } from "./processors"
export * from "./publishers" export * from "./publishers"
export * as analytics from "./analytics" export * as analytics from "./analytics"
export * as identification from "./identification"
export const shutdown = () => { export const shutdown = () => {
processors.shutdown() processors.shutdown()

View File

@ -79,14 +79,14 @@ exports.authenticateThirdParty = async function (
dbUser.forceResetPassword = false dbUser.forceResetPassword = false
// create or sync the user // create or sync the user
let response
try { try {
response = await saveUserFn(dbUser, false, false) await saveUserFn(dbUser, false, false)
} catch (err) { } catch (err) {
return authError(done, 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 // authenticate
const sessionId = newid() const sessionId = newid()

View File

@ -3,6 +3,7 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"exclude": [ "exclude": [
"node_modules", "node_modules",
"dist/**/*",
"**/*.spec.js", "**/*.spec.js",
"**/*.spec.ts" "**/*.spec.ts"
] ]

View File

@ -29,6 +29,7 @@
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",
"dist/**/*",
"**/*.spec.js", "**/*.spec.js",
// "**/*.spec.ts" // don't exclude spec.ts files for editor support // "**/*.spec.ts" // don't exclude spec.ts files for editor support
] ]

View File

@ -19,6 +19,7 @@ export interface SettingsConfig extends Config {
company: string company: string
logoUrl: string logoUrl: string
platformUrl: string platformUrl: string
globalId?: string
} }
} }

View File

@ -10,6 +10,9 @@ export interface User extends Document {
} }
providerType?: string providerType?: string
tenantId: string tenantId: string
email: string
password?: string
status?: string
} }
export interface UserRoles { export interface UserRoles {

View File

@ -11,10 +11,15 @@ export interface Identity {
tenantId: string tenantId: string
} }
export interface UserIdentity extends Identity { export interface TenantIdentity extends Identity {
hosting: Hosting hosting: Hosting
type: IdentityType type: IdentityType
authType: string }
export interface UserIdentity extends TenantIdentity {
hosting: Hosting
type: IdentityType
providerType?: string
} }
export interface BudibaseIdentity extends UserIdentity { export interface BudibaseIdentity extends UserIdentity {

View File

@ -2,13 +2,15 @@ import { EmailTemplatePurpose } from "../../../constants"
import { checkInviteCode } from "../../../utilities/redis" import { checkInviteCode } from "../../../utilities/redis"
import { sendEmail } from "../../../utilities/email" import { sendEmail } from "../../../utilities/email"
import { users } from "../../../sdk" 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 { const {
errors, errors,
users: usersCore, users: usersCore,
tenancy, tenancy,
db: dbUtils, db: dbUtils,
events,
} = require("@budibase/backend-core") } = require("@budibase/backend-core")
export const save = async (ctx: any) => { 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.") ctx.throw(403, "You cannot initialise once a global user has been created.")
} }
const user = { const user: User = {
email: email, email: email,
password: password, password: password,
createdAt: Date.now(),
roles: {}, roles: {},
builder: { builder: {
global: true, global: true,
@ -65,6 +66,7 @@ export const adminUser = async (ctx: any) => {
ctx.body = await tenancy.doInTenant(tenantId, async () => { ctx.body = await tenancy.doInTenant(tenantId, async () => {
return users.save(user, hashPassword, requirePassword) return users.save(user, hashPassword, requirePassword)
}) })
await events.identification.identifyTenant(tenantId)
} catch (err: any) { } catch (err: any) {
ctx.throw(err.status || 400, err) 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 // info is an extension of the user object that was stored by global
const { email, info }: any = await checkInviteCode(inviteCode) const { email, info }: any = await checkInviteCode(inviteCode)
ctx.body = await tenancy.doInTenant(info.tenantId, async () => { ctx.body = await tenancy.doInTenant(info.tenantId, async () => {
const user = await users.save({ const saved = await users.save({
firstName, firstName,
lastName, lastName,
password, password,
email, email,
...info, ...info,
}) })
const db = getGlobalDB()
const user = await db.get(saved._id)
await events.user.inviteAccepted(user) await events.user.inviteAccepted(user)
return user return saved
}) })
} catch (err: any) { } catch (err: any) {
if (err.code === errors.codes.USAGE_LIMIT_EXCEEDED) { if (err.code === errors.codes.USAGE_LIMIT_EXCEEDED) {

View File

@ -2,6 +2,7 @@ import env from "../../environment"
import { quotas } from "@budibase/pro" import { quotas } from "@budibase/pro"
import * as apps from "../../utilities/appService" import * as apps from "../../utilities/appService"
import * as eventHelpers from "./events" import * as eventHelpers from "./events"
import { User } from "@budibase/types"
const { const {
tenancy, tenancy,
@ -16,6 +17,8 @@ const {
HTTPError, HTTPError,
} = require("@budibase/backend-core") } = require("@budibase/backend-core")
import { events } from "@budibase/backend-core"
/** /**
* Retrieves all users from the current tenancy. * Retrieves all users from the current tenancy.
*/ */
@ -51,7 +54,7 @@ export const getUser = async (userId: string) => {
} }
export const save = async ( export const save = async (
user: any, user: User,
hashPassword = true, hashPassword = true,
requirePassword = true requirePassword = true
) => { ) => {
@ -97,7 +100,7 @@ export const save = async (
} }
if (!_id) { if (!_id) {
_id = dbUtils.generateGlobalUserID(email) _id = dbUtils.generateGlobalUserID()
} }
user = { user = {
@ -130,7 +133,7 @@ export const save = async (
user._rev = response.rev user._rev = response.rev
await eventHelpers.handleSaveEvents(user, dbUser) await eventHelpers.handleSaveEvents(user, dbUser)
await events.identification.identifyUser(user)
await tenancy.tryAddTenant(tenantId, _id, email) await tenancy.tryAddTenant(tenantId, _id, email)
await cache.user.invalidateUser(response.id) await cache.user.invalidateUser(response.id)
// let server know to sync user // let server know to sync user