diff --git a/packages/backend-core/src/platform/users.ts b/packages/backend-core/src/platform/users.ts index 6f030afb7c..ccaad76b19 100644 --- a/packages/backend-core/src/platform/users.ts +++ b/packages/backend-core/src/platform/users.ts @@ -20,7 +20,7 @@ export async function lookupTenantId(userId: string) { return user.tenantId } -async function getUserDoc(emailOrId: string): Promise { +export async function getUserDoc(emailOrId: string): Promise { const db = getPlatformDB() return db.get(emailOrId) } @@ -79,6 +79,17 @@ async function addUserDoc(emailOrId: string, newDocFn: () => PlatformUser) { } } +export async function addSsoUser( + ssoId: string, + email: string, + userId: string, + tenantId: string +) { + return addUserDoc(ssoId, () => + newUserSsoIdDoc(ssoId, email, userId, tenantId) + ) +} + export async function addUser( tenantId: string, userId: string, @@ -91,9 +102,7 @@ export async function addUser( ] if (ssoId) { - promises.push( - addUserDoc(ssoId, () => newUserSsoIdDoc(ssoId, email, userId, tenantId)) - ) + promises.push(addSsoUser(ssoId, email, userId, tenantId)) } await Promise.all(promises) diff --git a/packages/backend-core/tests/core/utilities/structures/users.ts b/packages/backend-core/tests/core/utilities/structures/users.ts index db90887af2..88fb02617f 100644 --- a/packages/backend-core/tests/core/utilities/structures/users.ts +++ b/packages/backend-core/tests/core/utilities/structures/users.ts @@ -86,6 +86,7 @@ export function ssoUser( oauth2: opts.details?.oauth2, provider: opts.details?.provider!, providerType: opts.details?.providerType!, + ssoId: opts.details?.userId || uuid(), thirdPartyProfile: { email: base.email, picture: base.pictureUrl, diff --git a/packages/types/src/api/web/user.ts b/packages/types/src/api/web/user.ts index d68d687dcb..0ef7493016 100644 --- a/packages/types/src/api/web/user.ts +++ b/packages/types/src/api/web/user.ts @@ -68,6 +68,11 @@ export interface CreateAdminUserRequest { ssoId?: string } +export interface AddSSoUserRequest { + ssoId: string + email: string +} + export interface CreateAdminUserResponse { _id: string _rev: string diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 6b9e533f78..4e59873e33 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -3,6 +3,7 @@ import env from "../../../environment" import { AcceptUserInviteRequest, AcceptUserInviteResponse, + AddSSoUserRequest, BulkUserRequest, BulkUserResponse, CloudAccount, @@ -15,6 +16,7 @@ import { LockName, LockType, MigrationType, + PlatformUserByEmail, SaveUserResponse, SearchUsersRequest, User, @@ -53,6 +55,23 @@ export const save = async (ctx: UserCtx) => { } } +export const addSsoSupport = async (ctx: Ctx) => { + const { email, ssoId } = ctx.request.body + try { + // Status is changed to 404 from getUserDoc if user is not found + let userByEmail = (await platform.users.getUserDoc(email)) as PlatformUserByEmail + await platform.users.addSsoUser( + ssoId, + email, + userByEmail.userId, + userByEmail.tenantId + ) + ctx.status = 200 + } catch (err: any) { + ctx.throw(err.status || 400, err) + } +} + const bulkDelete = async (userIds: string[], currentUserId: string) => { if (userIds?.indexOf(currentUserId) !== -1) { throw new Error("Unable to delete self.") diff --git a/packages/worker/src/api/index.ts b/packages/worker/src/api/index.ts index d7aef0b274..4936c104e1 100644 --- a/packages/worker/src/api/index.ts +++ b/packages/worker/src/api/index.ts @@ -41,6 +41,10 @@ const PUBLIC_ENDPOINTS = [ route: "/api/global/users/init", method: "POST", }, + { + route: "/api/global/users/sso", + method: "POST", + }, { route: "/api/global/users/invite/accept", method: "POST", @@ -81,6 +85,11 @@ const NO_TENANCY_ENDPOINTS = [ route: "/api/global/users/init", method: "POST", }, + // tenant is retrieved from the user found by the requested email + { + route: "/api/global/users/sso", + method: "POST", + }, // deprecated single tenant sso callback { route: "/api/admin/auth/google/callback", diff --git a/packages/worker/src/api/routes/global/tests/users.spec.ts b/packages/worker/src/api/routes/global/tests/users.spec.ts index 37f5721881..6923a7839b 100644 --- a/packages/worker/src/api/routes/global/tests/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/users.spec.ts @@ -520,10 +520,50 @@ describe("/api/global/users", () => { }) } + function createPasswordUser() { + return config.doInTenant(() => { + const user = structures.users.user() + return userSdk.db.save(user) + }) + } + it("should be able to update an sso user that has no password", async () => { const user = await createSSOUser() await config.api.users.saveUser(user) }) + + it("sso support couldn't be used by admin. It is cloud restricted and needs internal key", async () => { + const user = await config.createUser() + const ssoId = "fake-ssoId" + await config.api.users + .addSsoSupportDefaultAuth(ssoId, user.email) + .expect("Content-Type", /json/) + .expect(403) + }) + + it("if user email doesn't exist, SSO support couldn't be added. Not found error returned", async () => { + const ssoId = "fake-ssoId" + const email = "fake-email@budibase.com" + await config.api.users + .addSsoSupportInternalAPIAuth(ssoId, email) + .expect("Content-Type", /json/) + .expect(404) + }) + + it("if user email exist, SSO support is added", async () => { + const user = await createPasswordUser() + const ssoId = "fakessoId" + await config.api.users + .addSsoSupportInternalAPIAuth(ssoId, user.email) + .expect(200) + }) + + it("if user ssoId is already assigned, no change will be applied", async () => { + const user = await createSSOUser() + await config.api.users + .addSsoSupportInternalAPIAuth(user.ssoId, user.email) + .expect(200) + }) }) }) diff --git a/packages/worker/src/api/routes/global/users.ts b/packages/worker/src/api/routes/global/users.ts index 6b9291b88b..e7c77678fc 100644 --- a/packages/worker/src/api/routes/global/users.ts +++ b/packages/worker/src/api/routes/global/users.ts @@ -65,6 +65,12 @@ router users.buildUserSaveValidation(), controller.save ) + .post( + "/api/global/users/sso", + cloudRestricted, + users.buildAddSsoSupport(), + controller.addSsoSupport + ) .post( "/api/global/users/bulk", auth.adminOnly, diff --git a/packages/worker/src/api/routes/validation/users.ts b/packages/worker/src/api/routes/validation/users.ts index fbc85af2d3..cbd7567457 100644 --- a/packages/worker/src/api/routes/validation/users.ts +++ b/packages/worker/src/api/routes/validation/users.ts @@ -41,6 +41,15 @@ export const buildUserSaveValidation = () => { return auth.joiValidator.body(Joi.object(schema).required().unknown(true)) } +export const buildAddSsoSupport = () => { + return auth.joiValidator.body( + Joi.object({ + ssoId: Joi.string().required(), + email: Joi.string().required(), + }).required() + ) +} + export const buildUserBulkUserValidation = (isSelf = false) => { if (!isSelf) { schema = { diff --git a/packages/worker/src/tests/api/users.ts b/packages/worker/src/tests/api/users.ts index 45105c99da..d08a4ef8c7 100644 --- a/packages/worker/src/tests/api/users.ts +++ b/packages/worker/src/tests/api/users.ts @@ -127,6 +127,20 @@ export class UserAPI extends TestAPI { .expect(status ? status : 200) } + addSsoSupportInternalAPIAuth = (ssoId: string, email: string) => { + return this.request + .post(`/api/global/users/sso`) + .send({ ssoId, email }) + .set(this.config.internalAPIHeaders()) + } + + addSsoSupportDefaultAuth = (ssoId: string, email: string) => { + return this.request + .post(`/api/global/users/sso`) + .send({ ssoId, email }) + .set(this.config.defaultHeaders()) + } + deleteUser = (userId: string, status?: number) => { return this.request .delete(`/api/global/users/${userId}`)