diff --git a/lerna.json b/lerna.json index 9cffdba08a..bacdcb782f 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "2.22.12", + "version": "2.22.13", "npmClient": "yarn", "packages": [ "packages/*", diff --git a/packages/account-portal b/packages/account-portal index 60658a052d..63ce32bca8 160000 --- a/packages/account-portal +++ b/packages/account-portal @@ -1 +1 @@ -Subproject commit 60658a052d2642e5f4a8038e253f771a24f34907 +Subproject commit 63ce32bca871f0a752323f5f7ebb5ec16bbbacc3 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/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte index 7fa2401c88..0632993cf0 100644 --- a/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte +++ b/packages/builder/src/components/automation/SetupPanel/AutomationBlockSetup.svelte @@ -31,7 +31,7 @@ import ModalBindableInput from "components/common/bindings/ModalBindableInput.svelte" import CodeEditor from "components/common/CodeEditor/CodeEditor.svelte" import BindingSidePanel from "components/common/bindings/BindingSidePanel.svelte" - import { BindingHelpers } from "components/common/bindings/utils" + import { BindingHelpers, BindingType } from "components/common/bindings/utils" import { bindingsToCompletions, hbAutocomplete, @@ -576,6 +576,7 @@ { js: true, dontDecode: true, + type: BindingType.RUNTIME, } )} mode="javascript" diff --git a/packages/builder/src/components/common/bindings/utils.js b/packages/builder/src/components/common/bindings/utils.js index c60374f0f7..77e4a1dfb1 100644 --- a/packages/builder/src/components/common/bindings/utils.js +++ b/packages/builder/src/components/common/bindings/utils.js @@ -1,6 +1,11 @@ import { decodeJSBinding } from "@budibase/string-templates" import { hbInsert, jsInsert } from "components/common/CodeEditor" +export const BindingType = { + READABLE: "readableBinding", + RUNTIME: "runtimeBinding", +} + export class BindingHelpers { constructor(getCaretPosition, insertAtPos, { disableWrapping } = {}) { this.getCaretPosition = getCaretPosition @@ -25,16 +30,20 @@ export class BindingHelpers { } // Adds a data binding to the expression - onSelectBinding(value, binding, { js, dontDecode }) { + onSelectBinding( + value, + binding, + { js, dontDecode, type = BindingType.READABLE } + ) { const { start, end } = this.getCaretPosition() if (js) { const jsVal = dontDecode ? value : decodeJSBinding(value) - const insertVal = jsInsert(jsVal, start, end, binding.readableBinding, { + const insertVal = jsInsert(jsVal, start, end, binding[type], { disableWrapping: this.disableWrapping, }) this.insertAtPos({ start, end, value: insertVal }) } else { - const insertVal = hbInsert(value, start, end, binding.readableBinding) + const insertVal = hbInsert(value, start, end, binding[type]) this.insertAtPos({ start, end, value: insertVal }) } } diff --git a/packages/server/src/integrations/couchdb.ts b/packages/server/src/integrations/couchdb.ts index 079f646b60..c271fb12b2 100644 --- a/packages/server/src/integrations/couchdb.ts +++ b/packages/server/src/integrations/couchdb.ts @@ -9,6 +9,7 @@ import { QueryType, } from "@budibase/types" import { db as dbCore } from "@budibase/backend-core" +import { HOST_ADDRESS } from "./utils" interface CouchDBConfig { url: string @@ -28,7 +29,7 @@ const SCHEMA: Integration = { url: { type: DatasourceFieldType.STRING, required: true, - default: "http://localhost:5984", + default: `http://${HOST_ADDRESS}:5984`, }, database: { type: DatasourceFieldType.STRING, diff --git a/packages/server/src/integrations/elasticsearch.ts b/packages/server/src/integrations/elasticsearch.ts index 7ae0295298..af03baaef1 100644 --- a/packages/server/src/integrations/elasticsearch.ts +++ b/packages/server/src/integrations/elasticsearch.ts @@ -8,6 +8,7 @@ import { } from "@budibase/types" import { Client, ClientOptions } from "@elastic/elasticsearch" +import { HOST_ADDRESS } from "./utils" interface ElasticsearchConfig { url: string @@ -29,7 +30,7 @@ const SCHEMA: Integration = { url: { type: DatasourceFieldType.STRING, required: true, - default: "http://localhost:9200", + default: `http://${HOST_ADDRESS}:9200`, }, ssl: { type: DatasourceFieldType.BOOLEAN, diff --git a/packages/server/src/integrations/microsoftSqlServer.ts b/packages/server/src/integrations/microsoftSqlServer.ts index 3635eae2ce..5626d7eda3 100644 --- a/packages/server/src/integrations/microsoftSqlServer.ts +++ b/packages/server/src/integrations/microsoftSqlServer.ts @@ -22,6 +22,7 @@ import { finaliseExternalTables, SqlClient, checkExternalTables, + HOST_ADDRESS, } from "./utils" import Sql from "./base/sql" import { MSSQLTablesResponse, MSSQLColumn } from "./base/types" @@ -88,7 +89,6 @@ const SCHEMA: Integration = { user: { type: DatasourceFieldType.STRING, required: true, - default: "localhost", }, password: { type: DatasourceFieldType.PASSWORD, @@ -96,7 +96,7 @@ const SCHEMA: Integration = { }, server: { type: DatasourceFieldType.STRING, - default: "localhost", + default: HOST_ADDRESS, }, port: { type: DatasourceFieldType.NUMBER, diff --git a/packages/server/src/integrations/mongodb.ts b/packages/server/src/integrations/mongodb.ts index c9852e4c7a..dea752502d 100644 --- a/packages/server/src/integrations/mongodb.ts +++ b/packages/server/src/integrations/mongodb.ts @@ -22,6 +22,7 @@ import { InsertManyResult, } from "mongodb" import environment from "../environment" +import { HOST_ADDRESS } from "./utils" export interface MongoDBConfig { connectionString: string @@ -51,7 +52,7 @@ const getSchema = () => { connectionString: { type: DatasourceFieldType.STRING, required: true, - default: "mongodb://localhost:27017", + default: `mongodb://${HOST_ADDRESS}:27017`, display: "Connection string", }, db: { diff --git a/packages/server/src/integrations/mysql.ts b/packages/server/src/integrations/mysql.ts index 4b8abcfed0..19a63a44ad 100644 --- a/packages/server/src/integrations/mysql.ts +++ b/packages/server/src/integrations/mysql.ts @@ -22,6 +22,7 @@ import { generateColumnDefinition, finaliseExternalTables, checkExternalTables, + HOST_ADDRESS, } from "./utils" import dayjs from "dayjs" import { NUMBER_REGEX } from "../utilities" @@ -50,7 +51,7 @@ const SCHEMA: Integration = { datasource: { host: { type: DatasourceFieldType.STRING, - default: "localhost", + default: HOST_ADDRESS, required: true, }, port: { diff --git a/packages/server/src/integrations/oracle.ts b/packages/server/src/integrations/oracle.ts index 87fcd3d618..f6ec593f2f 100644 --- a/packages/server/src/integrations/oracle.ts +++ b/packages/server/src/integrations/oracle.ts @@ -22,6 +22,7 @@ import { finaliseExternalTables, getSqlQuery, SqlClient, + HOST_ADDRESS, } from "./utils" import Sql from "./base/sql" import { @@ -63,7 +64,7 @@ const SCHEMA: Integration = { datasource: { host: { type: DatasourceFieldType.STRING, - default: "localhost", + default: HOST_ADDRESS, required: true, }, port: { diff --git a/packages/server/src/integrations/postgres.ts b/packages/server/src/integrations/postgres.ts index 16597597ae..e810986757 100644 --- a/packages/server/src/integrations/postgres.ts +++ b/packages/server/src/integrations/postgres.ts @@ -21,6 +21,7 @@ import { finaliseExternalTables, SqlClient, checkExternalTables, + HOST_ADDRESS, } from "./utils" import Sql from "./base/sql" import { PostgresColumn } from "./base/types" @@ -72,7 +73,7 @@ const SCHEMA: Integration = { datasource: { host: { type: DatasourceFieldType.STRING, - default: "localhost", + default: HOST_ADDRESS, required: true, }, port: { diff --git a/packages/server/src/integrations/redis.ts b/packages/server/src/integrations/redis.ts index 6a6331ccd4..e127cddd56 100644 --- a/packages/server/src/integrations/redis.ts +++ b/packages/server/src/integrations/redis.ts @@ -6,6 +6,7 @@ import { QueryType, } from "@budibase/types" import Redis from "ioredis" +import { HOST_ADDRESS } from "./utils" interface RedisConfig { host: string @@ -28,7 +29,7 @@ const SCHEMA: Integration = { host: { type: DatasourceFieldType.STRING, required: true, - default: "localhost", + default: HOST_ADDRESS, }, port: { type: DatasourceFieldType.NUMBER, diff --git a/packages/server/src/integrations/utils.ts b/packages/server/src/integrations/utils.ts index 729005c166..1eb7e44c00 100644 --- a/packages/server/src/integrations/utils.ts +++ b/packages/server/src/integrations/utils.ts @@ -8,6 +8,7 @@ import { import { DocumentType, SEPARATOR } from "../db/utils" import { InvalidColumns, DEFAULT_BB_DATASOURCE_ID } from "../constants" import { helpers } from "@budibase/shared-core" +import env from "../environment" const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}` const ROW_ID_REGEX = /^\[.*]$/g @@ -88,6 +89,14 @@ export enum SqlClient { SQL_LITE = "sqlite3", } +const isCloud = env.isProd() && !env.SELF_HOSTED +const isSelfHost = env.isProd() && env.SELF_HOSTED +export const HOST_ADDRESS = isSelfHost + ? "host.docker.internal" + : isCloud + ? "" + : "localhost" + export function isExternalTableID(tableId: string) { return tableId.includes(DocumentType.DATASOURCE) } 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..93f35b4c37 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,25 @@ 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..2198757be1 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,51 @@ 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() + user.ssoId = "testssoId" + 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}`)