From b979b29313ab396940338e572334a5a0e411e59f Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Sat, 23 Sep 2023 00:10:12 +0100 Subject: [PATCH 1/5] e2e secure microsoft auth --- packages/backend-core/src/db/views.ts | 4 +++ packages/backend-core/src/platform/users.ts | 28 ++++++++++++++++--- packages/backend-core/src/users/db.ts | 2 +- packages/types/src/api/account/accounts.ts | 4 ++- .../types/src/documents/account/account.ts | 7 +++++ packages/types/src/documents/global/user.ts | 1 + .../types/src/documents/platform/users.ts | 11 +++++++- .../src/api/controllers/global/users.ts | 5 +++- .../worker/src/api/routes/global/users.ts | 1 + 9 files changed, 55 insertions(+), 8 deletions(-) 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..684dd40052 100644 --- a/packages/backend-core/src/platform/users.ts +++ b/packages/backend-core/src/platform/users.ts @@ -4,7 +4,7 @@ import env from "../environment" import { PlatformUser, PlatformUserByEmail, - PlatformUserById, + PlatformUserById, PlatformUserBySsoId, User, } from "@budibase/types" @@ -45,6 +45,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 +78,17 @@ 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..b39fbb5d61 100644 --- a/packages/backend-core/src/users/db.ts +++ b/packages/backend-core/src/users/db.ts @@ -278,7 +278,7 @@ 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/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/documents/account/account.ts b/packages/types/src/documents/account/account.ts index 5321aa7e08..c22ed3572b 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 } 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..42754b5e5a 100644 --- a/packages/types/src/documents/platform/users.ts +++ b/packages/types/src/documents/platform/users.ts @@ -15,4 +15,13 @@ 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..8968ebdcd0 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -95,7 +95,8 @@ const parseBooleanParam = (param: any) => { export const adminUser = async ( ctx: Ctx ) => { - const { email, password, tenantId } = ctx.request.body + // @ts-ignore + const { email, password, tenantId, ssoId } = ctx.request.body if (await platform.tenants.exists(tenantId)) { ctx.throw(403, "Organisation already exists.") @@ -136,6 +137,8 @@ export const adminUser = async ( global: true, }, tenantId, + // @ts-ignore + 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) From 9cf41133fbff0e644e81b9f5867dcc58d0274f48 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Sun, 24 Sep 2023 19:40:37 +0100 Subject: [PATCH 2/5] add verifiable SSO providers type --- packages/types/src/documents/account/account.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/types/src/documents/account/account.ts b/packages/types/src/documents/account/account.ts index c22ed3572b..764b32a82d 100644 --- a/packages/types/src/documents/account/account.ts +++ b/packages/types/src/documents/account/account.ts @@ -94,6 +94,10 @@ export enum AccountSSOProvider { MICROSOFT = "microsoft", } +export enum VerifiableSSOProviders { + MICROSOFT = "microsoft" +} + export interface AccountSSO { provider: AccountSSOProvider providerType: AccountSSOProviderType From ba52a901a941c60d1f184dbc0c4bde4bbb330f28 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Sun, 24 Sep 2023 20:47:29 +0100 Subject: [PATCH 3/5] improved type guards --- packages/types/src/documents/account/account.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/types/src/documents/account/account.ts b/packages/types/src/documents/account/account.ts index 764b32a82d..035f95c4f2 100644 --- a/packages/types/src/documents/account/account.ts +++ b/packages/types/src/documents/account/account.ts @@ -94,8 +94,9 @@ export enum AccountSSOProvider { MICROSOFT = "microsoft", } -export enum VerifiableSSOProviders { - MICROSOFT = "microsoft" +const verifiableSSOProviders: AccountSSOProvider[] = [AccountSSOProvider.MICROSOFT] +export function isVerifiableSSOProvider(provider: AccountSSOProvider): boolean { + return verifiableSSOProviders.includes(provider); } export interface AccountSSO { From 6d4e3082e355d0853f2deaf6b1487e92f1125358 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Sun, 24 Sep 2023 22:07:40 +0100 Subject: [PATCH 4/5] type tidy up and lint --- packages/backend-core/src/platform/users.ts | 16 ++++++-- packages/backend-core/src/users/db.ts | 7 +++- .../core/utilities/structures/accounts.ts | 37 +++++++++++++++++-- packages/types/src/api/web/user.ts | 1 + .../types/src/documents/account/account.ts | 6 ++- .../types/src/documents/platform/users.ts | 5 ++- .../src/api/controllers/global/users.ts | 2 - 7 files changed, 61 insertions(+), 13 deletions(-) diff --git a/packages/backend-core/src/platform/users.ts b/packages/backend-core/src/platform/users.ts index 684dd40052..6f030afb7c 100644 --- a/packages/backend-core/src/platform/users.ts +++ b/packages/backend-core/src/platform/users.ts @@ -4,7 +4,8 @@ import env from "../environment" import { PlatformUser, PlatformUserByEmail, - PlatformUserById, PlatformUserBySsoId, + PlatformUserById, + PlatformUserBySsoId, User, } from "@budibase/types" @@ -49,7 +50,7 @@ function newUserSsoIdDoc( ssoId: string, email: string, userId: string, - tenantId: string, + tenantId: string ): PlatformUserBySsoId { return { _id: ssoId, @@ -78,14 +79,21 @@ async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) { } } -export async function addUser(tenantId: string, userId: string, email: string, ssoId?: string) { +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))) + promises.push( + addUserDoc(ssoId, () => newUserSsoIdDoc(ssoId, email, userId, tenantId)) + ) } await Promise.all(promises) diff --git a/packages/backend-core/src/users/db.ts b/packages/backend-core/src/users/db.ts index b39fbb5d61..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, builtUser.ssoId) + 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/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 035f95c4f2..2f74b9e7b3 100644 --- a/packages/types/src/documents/account/account.ts +++ b/packages/types/src/documents/account/account.ts @@ -94,9 +94,11 @@ export enum AccountSSOProvider { MICROSOFT = "microsoft", } -const verifiableSSOProviders: AccountSSOProvider[] = [AccountSSOProvider.MICROSOFT] +const verifiableSSOProviders: AccountSSOProvider[] = [ + AccountSSOProvider.MICROSOFT, +] export function isVerifiableSSOProvider(provider: AccountSSOProvider): boolean { - return verifiableSSOProviders.includes(provider); + return verifiableSSOProviders.includes(provider) } export interface AccountSSO { diff --git a/packages/types/src/documents/platform/users.ts b/packages/types/src/documents/platform/users.ts index 42754b5e5a..8f24329502 100644 --- a/packages/types/src/documents/platform/users.ts +++ b/packages/types/src/documents/platform/users.ts @@ -24,4 +24,7 @@ export interface PlatformUserBySsoId extends Document { email: string } -export type PlatformUser = PlatformUserByEmail | PlatformUserById | PlatformUserBySsoId +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 8968ebdcd0..822a16d33e 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -95,7 +95,6 @@ const parseBooleanParam = (param: any) => { export const adminUser = async ( ctx: Ctx ) => { - // @ts-ignore const { email, password, tenantId, ssoId } = ctx.request.body if (await platform.tenants.exists(tenantId)) { @@ -137,7 +136,6 @@ export const adminUser = async ( global: true, }, tenantId, - // @ts-ignore ssoId, } try { From 83f042eeaf67aac1d13f3a8d29b1c6332dacc624 Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Mon, 25 Sep 2023 13:57:10 +0100 Subject: [PATCH 5/5] update name of platform users view so it rebuilds --- packages/backend-core/src/constants/db.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", }