1
0
Fork 0
mirror of synced 2024-06-01 18:20:18 +12:00

Identity updates

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

View file

@ -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 => {

View file

@ -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)
}

View file

@ -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<Identity> => {
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<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) => {
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,

View file

@ -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()

View file

@ -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()

View file

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

View file

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

View file

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

View file

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

View file

@ -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 {

View file

@ -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) {

View file

@ -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