diff --git a/lerna.json b/lerna.json index 6b5509ed5c..0c9e739afc 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.10.13", + "version": "2.10.14", "npmClient": "yarn", "packages": [ "packages/*" diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index 83f8298f54..f918dcc352 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -18,7 +18,7 @@ export enum ViewName { ROUTING = "screen_routes", AUTOMATION_LOGS = "automation_logs", ACCOUNT_BY_EMAIL = "account_by_email", - PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", + PLATFORM_USERS_LOWERCASE = "platform_users_lowercase_2", USER_BY_GROUP = "user_by_group", APP_BACKUP_BY_TRIGGER = "by_trigger", } diff --git a/packages/backend-core/src/db/views.ts b/packages/backend-core/src/db/views.ts index 7f5ef29a0a..f0980ad217 100644 --- a/packages/backend-core/src/db/views.ts +++ b/packages/backend-core/src/db/views.ts @@ -190,6 +190,10 @@ export const createPlatformUserView = async () => { if (doc.tenantId) { emit(doc._id.toLowerCase(), doc._id) } + + if (doc.ssoId) { + emit(doc.ssoId, doc._id) + } }` await createPlatformView(viewJs, ViewName.PLATFORM_USERS_LOWERCASE) } diff --git a/packages/backend-core/src/platform/users.ts b/packages/backend-core/src/platform/users.ts index c65a7e0ec4..6f030afb7c 100644 --- a/packages/backend-core/src/platform/users.ts +++ b/packages/backend-core/src/platform/users.ts @@ -5,6 +5,7 @@ import { PlatformUser, PlatformUserByEmail, PlatformUserById, + PlatformUserBySsoId, User, } from "@budibase/types" @@ -45,6 +46,20 @@ function newUserEmailDoc( } } +function newUserSsoIdDoc( + ssoId: string, + email: string, + userId: string, + tenantId: string +): PlatformUserBySsoId { + return { + _id: ssoId, + userId, + email, + tenantId, + } +} + /** * Add a new user id or email doc if it doesn't exist. */ @@ -64,11 +79,24 @@ async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) { } } -export async function addUser(tenantId: string, userId: string, email: string) { - await Promise.all([ +export async function addUser( + tenantId: string, + userId: string, + email: string, + ssoId?: string +) { + const promises = [ addUserDoc(userId, () => newUserIdDoc(userId, tenantId)), addUserDoc(email, () => newUserEmailDoc(userId, email, tenantId)), - ]) + ] + + if (ssoId) { + promises.push( + addUserDoc(ssoId, () => newUserSsoIdDoc(ssoId, email, userId, tenantId)) + ) + } + + await Promise.all(promises) } // DELETE diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index c288540f35..1d02bebc32 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -278,7 +278,12 @@ export class UserDB { builtUser._rev = response.rev await eventHelpers.handleSaveEvents(builtUser, dbUser) - await platform.users.addUser(tenantId, builtUser._id!, builtUser.email) + await platform.users.addUser( + tenantId, + builtUser._id!, + builtUser.email, + builtUser.ssoId + ) await cache.user.invalidateUser(response.id) await Promise.all(groupPromises) diff --git a/packages/backend-core/tests/core/utilities/structures/accounts.ts b/packages/backend-core/tests/core/utilities/structures/accounts.ts index 67e4411ea3..515f94db1e 100644 --- a/packages/backend-core/tests/core/utilities/structures/accounts.ts +++ b/packages/backend-core/tests/core/utilities/structures/accounts.ts @@ -1,4 +1,4 @@ -import { generator, uuid, quotas } from "." +import { generator, quotas, uuid } from "." import { generateGlobalUserID } from "../../../../src/docIds" import { Account, @@ -6,10 +6,11 @@ import { AccountSSOProviderType, AuthType, CloudAccount, - Hosting, - SSOAccount, CreateAccount, CreatePassswordAccount, + CreateVerifiableSSOAccount, + Hosting, + SSOAccount, } from "@budibase/types" import sample from "lodash/sample" @@ -68,6 +69,23 @@ export function ssoAccount(account: Account = cloudAccount()): SSOAccount { } } +export function verifiableSsoAccount( + account: Account = cloudAccount() +): SSOAccount { + return { + ...account, + authType: AuthType.SSO, + oauth2: { + accessToken: generator.string(), + refreshToken: generator.string(), + }, + pictureUrl: generator.url(), + provider: AccountSSOProvider.MICROSOFT, + providerType: AccountSSOProviderType.MICROSOFT, + thirdPartyProfile: { id: "abc123" }, + } +} + export const cloudCreateAccount: CreatePassswordAccount = { email: "cloud@budibase.com", tenantId: "cloud", @@ -91,6 +109,19 @@ export const cloudSSOCreateAccount: CreateAccount = { profession: "Software Engineer", } +export const cloudVerifiableSSOCreateAccount: CreateVerifiableSSOAccount = { + email: "cloud-sso@budibase.com", + tenantId: "cloud-sso", + hosting: Hosting.CLOUD, + authType: AuthType.SSO, + tenantName: "cloudsso", + name: "Budi Armstrong", + size: "10+", + profession: "Software Engineer", + provider: AccountSSOProvider.MICROSOFT, + thirdPartyProfile: { id: "abc123" }, +} + export const selfCreateAccount: CreatePassswordAccount = { email: "self@budibase.com", tenantId: "self", diff --git a/packages/types/src/api/account/accounts.ts b/packages/types/src/api/account/accounts.ts index bb3419c5d1..1be506e14e 100644 --- a/packages/types/src/api/account/accounts.ts +++ b/packages/types/src/api/account/accounts.ts @@ -1,4 +1,4 @@ -import { Account } from "../../documents" +import { Account, AccountSSOProvider } from "../../documents" import { Hosting } from "../../sdk" export interface CreateAccountRequest { @@ -11,6 +11,8 @@ export interface CreateAccountRequest { tenantName?: string name?: string password: string + provider?: AccountSSOProvider + thirdPartyProfile: object } export interface SearchAccountsRequest { diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index 619362805a..85e2d89ad1 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -61,6 +61,7 @@ export interface CreateAdminUserRequest { email: string password: string tenantId: string + ssoId?: string } export interface CreateAdminUserResponse { diff --git a/packages/types/src/documents/account/account.ts b/packages/types/src/documents/account/account.ts index 5321aa7e08..2f74b9e7b3 100644 --- a/packages/types/src/documents/account/account.ts +++ b/packages/types/src/documents/account/account.ts @@ -20,6 +20,11 @@ export interface CreatePassswordAccount extends CreateAccount { password: string } +export interface CreateVerifiableSSOAccount extends CreateAccount { + provider?: AccountSSOProvider + thirdPartyProfile?: any +} + export const isCreatePasswordAccount = ( account: CreateAccount ): account is CreatePassswordAccount => account.authType === AuthType.PASSWORD @@ -50,6 +55,8 @@ export interface Account extends CreateAccount { licenseKeyActivatedAt?: number licenseRequestedAt?: number licenseOverrides?: LicenseOverrides + provider?: AccountSSOProvider + providerType?: AccountSSOProviderType quotaUsage?: QuotaUsage offlineLicenseToken?: string } @@ -87,6 +94,13 @@ export enum AccountSSOProvider { MICROSOFT = "microsoft", } +const verifiableSSOProviders: AccountSSOProvider[] = [ + AccountSSOProvider.MICROSOFT, +] +export function isVerifiableSSOProvider(provider: AccountSSOProvider): boolean { + return verifiableSSOProviders.includes(provider) +} + export interface AccountSSO { provider: AccountSSOProvider providerType: AccountSSOProviderType diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index 2ce714801d..9769661cd5 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -55,6 +55,7 @@ export interface User extends Document { userGroups?: string[] onboardedAt?: string scimInfo?: { isSync: true } & Record + ssoId?: string } export enum UserStatus { diff --git a/packages/types/src/documents/platform/users.ts b/packages/types/src/documents/platform/users.ts index 46cc44b31d..8f24329502 100644 --- a/packages/types/src/documents/platform/users.ts +++ b/packages/types/src/documents/platform/users.ts @@ -15,4 +15,16 @@ export interface PlatformUserById extends Document { tenantId: string } -export type PlatformUser = PlatformUserByEmail | PlatformUserById +/** + * doc id is a unique SSO provider ID for the user + */ +export interface PlatformUserBySsoId extends Document { + tenantId: string + userId: string + email: string +} + +export type PlatformUser = + | PlatformUserByEmail + | PlatformUserById + | PlatformUserBySsoId diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 6d3a13aa4e..822a16d33e 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -95,7 +95,7 @@ const parseBooleanParam = (param: any) => { export const adminUser = async ( ctx: Ctx ) => { - const { email, password, tenantId } = ctx.request.body + const { email, password, tenantId, ssoId } = ctx.request.body if (await platform.tenants.exists(tenantId)) { ctx.throw(403, "Organisation already exists.") @@ -136,6 +136,7 @@ export const adminUser = async ( global: true, }, tenantId, + ssoId, } try { // always bust checklist beforehand, if an error occurs but can proceed, don't get diff --git a/packages/worker/src/api/routes/global/users.ts b/packages/worker/src/api/routes/global/users.ts index 47e76c17be..a57f7834ac 100644 --- a/packages/worker/src/api/routes/global/users.ts +++ b/packages/worker/src/api/routes/global/users.ts @@ -14,6 +14,7 @@ function buildAdminInitValidation() { email: Joi.string().required(), password: Joi.string(), tenantId: Joi.string().required(), + ssoId: Joi.string(), }) .required() .unknown(false)