From f65ded4282d91470797b0e36dfd5e2d108954d98 Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 9 Mar 2023 14:02:55 +0100 Subject: [PATCH 001/135] Allow bearer token for auth --- packages/backend-core/src/constants/misc.ts | 1 + packages/backend-core/src/middleware/authenticated.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/backend-core/src/constants/misc.ts b/packages/backend-core/src/constants/misc.ts index e25c90575f..a4a1806618 100644 --- a/packages/backend-core/src/constants/misc.ts +++ b/packages/backend-core/src/constants/misc.ts @@ -23,6 +23,7 @@ export enum Header { TOKEN = "x-budibase-token", CSRF_TOKEN = "x-csrf-token", CORRELATION_ID = "x-budibase-correlation-id", + AUTHORIZATION = "authorization", } export enum GlobalRole { diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index 8a97319586..f877985ee0 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -96,9 +96,15 @@ export default function ( } try { // check the actual user is authenticated first, try header or cookie - const headerToken = ctx.request.headers[Header.TOKEN] + let headerToken = ctx.request.headers[Header.TOKEN] + const authCookie = getCookie(ctx, Cookie.Auth) || openJwt(headerToken) - const apiKey = ctx.request.headers[Header.API_KEY] + let apiKey = ctx.request.headers[Header.API_KEY] + + if (!apiKey && ctx.request.headers[Header.AUTHORIZATION]) { + apiKey = ctx.request.headers[Header.AUTHORIZATION].split(" ")[1] + } + const tenantId = ctx.request.headers[Header.TENANT_ID] let authenticated = false, user = null, From d083553373152262fa833ef0620b2c23ffee3e27 Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 9 Mar 2023 14:13:13 +0100 Subject: [PATCH 002/135] Add scim endpoints --- packages/worker/src/api/routes/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/worker/src/api/routes/index.ts b/packages/worker/src/api/routes/index.ts index c64ad44423..1ada7d9a01 100644 --- a/packages/worker/src/api/routes/index.ts +++ b/packages/worker/src/api/routes/index.ts @@ -39,4 +39,5 @@ export const routes: Router[] = [ accountRoutes, restoreRoutes, eventRoutes, + ...api.scimRoutes, ] From 4068faf9f325d611598a5f8f83211b29291b0144 Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 9 Mar 2023 16:15:50 +0100 Subject: [PATCH 003/135] Add scaffolding --- packages/types/src/api/web/global/index.ts | 1 + .../types/src/api/web/global/scim/index.ts | 1 + .../types/src/api/web/global/scim/users.ts | 32 +++++++++++++++ .../routes/global/tests/scim/users.spec.ts | 41 +++++++++++++++++++ .../worker/src/tests/TestConfiguration.ts | 24 +++++++++++ packages/worker/src/tests/api/index.ts | 4 ++ packages/worker/src/tests/api/scim/users.ts | 19 +++++++++ 7 files changed, 122 insertions(+) create mode 100644 packages/types/src/api/web/global/scim/index.ts create mode 100644 packages/types/src/api/web/global/scim/users.ts create mode 100644 packages/worker/src/api/routes/global/tests/scim/users.spec.ts create mode 100644 packages/worker/src/tests/api/scim/users.ts diff --git a/packages/types/src/api/web/global/index.ts b/packages/types/src/api/web/global/index.ts index 21a5de3727..bf4b43f0ba 100644 --- a/packages/types/src/api/web/global/index.ts +++ b/packages/types/src/api/web/global/index.ts @@ -2,3 +2,4 @@ export * from "./environmentVariables" export * from "./auditLogs" export * from "./events" export * from "./configs" +export * from "./scim" diff --git a/packages/types/src/api/web/global/scim/index.ts b/packages/types/src/api/web/global/scim/index.ts new file mode 100644 index 0000000000..056d6e5675 --- /dev/null +++ b/packages/types/src/api/web/global/scim/index.ts @@ -0,0 +1 @@ +export * from "./users" diff --git a/packages/types/src/api/web/global/scim/users.ts b/packages/types/src/api/web/global/scim/users.ts new file mode 100644 index 0000000000..079436e9ae --- /dev/null +++ b/packages/types/src/api/web/global/scim/users.ts @@ -0,0 +1,32 @@ +export interface ScimUser { + schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"] + id: string + externalId: string + meta: { + resourceType: "User" + created: string + lastModified: string + } + userName: string + name: { + formatted: string + familyName: string + givenName: string + } + active: boolean + emails: [ + { + value: string + type: "work" + primary: true + } + ] +} + +export interface ScimListResponse { + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"] + totalResults: number + Resources: ScimUser[] + startIndex: number + itemsPerPage: number +} diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts new file mode 100644 index 0000000000..21bef3d5a5 --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -0,0 +1,41 @@ +import { InviteUsersResponse, User } from "@budibase/types" + +jest.mock("nodemailer") +import { TestConfiguration, mocks, structures } from "../../../../../tests" +const sendMailMock = mocks.email.mock() +import { events, tenancy, accounts as _accounts } from "@budibase/backend-core" +import * as userSdk from "../../../../../sdk/users" + +const accounts = jest.mocked(_accounts) + +describe("/api/global/scim/v2/users", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe("GET /api/global/scim/v2/users", () => { + describe("no users exist", () => { + it("should retrieve empty list", async () => { + const response = await config.api.scimUsersAPI.get() + + expect(response).toEqual({ + Resources: [], + itemsPerPage: 20, + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + startIndex: 1, + totalResults: 0, + }) + }) + }) + }) +}) diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index e612742047..c1098729b1 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -20,6 +20,9 @@ import { auth, constants, env as coreEnv, + db as dbCore, + encryption, + utils, } from "@budibase/backend-core" import structures, { CSRF_TOKEN } from "./structures" import { SaveUserResponse, User, AuthToken } from "@budibase/types" @@ -31,6 +34,7 @@ class TestConfiguration { api: API tenantId: string user?: User + apiKey?: string userPassword = "test" constructor(opts: { openServer: boolean } = { openServer: true }) { @@ -201,6 +205,12 @@ class TestConfiguration { return { [constants.Header.API_KEY]: coreEnv.INTERNAL_API_KEY } } + bearerAPIHeaders() { + return { + [constants.Header.AUTHORIZATION]: `Bearer ${this.apiKey}`, + } + } + adminOnlyResponse = () => { return { message: "Admin user only endpoint.", status: 403 } } @@ -213,6 +223,20 @@ class TestConfiguration { }) await context.doInTenant(this.tenantId!, async () => { this.user = await this.createUser(user) + + const db = context.getGlobalDB() + + const id = dbCore.generateDevInfoID(this.user._id) + // TODO: dry + this.apiKey = encryption.encrypt( + `${this.tenantId}${dbCore.SEPARATOR}${utils.newid()}` + ) + const devInfo = { + _id: id, + userId: this.user._id, + apiKey: this.apiKey, + } + await db.put(devInfo) }) } diff --git a/packages/worker/src/tests/api/index.ts b/packages/worker/src/tests/api/index.ts index 166996e792..89f38d63fb 100644 --- a/packages/worker/src/tests/api/index.ts +++ b/packages/worker/src/tests/api/index.ts @@ -15,6 +15,8 @@ import { RolesAPI } from "./roles" import { TemplatesAPI } from "./templates" import { LicenseAPI } from "./license" import { AuditLogAPI } from "./auditLogs" +import { ScimUsersAPI } from "./scim/users" + export default class API { accounts: AccountAPI auth: AuthAPI @@ -32,6 +34,7 @@ export default class API { templates: TemplatesAPI license: LicenseAPI auditLogs: AuditLogAPI + scimUsersAPI: ScimUsersAPI constructor(config: TestConfiguration) { this.accounts = new AccountAPI(config) @@ -50,5 +53,6 @@ export default class API { this.templates = new TemplatesAPI(config) this.license = new LicenseAPI(config) this.auditLogs = new AuditLogAPI(config) + this.scimUsersAPI = new ScimUsersAPI(config) } } diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts new file mode 100644 index 0000000000..fd0a99b671 --- /dev/null +++ b/packages/worker/src/tests/api/scim/users.ts @@ -0,0 +1,19 @@ +import { AccountMetadata, ScimListResponse } from "@budibase/types" +import TestConfiguration from "../../TestConfiguration" +import { TestAPI } from "../base" + +export class ScimUsersAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + get = async ({ expect }: { expect: number } = { expect: 200 }) => { + const res = await this.request + .get(`/api/global/scim/v2/users`) + .set(this.config.bearerAPIHeaders()) + .expect("Content-Type", /json/) + .expect(expect) + + return res.body as ScimListResponse + } +} From f8cebeba4eb2278089b86d7537c97c05ff515602 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 10 Mar 2023 11:05:25 +0100 Subject: [PATCH 004/135] Add create types --- .../types/src/api/web/global/scim/users.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/types/src/api/web/global/scim/users.ts b/packages/types/src/api/web/global/scim/users.ts index 079436e9ae..eba122e436 100644 --- a/packages/types/src/api/web/global/scim/users.ts +++ b/packages/types/src/api/web/global/scim/users.ts @@ -30,3 +30,54 @@ export interface ScimListResponse { startIndex: number itemsPerPage: number } + +export interface ScimUserRequest { + schemas: [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" + ] + externalId: string + userName: string + active: boolean + emails: [ + { + primary: true + type: "work" + value: string + } + ] + meta: { + resourceType: "User" + } + name: { + formatted: string + familyName: string + givenName: string + } + roles: [] +} + +export interface ScimUserResponse { + schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"] + id: string + externalId: string + meta: { + resourceType: "User" + created: string + lastModified: string + } + userName: string + name: { + formatted: string + familyName: string + givenName: string + } + active: boolean + emails: [ + { + value: string + type: "work" + primary: true + } + ] +} From b120fce5dda80323b0f05d69199ec6258c98690e Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 10 Mar 2023 11:19:22 +0100 Subject: [PATCH 005/135] Add tests --- .../routes/global/tests/scim/users.spec.ts | 35 +++++++++++++++++++ packages/worker/src/tests/api/scim/users.ts | 26 ++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 21bef3d5a5..4321d56051 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -38,4 +38,39 @@ describe("/api/global/scim/v2/users", () => { }) }) }) + + describe("POST /api/global/scim/v2/users", () => { + describe("no users exist", () => { + it("should retrieve empty list", async () => { + const body = {} as any + const response = await config.api.scimUsersAPI.post(body) + + expect(response).toEqual({ + schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], + id: "48af03ac28ad4fb88478", + externalId: "0a21f0f2-8d2a-4f8e-bf98-7363c4aed4ef", + meta: { + resourceType: "User", + created: "2018-03-27T19:59:26.000Z", + lastModified: "2018-03-27T19:59:26.000Z", + }, + userName: "Test_User_ab6490ee-1e48-479e-a20b-2d77186b5dd1", + name: { + formatted: "givenName familyName", + familyName: "familyName", + givenName: "givenName", + }, + active: true, + emails: [ + { + value: + "Test_User_fd0ea19b-0777-472c-9f96-4f70d2226f2e@testuser.com", + type: "work", + primary: true, + }, + ], + }) + }) + }) + }) }) diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index fd0a99b671..935467ac8c 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -1,4 +1,8 @@ -import { AccountMetadata, ScimListResponse } from "@budibase/types" +import { + AccountMetadata, + ScimListResponse, + ScimUserRequest, +} from "@budibase/types" import TestConfiguration from "../../TestConfiguration" import { TestAPI } from "../base" @@ -7,7 +11,7 @@ export class ScimUsersAPI extends TestAPI { super(config) } - get = async ({ expect }: { expect: number } = { expect: 200 }) => { + get = async (expect = 200) => { const res = await this.request .get(`/api/global/scim/v2/users`) .set(this.config.bearerAPIHeaders()) @@ -16,4 +20,22 @@ export class ScimUsersAPI extends TestAPI { return res.body as ScimListResponse } + + post = async ( + { + body, + }: { + body: ScimUserRequest + }, + expect = 200 + ) => { + const res = await this.request + .post(`/api/global/scim/v2/users`) + .send(body) + .set(this.config.bearerAPIHeaders()) + .expect("Content-Type", /json/) + .expect(expect) + + return res.body as ScimListResponse + } } From 7fef377e1d669d48cea136901fb7e4d7cc2d7130 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 10 Mar 2023 11:35:35 +0100 Subject: [PATCH 006/135] Improve test helpers --- packages/worker/src/tests/api/scim/users.ts | 52 ++++++++++++++++----- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index 935467ac8c..fd7f1b5fb2 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -6,18 +6,48 @@ import { import TestConfiguration from "../../TestConfiguration" import { TestAPI } from "../base" +export interface RequestSettings { + expect: number + setHeaders: boolean +} + +const defaultConfig: RequestSettings = { + expect: 200, + setHeaders: true, +} + export class ScimUsersAPI extends TestAPI { constructor(config: TestConfiguration) { super(config) } - get = async (expect = 200) => { - const res = await this.request - .get(`/api/global/scim/v2/users`) - .set(this.config.bearerAPIHeaders()) + #createRequest = ( + url: string, + method: "get" | "post", + requestSettings?: Partial, + body?: object + ) => { + const { expect, setHeaders } = { ...defaultConfig, ...requestSettings } + let request = this.request[method](url) .expect("Content-Type", /json/) .expect(expect) + if (body) { + request = request.send(body) + } + + if (setHeaders) { + request = request.set(this.config.bearerAPIHeaders()) + } + return request + } + + get = async (requestSettings?: Partial) => { + const res = await this.#createRequest( + `/api/global/scim/v2/users`, + "get", + requestSettings + ) return res.body as ScimListResponse } @@ -27,14 +57,14 @@ export class ScimUsersAPI extends TestAPI { }: { body: ScimUserRequest }, - expect = 200 + requestSettings?: Partial ) => { - const res = await this.request - .post(`/api/global/scim/v2/users`) - .send(body) - .set(this.config.bearerAPIHeaders()) - .expect("Content-Type", /json/) - .expect(expect) + const res = await this.#createRequest( + `/api/global/scim/v2/users`, + "post", + requestSettings, + body + ) return res.body as ScimListResponse } From 81e086680d4a8799c781a5cf61ff8c06a7b1e1c5 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 10 Mar 2023 12:07:18 +0100 Subject: [PATCH 007/135] Test 403s --- .../routes/global/tests/scim/users.spec.ts | 33 +++++++++++++------ packages/worker/src/tests/api/scim/users.ts | 9 ++--- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 4321d56051..364aa7f450 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -1,12 +1,4 @@ -import { InviteUsersResponse, User } from "@budibase/types" - -jest.mock("nodemailer") -import { TestConfiguration, mocks, structures } from "../../../../../tests" -const sendMailMock = mocks.email.mock() -import { events, tenancy, accounts as _accounts } from "@budibase/backend-core" -import * as userSdk from "../../../../../sdk/users" - -const accounts = jest.mocked(_accounts) +import { TestConfiguration } from "../../../../../tests" describe("/api/global/scim/v2/users", () => { const config = new TestConfiguration() @@ -24,6 +16,15 @@ describe("/api/global/scim/v2/users", () => { }) describe("GET /api/global/scim/v2/users", () => { + it("unauthorised calls are not allowed", async () => { + const response = await config.api.scimUsersAPI.get({ + setHeaders: false, + expect: 403, + }) + + expect(response).toEqual({ message: "Tenant id not set", status: 403 }) + }) + describe("no users exist", () => { it("should retrieve empty list", async () => { const response = await config.api.scimUsersAPI.get() @@ -40,8 +41,20 @@ describe("/api/global/scim/v2/users", () => { }) describe("POST /api/global/scim/v2/users", () => { + it("unauthorised calls are not allowed", async () => { + const response = await config.api.scimUsersAPI.post( + { body: {} as any }, + { + setHeaders: false, + expect: 403, + } + ) + + expect(response).toEqual({ message: "Tenant id not set", status: 403 }) + }) + describe("no users exist", () => { - it("should retrieve empty list", async () => { + it("a new user can be created", async () => { const body = {} as any const response = await config.api.scimUsersAPI.post(body) diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index fd7f1b5fb2..4a1e995a96 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -6,16 +6,13 @@ import { import TestConfiguration from "../../TestConfiguration" import { TestAPI } from "../base" -export interface RequestSettings { - expect: number - setHeaders: boolean -} - -const defaultConfig: RequestSettings = { +const defaultConfig = { expect: 200, setHeaders: true, } +type RequestSettings = typeof defaultConfig + export class ScimUsersAPI extends TestAPI { constructor(config: TestConfiguration) { super(config) From 437672a6a31452beaed449f1db728f7ab5ed6990 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 12:14:57 +0100 Subject: [PATCH 008/135] Unify interfaces --- .../types/src/api/web/global/scim/users.ts | 29 ++----------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/packages/types/src/api/web/global/scim/users.ts b/packages/types/src/api/web/global/scim/users.ts index eba122e436..f2157f3c34 100644 --- a/packages/types/src/api/web/global/scim/users.ts +++ b/packages/types/src/api/web/global/scim/users.ts @@ -1,4 +1,4 @@ -export interface ScimUser { +export interface ScimUserResponse { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"] id: string externalId: string @@ -26,7 +26,7 @@ export interface ScimUser { export interface ScimListResponse { schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"] totalResults: number - Resources: ScimUser[] + Resources: ScimUserResponse[] startIndex: number itemsPerPage: number } @@ -56,28 +56,3 @@ export interface ScimUserRequest { } roles: [] } - -export interface ScimUserResponse { - schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"] - id: string - externalId: string - meta: { - resourceType: "User" - created: string - lastModified: string - } - userName: string - name: { - formatted: string - familyName: string - givenName: string - } - active: boolean - emails: [ - { - value: string - type: "work" - primary: true - } - ] -} From 9c64f54fa99019a325c6ed93759b698f6a3c9947 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 12:20:21 +0100 Subject: [PATCH 009/135] Rename types --- packages/types/src/api/web/global/scim/users.ts | 9 ++++++--- packages/worker/src/tests/api/scim/users.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/types/src/api/web/global/scim/users.ts b/packages/types/src/api/web/global/scim/users.ts index f2157f3c34..29ae696a72 100644 --- a/packages/types/src/api/web/global/scim/users.ts +++ b/packages/types/src/api/web/global/scim/users.ts @@ -23,15 +23,18 @@ export interface ScimUserResponse { ] } -export interface ScimListResponse { +interface ScimListResponse { schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"] totalResults: number - Resources: ScimUserResponse[] + Resources: T[] startIndex: number itemsPerPage: number } -export interface ScimUserRequest { +export interface ScimUserListResponse + extends ScimListResponse {} + +export interface ScimCreateUserRequest { schemas: [ "urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index 4a1e995a96..adef49faf6 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -1,7 +1,7 @@ import { - AccountMetadata, - ScimListResponse, - ScimUserRequest, + ScimUserListResponse, + ScimCreateUserRequest, + ScimUserResponse, } from "@budibase/types" import TestConfiguration from "../../TestConfiguration" import { TestAPI } from "../base" @@ -45,14 +45,14 @@ export class ScimUsersAPI extends TestAPI { "get", requestSettings ) - return res.body as ScimListResponse + return res.body as ScimUserListResponse } post = async ( { body, }: { - body: ScimUserRequest + body: ScimCreateUserRequest }, requestSettings?: Partial ) => { @@ -63,6 +63,6 @@ export class ScimUsersAPI extends TestAPI { body ) - return res.body as ScimListResponse + return res.body as ScimUserResponse } } From 39f9ffa4e64ea4ce53942a25da86ddbb63955432 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 12:22:28 +0100 Subject: [PATCH 010/135] Renames and consistency --- packages/worker/src/api/routes/index.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/worker/src/api/routes/index.ts b/packages/worker/src/api/routes/index.ts index 1ada7d9a01..6f0bbc7cca 100644 --- a/packages/worker/src/api/routes/index.ts +++ b/packages/worker/src/api/routes/index.ts @@ -1,5 +1,5 @@ import Router from "@koa/router" -import { api } from "@budibase/pro" +import { api as pro } from "@budibase/pro" import userRoutes from "./global/users" import configRoutes from "./global/configs" import workspaceRoutes from "./global/workspaces" @@ -17,9 +17,6 @@ import migrationRoutes from "./system/migrations" import accountRoutes from "./system/accounts" import restoreRoutes from "./system/restore" -let userGroupRoutes = api.groups -let auditLogRoutes = api.auditLogs - export const routes: Router[] = [ configRoutes, userRoutes, @@ -33,11 +30,11 @@ export const routes: Router[] = [ statusRoutes, selfRoutes, licenseRoutes, - userGroupRoutes, - auditLogRoutes, + pro.groups, + pro.auditLogs, migrationRoutes, accountRoutes, restoreRoutes, eventRoutes, - ...api.scimRoutes, + ...pro.scimRoutes, ] From 2072664294e19b2203889c3a3815dadc2ed0abf9 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 10 Mar 2023 17:06:53 +0100 Subject: [PATCH 011/135] Move user search to core --- packages/backend-core/src/db/utils.ts | 6 +-- packages/backend-core/src/users.ts | 42 ++++++++++++++++++- .../src/api/controllers/global/users.ts | 2 +- packages/worker/src/sdk/users/users.ts | 39 ----------------- 4 files changed, 45 insertions(+), 44 deletions(-) diff --git a/packages/backend-core/src/db/utils.ts b/packages/backend-core/src/db/utils.ts index 76c52d08ad..441c118235 100644 --- a/packages/backend-core/src/db/utils.ts +++ b/packages/backend-core/src/db/utils.ts @@ -434,8 +434,8 @@ export const getPluginParams = (pluginId?: string | null, otherProps = {}) => { return getDocParams(DocumentType.PLUGIN, pluginId, otherProps) } -export function pagination( - data: any[], +export function pagination( + data: T[], pageSize: number, { paginate, @@ -444,7 +444,7 @@ export function pagination( }: { paginate: boolean property: string - getKey?: (doc: any) => string | undefined + getKey?: (doc: T) => string | undefined } = { paginate: true, property: "_id", diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index dfc544c3ed..c7d8a94e95 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -8,8 +8,10 @@ import { DocumentType, SEPARATOR, directCouchFind, + getGlobalUserParams, + pagination, } from "./db" -import { BulkDocsResponse, User } from "@budibase/types" +import { BulkDocsResponse, SearchUsersRequest, User } from "@budibase/types" import { getGlobalDB } from "./context" import * as context from "./context" @@ -199,3 +201,41 @@ export const searchGlobalUsersByEmail = async ( } return users } + +const PAGE_LIMIT = 8 +export const paginatedUsers = async ({ + page, + email, + appId, +}: SearchUsersRequest = {}) => { + const db = getGlobalDB() + // get one extra document, to have the next page + const opts: any = { + include_docs: true, + limit: PAGE_LIMIT + 1, + } + // add a startkey if the page was specified (anchor) + if (page) { + opts.startkey = page + } + // property specifies what to use for the page/anchor + let userList: User[], + property = "_id", + getKey + if (appId) { + userList = await searchGlobalUsersByApp(appId, opts) + getKey = (doc: any) => getGlobalUserByAppPage(appId, doc) + } else if (email) { + userList = await searchGlobalUsersByEmail(email, opts) + property = "email" + } else { + // no search, query allDocs + const response = await db.allDocs(getGlobalUserParams(null, opts)) + userList = response.rows.map((row: any) => row.doc) + } + return pagination(userList, PAGE_LIMIT, { + paginate: true, + property, + getKey, + }) +} diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index 9c95268ce4..c0855ce193 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -197,7 +197,7 @@ export const search = async (ctx: any) => { if (body.paginated === false) { await getAppUsers(ctx) } else { - const paginated = await userSdk.paginatedUsers(body) + const paginated = await userSdk.core.paginatedUsers(body) // user hashed password shouldn't ever be returned for (let user of paginated.data) { if (user) { diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 135128d816..ad7fcc95e2 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -37,8 +37,6 @@ import { EmailTemplatePurpose } from "../../constants" import * as pro from "@budibase/pro" import * as accountSdk from "../accounts" -const PAGE_LIMIT = 8 - export const allUsers = async () => { const db = tenancy.getGlobalDB() const response = await db.allDocs( @@ -68,43 +66,6 @@ export const getUsersByAppAccess = async (appId?: string) => { return response } -export const paginatedUsers = async ({ - page, - email, - appId, -}: SearchUsersRequest = {}) => { - const db = tenancy.getGlobalDB() - // get one extra document, to have the next page - const opts: any = { - include_docs: true, - limit: PAGE_LIMIT + 1, - } - // add a startkey if the page was specified (anchor) - if (page) { - opts.startkey = page - } - // property specifies what to use for the page/anchor - let userList, - property = "_id", - getKey - if (appId) { - userList = await usersCore.searchGlobalUsersByApp(appId, opts) - getKey = (doc: any) => usersCore.getGlobalUserByAppPage(appId, doc) - } else if (email) { - userList = await usersCore.searchGlobalUsersByEmail(email, opts) - property = "email" - } else { - // no search, query allDocs - const response = await db.allDocs(dbUtils.getGlobalUserParams(null, opts)) - userList = response.rows.map((row: any) => row.doc) - } - return dbUtils.pagination(userList, PAGE_LIMIT, { - paginate: true, - property, - getKey, - }) -} - export async function getUserByEmail(email: string) { return usersCore.getGlobalUserByEmail(email) } From 21f01a53c5fe2fe9a9b6552dd538560a51e9d70a Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 10 Mar 2023 17:32:25 +0100 Subject: [PATCH 012/135] Add isScimSync flag --- packages/types/src/documents/global/user.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index af48dbb758..c67b9f87da 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -53,6 +53,10 @@ export interface User extends Document { dayPassRecordedAt?: string userGroups?: string[] onboardedAt?: string + scimInfo?: { + isSync: boolean + firstSync: number + } } export enum UserStatus { From f8396725d1d37e1b3d8e01e41af51d764ff6307e Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 13 Mar 2023 14:25:19 +0100 Subject: [PATCH 013/135] Init pro with the save user function --- packages/worker/src/index.ts | 2 ++ packages/worker/src/initPro.ts | 12 ++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 packages/worker/src/initPro.ts diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 0b4e3be817..5084972a4c 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -35,6 +35,7 @@ const logger = require("koa-pino-logger") const { userAgent } = require("koa-useragent") import destroyable from "server-destroy" +import { initPro } from "./initPro" // configure events to use the pro audit log write // can't integrate directly into backend-core due to cyclic issues @@ -108,6 +109,7 @@ const shutdown = () => { export default server.listen(parseInt(env.PORT || "4002"), async () => { console.log(`Worker running on ${JSON.stringify(server.address())}`) + await initPro() await redis.init() }) diff --git a/packages/worker/src/initPro.ts b/packages/worker/src/initPro.ts new file mode 100644 index 0000000000..793b8dd8b5 --- /dev/null +++ b/packages/worker/src/initPro.ts @@ -0,0 +1,12 @@ +import { sdk as proSdk } from "@budibase/pro" +import * as userSdk from "./sdk/users" + +export const initPro = async () => { + await proSdk.init({ + scimUserServiceConfig: { + functions: { + saveUser: userSdk.save, + }, + }, + }) +} From 3e68e8ebe8c6ba1ff0bba23bf85867ffa3e3f832 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 13 Mar 2023 15:25:27 +0100 Subject: [PATCH 014/135] Add tests --- packages/types/src/documents/global/user.ts | 1 + .../routes/global/tests/scim/users.spec.ts | 65 +++++++++++++++---- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index c67b9f87da..31e68e1a10 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -54,6 +54,7 @@ export interface User extends Document { userGroups?: string[] onboardedAt?: string scimInfo?: { + externalId: string isSync: boolean firstSync: number } diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 364aa7f450..80f174d46d 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -1,6 +1,17 @@ +import tk from "timekeeper" +import { structures } from "@budibase/backend-core/tests" +import { ScimUserRequest } from "@budibase/types" import { TestConfiguration } from "../../../../../tests" describe("/api/global/scim/v2/users", () => { + let mockedTime = new Date(structures.generator.timestamp()) + + beforeEach(() => { + tk.reset() + mockedTime = new Date(structures.generator.timestamp()) + tk.freeze(mockedTime) + }) + const config = new TestConfiguration() beforeAll(async () => { @@ -55,29 +66,59 @@ describe("/api/global/scim/v2/users", () => { describe("no users exist", () => { it("a new user can be created", async () => { - const body = {} as any - const response = await config.api.scimUsersAPI.post(body) + const userData = { + externalId: structures.uuid(), + email: structures.generator.email(), + firstName: structures.generator.first(), + lastName: structures.generator.last(), + } + const body: ScimUserRequest = { + schemas: [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", + ], + externalId: userData.externalId, + userName: structures.generator.name(), + active: true, + emails: [ + { + primary: true, + type: "work", + value: userData.email, + }, + ], + meta: { + resourceType: "User", + }, + name: { + formatted: structures.generator.name(), + familyName: userData.lastName, + givenName: userData.firstName, + }, + roles: [], + } + + const response = await config.api.scimUsersAPI.post({ body }) expect(response).toEqual({ schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], - id: "48af03ac28ad4fb88478", - externalId: "0a21f0f2-8d2a-4f8e-bf98-7363c4aed4ef", + id: expect.any(String), + externalId: userData.externalId, meta: { resourceType: "User", - created: "2018-03-27T19:59:26.000Z", - lastModified: "2018-03-27T19:59:26.000Z", + created: mockedTime.toISOString(), + lastModified: mockedTime.toISOString(), }, - userName: "Test_User_ab6490ee-1e48-479e-a20b-2d77186b5dd1", + userName: `${userData.firstName} ${userData.lastName}`, name: { - formatted: "givenName familyName", - familyName: "familyName", - givenName: "givenName", + formatted: `${userData.firstName} ${userData.lastName}`, + familyName: userData.lastName, + givenName: userData.firstName, }, active: true, emails: [ { - value: - "Test_User_fd0ea19b-0777-472c-9f96-4f70d2226f2e@testuser.com", + value: userData.email, type: "work", primary: true, }, From 2c157c0feb9bff48aa94137ca59b959b12476466 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 13 Mar 2023 15:28:01 +0100 Subject: [PATCH 015/135] Test persistation --- .../api/routes/global/tests/scim/users.spec.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 80f174d46d..e32fb5cf1f 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -65,7 +65,7 @@ describe("/api/global/scim/v2/users", () => { }) describe("no users exist", () => { - it("a new user can be created", async () => { + it("a new user can be created and persisted", async () => { const userData = { externalId: structures.uuid(), email: structures.generator.email(), @@ -100,7 +100,7 @@ describe("/api/global/scim/v2/users", () => { const response = await config.api.scimUsersAPI.post({ body }) - expect(response).toEqual({ + const expectedScimUser = { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], id: expect.any(String), externalId: userData.externalId, @@ -123,7 +123,16 @@ describe("/api/global/scim/v2/users", () => { primary: true, }, ], - }) + } + expect(response).toEqual(expectedScimUser) + + const persistedUsers = await config.api.scimUsersAPI.get() + expect(persistedUsers).toEqual( + expect.objectContaining({ + totalResults: 1, + Resources: [expectedScimUser], + }) + ) }) }) }) From a5b23c40672054b7bfe49747c0575a390c7777db Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 12:27:53 +0100 Subject: [PATCH 016/135] Fix types --- .../worker/src/api/routes/global/tests/scim/users.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index e32fb5cf1f..e39f8fc94e 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -1,6 +1,6 @@ import tk from "timekeeper" import { structures } from "@budibase/backend-core/tests" -import { ScimUserRequest } from "@budibase/types" +import { ScimCreateUserRequest } from "@budibase/types" import { TestConfiguration } from "../../../../../tests" describe("/api/global/scim/v2/users", () => { @@ -72,7 +72,7 @@ describe("/api/global/scim/v2/users", () => { firstName: structures.generator.first(), lastName: structures.generator.last(), } - const body: ScimUserRequest = { + const body: ScimCreateUserRequest = { schemas: [ "urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", From 97e181fffe4f8296a78aa20a47ad20d58b73e656 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 13 Mar 2023 16:27:45 +0100 Subject: [PATCH 017/135] Add feature --- packages/types/src/sdk/licensing/feature.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/types/src/sdk/licensing/feature.ts b/packages/types/src/sdk/licensing/feature.ts index 1ff4f8febb..7af42e9d28 100644 --- a/packages/types/src/sdk/licensing/feature.ts +++ b/packages/types/src/sdk/licensing/feature.ts @@ -5,4 +5,5 @@ export enum Feature { AUDIT_LOGS = "auditLogs", ENFORCEABLE_SSO = "enforceableSSO", BRANDING = "branding", + SCIM_INTEGRATION = "scimIntegration", } From f62647f284a7941e7e6e5992c49eaafd5dfead15 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 13 Mar 2023 16:34:08 +0100 Subject: [PATCH 018/135] Feature tests --- .../tests/utilities/mocks/licenses.ts | 4 ++ .../routes/global/tests/scim/users.spec.ts | 40 ++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/backend-core/tests/utilities/mocks/licenses.ts b/packages/backend-core/tests/utilities/mocks/licenses.ts index 2ca41616e4..85bf9c8a35 100644 --- a/packages/backend-core/tests/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/utilities/mocks/licenses.ts @@ -86,6 +86,10 @@ export const useAuditLogs = () => { return useFeature(Feature.AUDIT_LOGS) } +export const useScimIntegration = () => { + return useFeature(Feature.SCIM_INTEGRATION) +} + // QUOTAS export const setAutomationLogsQuota = (value: number) => { diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index e39f8fc94e..6d3e04213e 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -1,5 +1,5 @@ import tk from "timekeeper" -import { structures } from "@budibase/backend-core/tests" +import { mocks, structures } from "@budibase/backend-core/tests" import { ScimCreateUserRequest } from "@budibase/types" import { TestConfiguration } from "../../../../../tests" @@ -10,6 +10,8 @@ describe("/api/global/scim/v2/users", () => { tk.reset() mockedTime = new Date(structures.generator.timestamp()) tk.freeze(mockedTime) + + mocks.licenses.useScimIntegration() }) const config = new TestConfiguration() @@ -26,9 +28,21 @@ describe("/api/global/scim/v2/users", () => { jest.clearAllMocks() }) + const featureDisabledResponse = { + error: { + code: "feature_disabled", + featureName: "scimIntegration", + type: "license_error", + }, + message: "scimIntegration is not currently enabled", + status: 400, + } + describe("GET /api/global/scim/v2/users", () => { + const getScimUsers = config.api.scimUsersAPI.get + it("unauthorised calls are not allowed", async () => { - const response = await config.api.scimUsersAPI.get({ + const response = await getScimUsers({ setHeaders: false, expect: 403, }) @@ -36,9 +50,16 @@ describe("/api/global/scim/v2/users", () => { expect(response).toEqual({ message: "Tenant id not set", status: 403 }) }) + it("cannot be called when feature is disabled", async () => { + mocks.licenses.useCloudFree() + const response = await getScimUsers({ expect: 400 }) + + expect(response).toEqual(featureDisabledResponse) + }) + describe("no users exist", () => { it("should retrieve empty list", async () => { - const response = await config.api.scimUsersAPI.get() + const response = await getScimUsers() expect(response).toEqual({ Resources: [], @@ -52,8 +73,10 @@ describe("/api/global/scim/v2/users", () => { }) describe("POST /api/global/scim/v2/users", () => { + const postScimUser = config.api.scimUsersAPI.post + it("unauthorised calls are not allowed", async () => { - const response = await config.api.scimUsersAPI.post( + const response = await postScimUser( { body: {} as any }, { setHeaders: false, @@ -64,6 +87,13 @@ describe("/api/global/scim/v2/users", () => { expect(response).toEqual({ message: "Tenant id not set", status: 403 }) }) + it("cannot be called when feature is disabled", async () => { + mocks.licenses.useCloudFree() + const response = await postScimUser({ body: {} as any }, { expect: 400 }) + + expect(response).toEqual(featureDisabledResponse) + }) + describe("no users exist", () => { it("a new user can be created and persisted", async () => { const userData = { @@ -98,7 +128,7 @@ describe("/api/global/scim/v2/users", () => { roles: [], } - const response = await config.api.scimUsersAPI.post({ body }) + const response = await postScimUser({ body }) const expectedScimUser = { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"], From afdefce55d3eabf62704751870ab0259f088bdb1 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 13 Mar 2023 17:44:29 +0100 Subject: [PATCH 019/135] Add find endpoint tests --- .../routes/global/tests/scim/users.spec.ts | 103 +++++++++++++----- packages/worker/src/tests/api/scim/users.ts | 9 ++ 2 files changed, 86 insertions(+), 26 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 6d3e04213e..86339f4ea6 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -1,8 +1,49 @@ import tk from "timekeeper" import { mocks, structures } from "@budibase/backend-core/tests" -import { ScimCreateUserRequest } from "@budibase/types" +import { ScimCreateUserRequest, ScimUserResponse } from "@budibase/types" import { TestConfiguration } from "../../../../../tests" +function createScimCreateUserRequest(userData?: { + externalId?: string + email?: string + firstName?: string + lastName?: string +}) { + const { + externalId = structures.uuid(), + email = structures.generator.email(), + firstName = structures.generator.first(), + lastName = structures.generator.last(), + } = userData || {} + + const user: ScimCreateUserRequest = { + schemas: [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", + ], + externalId, + userName: structures.generator.name(), + active: true, + emails: [ + { + primary: true, + type: "work", + value: email, + }, + ], + meta: { + resourceType: "User", + }, + name: { + formatted: structures.generator.name(), + familyName: lastName, + givenName: firstName, + }, + roles: [], + } + return user +} + describe("/api/global/scim/v2/users", () => { let mockedTime = new Date(structures.generator.timestamp()) @@ -102,31 +143,7 @@ describe("/api/global/scim/v2/users", () => { firstName: structures.generator.first(), lastName: structures.generator.last(), } - const body: ScimCreateUserRequest = { - schemas: [ - "urn:ietf:params:scim:schemas:core:2.0:User", - "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", - ], - externalId: userData.externalId, - userName: structures.generator.name(), - active: true, - emails: [ - { - primary: true, - type: "work", - value: userData.email, - }, - ], - meta: { - resourceType: "User", - }, - name: { - formatted: structures.generator.name(), - familyName: userData.lastName, - givenName: userData.firstName, - }, - roles: [], - } + const body = createScimCreateUserRequest(userData) const response = await postScimUser({ body }) @@ -166,4 +183,38 @@ describe("/api/global/scim/v2/users", () => { }) }) }) + + describe("GET /api/global/scim/v2/users/:id", () => { + let user: ScimUserResponse + + beforeEach(async () => { + const body = createScimCreateUserRequest() + + user = await config.api.scimUsersAPI.post({ body }) + }) + + const findScimUser = config.api.scimUsersAPI.find + + it("unauthorised calls are not allowed", async () => { + const response = await findScimUser(user.id, { + setHeaders: false, + expect: 403, + }) + + expect(response).toEqual({ message: "Tenant id not set", status: 403 }) + }) + + it("cannot be called when feature is disabled", async () => { + mocks.licenses.useCloudFree() + const response = await findScimUser(user.id, { expect: 400 }) + + expect(response).toEqual(featureDisabledResponse) + }) + + it("should return existing user", async () => { + const response = await findScimUser(user.id) + + expect(response).toEqual(user) + }) + }) }) diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index adef49faf6..34fdf5f95a 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -48,6 +48,15 @@ export class ScimUsersAPI extends TestAPI { return res.body as ScimUserListResponse } + find = async (id: string, requestSettings?: Partial) => { + const res = await this.#createRequest( + `/api/global/scim/v2/users/${id}`, + "get", + requestSettings + ) + return res.body as ScimUserResponse + } + post = async ( { body, From 263d3613be8300bb4342976cfdc82d1775480f2e Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 13 Mar 2023 17:46:29 +0100 Subject: [PATCH 020/135] Test 404 --- .../src/api/routes/global/tests/scim/users.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 86339f4ea6..b86eb790ed 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -216,5 +216,14 @@ describe("/api/global/scim/v2/users", () => { expect(response).toEqual(user) }) + + it("should return 404 when requesting unexisting user id", async () => { + const response = await findScimUser(structures.uuid(), { expect: 404 }) + + expect(response).toEqual({ + message: "missing", + status: 404, + }) + }) }) }) From a509dc1739434541df31a0bae2cc1ce629eea7b2 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 13 Mar 2023 17:44:29 +0100 Subject: [PATCH 021/135] Add find endpoint tests --- packages/worker/src/tests/api/scim/users.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index 34fdf5f95a..3daf0f4fad 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -57,6 +57,15 @@ export class ScimUsersAPI extends TestAPI { return res.body as ScimUserResponse } + find = async (id: string, requestSettings?: Partial) => { + const res = await this.#createRequest( + `/api/global/scim/v2/users/${id}`, + "get", + requestSettings + ) + return res.body as ScimUser + } + post = async ( { body, From 7c719df895211d2528d407d53dc483d363f88912 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 13 Mar 2023 16:57:19 +0100 Subject: [PATCH 022/135] Add update endpoint --- .../types/src/api/web/global/scim/users.ts | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/types/src/api/web/global/scim/users.ts b/packages/types/src/api/web/global/scim/users.ts index 29ae696a72..155b906489 100644 --- a/packages/types/src/api/web/global/scim/users.ts +++ b/packages/types/src/api/web/global/scim/users.ts @@ -23,17 +23,6 @@ export interface ScimUserResponse { ] } -interface ScimListResponse { - schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"] - totalResults: number - Resources: T[] - startIndex: number - itemsPerPage: number -} - -export interface ScimUserListResponse - extends ScimListResponse {} - export interface ScimCreateUserRequest { schemas: [ "urn:ietf:params:scim:schemas:core:2.0:User", @@ -59,3 +48,25 @@ export interface ScimCreateUserRequest { } roles: [] } + +export interface ScimUpdateRequest { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"] + Operations: [ + { + op: "add" | "replace" | "remove" + path: string + value: string + } + ] +} + +interface ScimListResponse { + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"] + totalResults: number + Resources: T[] + startIndex: number + itemsPerPage: number +} + +export interface ScimUserListResponse + extends ScimListResponse {} From 3500aabc8a61ca3fc8a1357b285aae036099f322 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 13 Mar 2023 17:24:36 +0100 Subject: [PATCH 023/135] Patch endpoint --- .../routes/global/tests/scim/users.spec.ts | 54 +++++++++++++++++++ packages/worker/src/tests/api/scim/users.ts | 22 +++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index b86eb790ed..041f2f88e7 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -226,4 +226,58 @@ describe("/api/global/scim/v2/users", () => { }) }) }) + + describe("PATCH /api/global/scim/v2/users", () => { + const patchScimUser = config.api.scimUsersAPI.patch + + let user: ScimUser + + beforeEach(async () => { + const body = createScimCreateUserRequest() + + user = await config.api.scimUsersAPI.post({ body }) + }) + + it("unauthorised calls are not allowed", async () => { + const response = await patchScimUser({} as any, { + setHeaders: false, + expect: 403, + }) + + expect(response).toEqual({ message: "Tenant id not set", status: 403 }) + }) + + it("cannot be called when feature is disabled", async () => { + mocks.licenses.useCloudFree() + const response = await patchScimUser({} as any, { expect: 400 }) + + expect(response).toEqual(featureDisabledResponse) + }) + + it("an existing user can be updated", async () => { + const body: ScimUpdateRequest = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [ + { + op: "add", + path: "string", + value: "string", + }, + ], + } + + const response = await patchScimUser({ id: user.id, body }) + + const expectedScimUser = { ...user } + expect(response).toEqual(expectedScimUser) + + const persistedUsers = await config.api.scimUsersAPI.get() + expect(persistedUsers).toEqual( + expect.objectContaining({ + totalResults: 1, + Resources: [expectedScimUser], + }) + ) + }) + }) }) diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index 3daf0f4fad..f0ebc9e6b6 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -20,7 +20,7 @@ export class ScimUsersAPI extends TestAPI { #createRequest = ( url: string, - method: "get" | "post", + method: "get" | "post" | "patch", requestSettings?: Partial, body?: object ) => { @@ -83,4 +83,24 @@ export class ScimUsersAPI extends TestAPI { return res.body as ScimUserResponse } + + patch = async ( + { + id, + body, + }: { + id: string + body: ScimUpdateRequest + }, + requestSettings?: Partial + ) => { + const res = await this.#createRequest( + `/api/global/scim/v2/users/${id}`, + "patch", + requestSettings, + body + ) + + return res.body as ScimUser + } } From 40a1921f022dde4f02dedab080de0da50930e3e9 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 10:15:44 +0100 Subject: [PATCH 024/135] Use scim-patch package --- packages/types/package.json | 3 +++ .../types/src/api/web/global/scim/users.ts | 16 +++++----------- packages/types/yarn.lock | 18 ++++++++++++++++++ .../api/routes/global/tests/scim/users.spec.ts | 9 ++------- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/types/package.json b/packages/types/package.json index fb82d7633e..db7c274096 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -29,5 +29,8 @@ "koa-body": "4.2.0", "rimraf": "3.0.2", "typescript": "4.7.3" + }, + "dependencies": { + "scim-patch": "^0.7.0" } } diff --git a/packages/types/src/api/web/global/scim/users.ts b/packages/types/src/api/web/global/scim/users.ts index 155b906489..8748b4e8e6 100644 --- a/packages/types/src/api/web/global/scim/users.ts +++ b/packages/types/src/api/web/global/scim/users.ts @@ -1,11 +1,11 @@ -export interface ScimUserResponse { +import { ScimResource, ScimMeta, ScimPatchOperation } from "scim-patch" + +export interface ScimUserResponse extends ScimResource { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"] id: string externalId: string - meta: { + meta: ScimMeta & { resourceType: "User" - created: string - lastModified: string } userName: string name: { @@ -51,13 +51,7 @@ export interface ScimCreateUserRequest { export interface ScimUpdateRequest { schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"] - Operations: [ - { - op: "add" | "replace" | "remove" - path: string - value: string - } - ] + Operations: ScimPatchOperation[] } interface ScimListResponse { diff --git a/packages/types/yarn.lock b/packages/types/yarn.lock index 64aaf584a4..c59a37107c 100644 --- a/packages/types/yarn.lock +++ b/packages/types/yarn.lock @@ -487,6 +487,11 @@ escalade@^3.1.1: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +fast-deep-equal@3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" @@ -743,6 +748,19 @@ rxjs@^7.0.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +scim-patch@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/scim-patch/-/scim-patch-0.7.0.tgz#3f6d94256c07be415a74a49c0ff48dc91e4e0219" + integrity sha512-wXKcsZl+aLfE0yId7MjiOd91v8as6dEYLFvm1gGu3yJxSPhl1Fl3vWiNN4V3D68UKpqO/umK5rwWc8wGpBaOHw== + dependencies: + fast-deep-equal "3.1.3" + scim2-parse-filter "0.2.8" + +scim2-parse-filter@0.2.8: + version "0.2.8" + resolved "https://registry.yarnpkg.com/scim2-parse-filter/-/scim2-parse-filter-0.2.8.tgz#12e836514b9a55ae51218dd6e7fbea91daccfa4d" + integrity sha512-1V+6FIMIiP+gDiFkC3dIw86KfoXhnQRXhfPaiQImeeFukpLtEkTtYq/Vmy1yDgHQcIHQxQQqOWyGLKX0FTvvaA== + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 041f2f88e7..b1727e99fd 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -271,13 +271,8 @@ describe("/api/global/scim/v2/users", () => { const expectedScimUser = { ...user } expect(response).toEqual(expectedScimUser) - const persistedUsers = await config.api.scimUsersAPI.get() - expect(persistedUsers).toEqual( - expect.objectContaining({ - totalResults: 1, - Resources: [expectedScimUser], - }) - ) + const persistedUser = await config.api.scimUsersAPI.find(user.id) + expect(persistedUser).toEqual(expectedScimUser) }) }) }) From 24d2937d0b245ca9cd31363a11228928fea66fb6 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 10:47:41 +0100 Subject: [PATCH 025/135] Implement patch tests --- packages/types/src/documents/global/user.ts | 1 + .../routes/global/tests/scim/users.spec.ts | 30 +++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index 31e68e1a10..39f0de9507 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -54,6 +54,7 @@ export interface User extends Document { userGroups?: string[] onboardedAt?: string scimInfo?: { + username: string externalId: string isSync: boolean firstSync: number diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index b1727e99fd..fda5e9d9fc 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -8,12 +8,14 @@ function createScimCreateUserRequest(userData?: { email?: string firstName?: string lastName?: string + username?: string }) { const { externalId = structures.uuid(), email = structures.generator.email(), firstName = structures.generator.first(), lastName = structures.generator.last(), + username = structures.generator.name(), } = userData || {} const user: ScimCreateUserRequest = { @@ -22,7 +24,7 @@ function createScimCreateUserRequest(userData?: { "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", ], externalId, - userName: structures.generator.name(), + userName: username, active: true, emails: [ { @@ -142,6 +144,7 @@ describe("/api/global/scim/v2/users", () => { email: structures.generator.email(), firstName: structures.generator.first(), lastName: structures.generator.last(), + username: structures.generator.name(), } const body = createScimCreateUserRequest(userData) @@ -156,7 +159,7 @@ describe("/api/global/scim/v2/users", () => { created: mockedTime.toISOString(), lastModified: mockedTime.toISOString(), }, - userName: `${userData.firstName} ${userData.lastName}`, + userName: userData.username, name: { formatted: `${userData.firstName} ${userData.lastName}`, familyName: userData.lastName, @@ -255,20 +258,35 @@ describe("/api/global/scim/v2/users", () => { }) it("an existing user can be updated", async () => { + const newUserName = structures.generator.name() + const newFamilyName = structures.generator.last() const body: ScimUpdateRequest = { schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], Operations: [ { - op: "add", - path: "string", - value: "string", + op: "Replace", + path: "userName", + value: newUserName, + }, + { + op: "Replace", + path: "name.familyName", + value: newFamilyName, }, ], } const response = await patchScimUser({ id: user.id, body }) - const expectedScimUser = { ...user } + const expectedScimUser: ScimUser = { + ...user, + userName: newUserName, + name: { + ...user.name, + familyName: newFamilyName, + formatted: `${user.name.givenName} ${newFamilyName}`, + }, + } expect(response).toEqual(expectedScimUser) const persistedUser = await config.api.scimUsersAPI.find(user.id) From 89957f549002464e663c5d525052b00947409e45 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 15:08:54 +0100 Subject: [PATCH 026/135] Fix merge conflicts --- .../src/api/routes/global/tests/scim/users.spec.ts | 10 +++++++--- packages/worker/src/tests/api/scim/users.ts | 12 ++---------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index fda5e9d9fc..36259e1427 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -1,6 +1,10 @@ import tk from "timekeeper" import { mocks, structures } from "@budibase/backend-core/tests" -import { ScimCreateUserRequest, ScimUserResponse } from "@budibase/types" +import { + ScimCreateUserRequest, + ScimUpdateRequest, + ScimUserResponse, +} from "@budibase/types" import { TestConfiguration } from "../../../../../tests" function createScimCreateUserRequest(userData?: { @@ -233,7 +237,7 @@ describe("/api/global/scim/v2/users", () => { describe("PATCH /api/global/scim/v2/users", () => { const patchScimUser = config.api.scimUsersAPI.patch - let user: ScimUser + let user: ScimUserResponse beforeEach(async () => { const body = createScimCreateUserRequest() @@ -278,7 +282,7 @@ describe("/api/global/scim/v2/users", () => { const response = await patchScimUser({ id: user.id, body }) - const expectedScimUser: ScimUser = { + const expectedScimUser: ScimUserResponse = { ...user, userName: newUserName, name: { diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index f0ebc9e6b6..cac310dcba 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -2,6 +2,7 @@ import { ScimUserListResponse, ScimCreateUserRequest, ScimUserResponse, + ScimUpdateRequest, } from "@budibase/types" import TestConfiguration from "../../TestConfiguration" import { TestAPI } from "../base" @@ -57,15 +58,6 @@ export class ScimUsersAPI extends TestAPI { return res.body as ScimUserResponse } - find = async (id: string, requestSettings?: Partial) => { - const res = await this.#createRequest( - `/api/global/scim/v2/users/${id}`, - "get", - requestSettings - ) - return res.body as ScimUser - } - post = async ( { body, @@ -101,6 +93,6 @@ export class ScimUsersAPI extends TestAPI { body ) - return res.body as ScimUser + return res.body as ScimUserResponse } } From 4f9b5a6aea393de4bc9a71f40b3c2632162d1436 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 13 Mar 2023 17:24:36 +0100 Subject: [PATCH 027/135] Patch endpoint --- packages/worker/src/tests/api/scim/users.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index cac310dcba..e1f9616c23 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -95,4 +95,24 @@ export class ScimUsersAPI extends TestAPI { return res.body as ScimUserResponse } + + patch = async ( + { + id, + body, + }: { + id: string + body: ScimUpdateRequest + }, + requestSettings?: Partial + ) => { + const res = await this.#createRequest( + `/api/global/scim/v2/users/${id}`, + "patch", + requestSettings, + body + ) + + return res.body as ScimUser + } } From fbd53d5fd3bb9890b800b8ffec0a260ace512a7a Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 11:47:40 +0100 Subject: [PATCH 028/135] Add delete test --- .../routes/global/tests/scim/users.spec.ts | 38 ++++++++++++++++++- packages/worker/src/initPro.ts | 1 + packages/worker/src/tests/api/scim/users.ts | 19 ++++++++-- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 36259e1427..73853dea1b 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -234,7 +234,7 @@ describe("/api/global/scim/v2/users", () => { }) }) - describe("PATCH /api/global/scim/v2/users", () => { + describe("PATCH /api/global/scim/v2/users/:id", () => { const patchScimUser = config.api.scimUsersAPI.patch let user: ScimUserResponse @@ -297,4 +297,40 @@ describe("/api/global/scim/v2/users", () => { expect(persistedUser).toEqual(expectedScimUser) }) }) + + describe("DELETE /api/global/scim/v2/users/:id", () => { + const deleteScimUser = config.api.scimUsersAPI.delete + + let user: ScimUser + + beforeEach(async () => { + const body = createScimCreateUserRequest() + + user = await config.api.scimUsersAPI.post({ body }) + }) + + it("unauthorised calls are not allowed", async () => { + const response = await deleteScimUser({} as any, { + setHeaders: false, + expect: 403, + }) + + expect(response).toEqual({ message: "Tenant id not set", status: 403 }) + }) + + it("cannot be called when feature is disabled", async () => { + mocks.licenses.useCloudFree() + const response = await deleteScimUser({} as any, { expect: 400 }) + + expect(response).toEqual(featureDisabledResponse) + }) + + it("an existing user can be deleted", async () => { + const response = await deleteScimUser(user.id, { expect: 204 }) + + expect(response).toEqual({}) + + await config.api.scimUsersAPI.find(user.id, { expect: 404 }) + }) + }) }) diff --git a/packages/worker/src/initPro.ts b/packages/worker/src/initPro.ts index 793b8dd8b5..44dc99a589 100644 --- a/packages/worker/src/initPro.ts +++ b/packages/worker/src/initPro.ts @@ -6,6 +6,7 @@ export const initPro = async () => { scimUserServiceConfig: { functions: { saveUser: userSdk.save, + removeUser: (id: string) => userSdk.destroy(id, undefined), }, }, }) diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index e1f9616c23..040ad4d14e 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -21,15 +21,19 @@ export class ScimUsersAPI extends TestAPI { #createRequest = ( url: string, - method: "get" | "post" | "patch", + method: "get" | "post" | "patch" | "delete", requestSettings?: Partial, body?: object ) => { const { expect, setHeaders } = { ...defaultConfig, ...requestSettings } - let request = this.request[method](url) - .expect("Content-Type", /json/) + let request = + this.request[method](url) .expect(expect) + if (method !== "delete") { + request = request.expect("Content-Type", /json/) + } + if (body) { request = request.send(body) } @@ -115,4 +119,13 @@ export class ScimUsersAPI extends TestAPI { return res.body as ScimUser } + + delete = async (id: string, requestSettings?: Partial) => { + const res = await this.#createRequest( + `/api/global/scim/v2/users/${id}`, + "delete", + requestSettings + ) + return res.body as ScimUser + } } From 829aee1f686c7939235740cb5857be1f6398ca93 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 11:49:39 +0100 Subject: [PATCH 029/135] Add tests --- .../worker/src/api/routes/global/tests/scim/users.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 73853dea1b..bbf8f8eea2 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -310,7 +310,7 @@ describe("/api/global/scim/v2/users", () => { }) it("unauthorised calls are not allowed", async () => { - const response = await deleteScimUser({} as any, { + const response = await deleteScimUser(user.id, { setHeaders: false, expect: 403, }) @@ -320,7 +320,7 @@ describe("/api/global/scim/v2/users", () => { it("cannot be called when feature is disabled", async () => { mocks.licenses.useCloudFree() - const response = await deleteScimUser({} as any, { expect: 400 }) + const response = await deleteScimUser(user.id, { expect: 400 }) expect(response).toEqual(featureDisabledResponse) }) @@ -332,5 +332,9 @@ describe("/api/global/scim/v2/users", () => { await config.api.scimUsersAPI.find(user.id, { expect: 404 }) }) + + it("an non existing user can not be deleted", async () => { + await deleteScimUser(structures.uuid(), { expect: 404 }) + }) }) }) From f8959aacb049312873ffe99143f2be8e16990230 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 15:00:02 +0100 Subject: [PATCH 030/135] Fix merge conflicts --- .../routes/global/tests/scim/users.spec.ts | 2 +- packages/worker/src/tests/api/scim/users.ts | 26 ++----------------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index bbf8f8eea2..f2188c2909 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -301,7 +301,7 @@ describe("/api/global/scim/v2/users", () => { describe("DELETE /api/global/scim/v2/users/:id", () => { const deleteScimUser = config.api.scimUsersAPI.delete - let user: ScimUser + let user: ScimUserResponse beforeEach(async () => { const body = createScimCreateUserRequest() diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index 040ad4d14e..ec61504db3 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -26,9 +26,7 @@ export class ScimUsersAPI extends TestAPI { body?: object ) => { const { expect, setHeaders } = { ...defaultConfig, ...requestSettings } - let request = - this.request[method](url) - .expect(expect) + let request = this.request[method](url).expect(expect) if (method !== "delete") { request = request.expect("Content-Type", /json/) @@ -100,32 +98,12 @@ export class ScimUsersAPI extends TestAPI { return res.body as ScimUserResponse } - patch = async ( - { - id, - body, - }: { - id: string - body: ScimUpdateRequest - }, - requestSettings?: Partial - ) => { - const res = await this.#createRequest( - `/api/global/scim/v2/users/${id}`, - "patch", - requestSettings, - body - ) - - return res.body as ScimUser - } - delete = async (id: string, requestSettings?: Partial) => { const res = await this.#createRequest( `/api/global/scim/v2/users/${id}`, "delete", requestSettings ) - return res.body as ScimUser + return res.body as ScimUserResponse } } From 4f2696ed32c47b72a52eaa8b7495990fe585b340 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 15:46:50 +0100 Subject: [PATCH 031/135] Use ctx.identity instead of passing it as param --- packages/worker/src/api/controllers/global/users.ts | 2 +- packages/worker/src/initPro.ts | 2 +- packages/worker/src/sdk/users/users.ts | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/worker/src/api/controllers/global/users.ts b/packages/worker/src/api/controllers/global/users.ts index c0855ce193..1b063599ab 100644 --- a/packages/worker/src/api/controllers/global/users.ts +++ b/packages/worker/src/api/controllers/global/users.ts @@ -177,7 +177,7 @@ export const destroy = async (ctx: any) => { ctx.throw(400, "Unable to delete self.") } - await userSdk.destroy(id, ctx.user) + await userSdk.destroy(id) ctx.body = { message: `User ${id} deleted.`, diff --git a/packages/worker/src/initPro.ts b/packages/worker/src/initPro.ts index 44dc99a589..3c144a5c83 100644 --- a/packages/worker/src/initPro.ts +++ b/packages/worker/src/initPro.ts @@ -6,7 +6,7 @@ export const initPro = async () => { scimUserServiceConfig: { functions: { saveUser: userSdk.save, - removeUser: (id: string) => userSdk.destroy(id, undefined), + removeUser: (id: string) => userSdk.destroy(id), }, }, }) diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index ad7fcc95e2..f05c6b98d2 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -15,6 +15,7 @@ import { utils, ViewName, env as coreEnv, + context, } from "@budibase/backend-core" import { AccountMetadata, @@ -537,7 +538,7 @@ export const bulkDelete = async ( return response } -export const destroy = async (id: string, currentUser: any) => { +export const destroy = async (id: string) => { const db = tenancy.getGlobalDB() const dbUser = (await db.get(id)) as User const userId = dbUser._id as string @@ -547,7 +548,7 @@ export const destroy = async (id: string, currentUser: any) => { const email = dbUser.email const account = await accounts.getAccount(email) if (account) { - if (email === currentUser.email) { + if (dbUser.userId === context.getIdentity()!._id) { throw new HTTPError('Please visit "Account" to delete this user', 400) } else { throw new HTTPError("Account holder cannot be deleted", 400) From 621c06eadaf3f4aa733da712eb59be3bedc2983e Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 17:02:37 +0100 Subject: [PATCH 032/135] Add view --- packages/backend-core/src/constants/db.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index d41098c405..4fe7b1233a 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -22,6 +22,7 @@ export enum ViewName { PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", USER_BY_GROUP = "by_group_user", APP_BACKUP_BY_TRIGGER = "by_trigger", + SCIM_USERS = "scim_users", } export const DeprecatedViews = { From e679cc3987725b5f3ac99ca98668fb61ef606aa3 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 17:12:00 +0100 Subject: [PATCH 033/135] Add multifetch test --- .../routes/global/tests/scim/users.spec.ts | 34 ++++++++++++++++--- .../worker/src/tests/TestConfiguration.ts | 6 ++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index f2188c2909..4d94a8d1c6 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -71,10 +71,6 @@ describe("/api/global/scim/v2/users", () => { await config.afterAll() }) - beforeEach(() => { - jest.clearAllMocks() - }) - const featureDisabledResponse = { error: { code: "feature_disabled", @@ -117,11 +113,41 @@ describe("/api/global/scim/v2/users", () => { }) }) }) + + describe("multiple users exist", () => { + const userCount = 30 + let users: ScimUserResponse[] + + beforeEach(async () => { + users = [] + + for (let i = 0; i < userCount; i++) { + const body = createScimCreateUserRequest() + users.push(await config.api.scimUsersAPI.post({ body })) + } + }) + + it("fetch full first page", async () => { + const response = await getScimUsers() + + expect(response).toEqual({ + Resources: expect.arrayContaining(users.splice(0, 20)), + itemsPerPage: 20, + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + startIndex: 1, + totalResults: userCount, + }) + }) + }) }) describe("POST /api/global/scim/v2/users", () => { const postScimUser = config.api.scimUsersAPI.post + beforeAll(async () => { + await config.useNewTenant() + }) + it("unauthorised calls are not allowed", async () => { const response = await postScimUser( { body: {} as any }, diff --git a/packages/worker/src/tests/TestConfiguration.ts b/packages/worker/src/tests/TestConfiguration.ts index c1098729b1..5e606143f2 100644 --- a/packages/worker/src/tests/TestConfiguration.ts +++ b/packages/worker/src/tests/TestConfiguration.ts @@ -53,6 +53,12 @@ class TestConfiguration { this.api = new API(this) } + async useNewTenant() { + this.tenantId = structures.tenant.id() + + await this.beforeAll() + } + getRequest() { return this.request } From 86d848458efddcaa639414fa7cf8d4c903eef396 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 14 Mar 2023 17:37:14 +0100 Subject: [PATCH 034/135] Test second page --- .../routes/global/tests/scim/users.spec.ts | 24 +++++++++++++++---- packages/worker/src/tests/api/scim/users.ts | 18 +++++++++----- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 4d94a8d1c6..5c7ddb0393 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -85,7 +85,7 @@ describe("/api/global/scim/v2/users", () => { const getScimUsers = config.api.scimUsersAPI.get it("unauthorised calls are not allowed", async () => { - const response = await getScimUsers({ + const response = await getScimUsers(undefined, { setHeaders: false, expect: 403, }) @@ -95,7 +95,7 @@ describe("/api/global/scim/v2/users", () => { it("cannot be called when feature is disabled", async () => { mocks.licenses.useCloudFree() - const response = await getScimUsers({ expect: 400 }) + const response = await getScimUsers(undefined, { expect: 400 }) expect(response).toEqual(featureDisabledResponse) }) @@ -118,26 +118,40 @@ describe("/api/global/scim/v2/users", () => { const userCount = 30 let users: ScimUserResponse[] - beforeEach(async () => { + beforeAll(async () => { users = [] for (let i = 0; i < userCount; i++) { const body = createScimCreateUserRequest() users.push(await config.api.scimUsersAPI.post({ body })) } + + users = users.sort((a, b) => (a.id > b.id ? 1 : -1)) }) - it("fetch full first page", async () => { + it("fetches full first page", async () => { const response = await getScimUsers() expect(response).toEqual({ - Resources: expect.arrayContaining(users.splice(0, 20)), + Resources: expect.arrayContaining(users.slice(0, 20)), itemsPerPage: 20, schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], startIndex: 1, totalResults: userCount, }) }) + + it("fetches second page", async () => { + const response = await getScimUsers({ startIndex: 20 }) + + expect(response).toEqual({ + Resources: users.slice(20), + itemsPerPage: 20, + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + startIndex: 21, + totalResults: userCount, + }) + }) }) }) diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index ec61504db3..69b1cf0fc4 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -42,12 +42,18 @@ export class ScimUsersAPI extends TestAPI { return request } - get = async (requestSettings?: Partial) => { - const res = await this.#createRequest( - `/api/global/scim/v2/users`, - "get", - requestSettings - ) + get = async ( + params?: { startIndex?: number; pageSize?: number }, + requestSettings?: Partial + ) => { + let url = `/api/global/scim/v2/users?` + if (params?.pageSize) { + url += `count=${params.pageSize}&` + } + if (params?.startIndex) { + url += `startIndex=${params.startIndex}&` + } + const res = await this.#createRequest(url, "get", requestSettings) return res.body as ScimUserListResponse } From 2fda1bb5d1ffe1794add639cf243d2c4d1539030 Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 15 Mar 2023 08:26:56 +0100 Subject: [PATCH 035/135] Clean tests --- .../worker/src/api/routes/global/tests/scim/users.spec.ts | 6 +++--- packages/worker/src/tests/api/scim/users.ts | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 5c7ddb0393..dea9f02886 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -85,7 +85,7 @@ describe("/api/global/scim/v2/users", () => { const getScimUsers = config.api.scimUsersAPI.get it("unauthorised calls are not allowed", async () => { - const response = await getScimUsers(undefined, { + const response = await getScimUsers({ setHeaders: false, expect: 403, }) @@ -95,7 +95,7 @@ describe("/api/global/scim/v2/users", () => { it("cannot be called when feature is disabled", async () => { mocks.licenses.useCloudFree() - const response = await getScimUsers(undefined, { expect: 400 }) + const response = await getScimUsers({ expect: 400 }) expect(response).toEqual(featureDisabledResponse) }) @@ -142,7 +142,7 @@ describe("/api/global/scim/v2/users", () => { }) it("fetches second page", async () => { - const response = await getScimUsers({ startIndex: 20 }) + const response = await getScimUsers({ params: { startIndex: 20 } }) expect(response).toEqual({ Resources: users.slice(20), diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index 69b1cf0fc4..a40fac6308 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -43,10 +43,12 @@ export class ScimUsersAPI extends TestAPI { } get = async ( - params?: { startIndex?: number; pageSize?: number }, - requestSettings?: Partial + requestSettings?: Partial & { + params?: { startIndex?: number; pageSize?: number } + } ) => { let url = `/api/global/scim/v2/users?` + const params = requestSettings?.params if (params?.pageSize) { url += `count=${params.pageSize}&` } From fc0c4815af1817612ceaec94eb1284127404267d Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 15 Mar 2023 12:23:55 +0100 Subject: [PATCH 036/135] Handle SCIM body requests --- packages/worker/src/index.ts | 5 ++++- packages/worker/src/middleware/handleScimBody.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 packages/worker/src/middleware/handleScimBody.ts diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 5084972a4c..4b35f9bafb 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -36,6 +36,7 @@ const { userAgent } = require("koa-useragent") import destroyable from "server-destroy" import { initPro } from "./initPro" +import { handleScimBody } from "./middleware/handleScimBody" // configure events to use the pro audit log write // can't integrate directly into backend-core due to cyclic issues @@ -55,7 +56,9 @@ const app: Application = new Koa() app.keys = ["secret", "key"] // set up top level koa middleware -app.use(koaBody({ multipart: true })) +app.use(handleScimBody) +app.use(koaBody({ multipart: true, jsonStrict: false })) + app.use(koaSession(app)) app.use(middleware.logging) app.use(logger(logging.pinoSettings())) diff --git a/packages/worker/src/middleware/handleScimBody.ts b/packages/worker/src/middleware/handleScimBody.ts new file mode 100644 index 0000000000..06a22625f3 --- /dev/null +++ b/packages/worker/src/middleware/handleScimBody.ts @@ -0,0 +1,12 @@ +import { Ctx } from "@budibase/types" + +export const handleScimBody = (ctx: Ctx, next: any) => { + var type = ctx.req.headers["content-type"] || "" + type = type.split(";")[0] + + if (type === "application/scim+json") { + ctx.req.headers["content-type"] = "application/json" + } + + next() +} From e568c5756f2ffe808d06f72bad1f20b00dc93d2b Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 15 Mar 2023 12:27:30 +0100 Subject: [PATCH 037/135] Fix tests --- packages/worker/src/middleware/handleScimBody.ts | 2 +- packages/worker/src/tests/api/scim/users.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/worker/src/middleware/handleScimBody.ts b/packages/worker/src/middleware/handleScimBody.ts index 06a22625f3..bfcd6dfcf2 100644 --- a/packages/worker/src/middleware/handleScimBody.ts +++ b/packages/worker/src/middleware/handleScimBody.ts @@ -8,5 +8,5 @@ export const handleScimBody = (ctx: Ctx, next: any) => { ctx.req.headers["content-type"] = "application/json" } - next() + return next() } diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index a40fac6308..06acc99593 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -28,6 +28,11 @@ export class ScimUsersAPI extends TestAPI { const { expect, setHeaders } = { ...defaultConfig, ...requestSettings } let request = this.request[method](url).expect(expect) + request = request.set( + "content-type", + "application/scim+json; charset=utf-8" + ) + if (method !== "delete") { request = request.expect("Content-Type", /json/) } From 495c8f4b0e21c084996dbbecea5222c25615861f Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 15 Mar 2023 13:34:17 +0100 Subject: [PATCH 038/135] Undo unwanted changes --- packages/worker/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 4b35f9bafb..1a646c623d 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -57,7 +57,7 @@ app.keys = ["secret", "key"] // set up top level koa middleware app.use(handleScimBody) -app.use(koaBody({ multipart: true, jsonStrict: false })) +app.use(koaBody({ multipart: true })) app.use(koaSession(app)) app.use(middleware.logging) From 900e6c812994e7691c7fb298127abc89bf0664e3 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Wed, 15 Mar 2023 15:59:09 +0000 Subject: [PATCH 039/135] Adding search index for user. --- packages/backend-core/src/db/lucene.ts | 1 + .../src/db/searchIndexes/index.ts | 0 .../src/db/searchIndexes/searchIndexes.ts | 52 +++++++++++++++++++ packages/types/src/sdk/db.ts | 1 + 4 files changed, 54 insertions(+) create mode 100644 packages/backend-core/src/db/searchIndexes/index.ts create mode 100644 packages/backend-core/src/db/searchIndexes/searchIndexes.ts diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index 71ce4ba9ac..3e6bdfef9a 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -1,6 +1,7 @@ import fetch from "node-fetch" import { getCouchInfo } from "./couch" import { SearchFilters, Row } from "@budibase/types" +import { createUserIndex } from "./searchIndexes/searchIndexes" const QUERY_START_REGEX = /\d[0-9]*:/g diff --git a/packages/backend-core/src/db/searchIndexes/index.ts b/packages/backend-core/src/db/searchIndexes/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/backend-core/src/db/searchIndexes/searchIndexes.ts b/packages/backend-core/src/db/searchIndexes/searchIndexes.ts new file mode 100644 index 0000000000..2a7db95945 --- /dev/null +++ b/packages/backend-core/src/db/searchIndexes/searchIndexes.ts @@ -0,0 +1,52 @@ +import { User, SearchIndex } from "@budibase/types" +import { getGlobalDB } from "../../context" + +export async function createUserIndex() { + const db = getGlobalDB() + let designDoc + try { + designDoc = await db.get("_design/database") + } catch (err: any) { + if (err.status === 404) { + designDoc = { _id: "_design/database" } + } + } + // this is a very specific function given that it is only for audit logs + const fn = function (user: User) { + if (user._id && !user._id.startsWith("us_")) { + return + } + const ignoredFields = ["_id", "_rev", "password"] + function idx(input: Record, prev?: string) { + for (let key of Object.keys(input)) { + if (ignoredFields.includes(key)) { + continue + } + let idxKey = prev != null ? `${prev}.${key}` : key + if (typeof input[key] === "string") { + // eslint-disable-next-line no-undef + // @ts-ignore + index(idxKey, input[key].toLowerCase(), { facet: true }) + } else if (typeof input[key] !== "object") { + // eslint-disable-next-line no-undef + // @ts-ignore + index(idxKey, input[key], { facet: true }) + } else { + idx(input[key], idxKey) + } + } + } + idx(user) + } + + designDoc.indexes = { + [SearchIndex.USER]: { + index: fn.toString(), + analyzer: { + default: "keyword", + name: "perfield", + }, + }, + } + await db.put(designDoc) +} diff --git a/packages/types/src/sdk/db.ts b/packages/types/src/sdk/db.ts index 6e213b5831..00170a1105 100644 --- a/packages/types/src/sdk/db.ts +++ b/packages/types/src/sdk/db.ts @@ -5,6 +5,7 @@ import { Writable } from "stream" export enum SearchIndex { ROWS = "rows", AUDIT = "audit", + USER = "user", } export type PouchOptions = { From c763c6fae524d7ea830e146b8ab7d17f5b1c1178 Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 15 Mar 2023 17:39:06 +0100 Subject: [PATCH 040/135] Expose index --- packages/backend-core/src/db/index.ts | 1 + packages/backend-core/src/db/searchIndexes/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/backend-core/src/db/index.ts b/packages/backend-core/src/db/index.ts index a569b17b36..ea93b91d14 100644 --- a/packages/backend-core/src/db/index.ts +++ b/packages/backend-core/src/db/index.ts @@ -8,3 +8,4 @@ export { default as Replication } from "./Replication" export * from "../constants/db" export { getGlobalDBName, baseGlobalDBName } from "../context" export * from "./lucene" +export * as searchIndexes from "./searchIndexes" diff --git a/packages/backend-core/src/db/searchIndexes/index.ts b/packages/backend-core/src/db/searchIndexes/index.ts index e69de29bb2..d3054e90c0 100644 --- a/packages/backend-core/src/db/searchIndexes/index.ts +++ b/packages/backend-core/src/db/searchIndexes/index.ts @@ -0,0 +1 @@ +export * from "./searchIndexes" From 1c828db69404c14e014cd301c05cb04b38b0958e Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 15 Mar 2023 17:39:20 +0100 Subject: [PATCH 041/135] Return total rows --- packages/backend-core/src/db/lucene.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index 3e6bdfef9a..a9f54cb79a 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -7,7 +7,8 @@ const QUERY_START_REGEX = /\d[0-9]*:/g interface SearchResponse { rows: T[] | any[] - bookmark: string + bookmark?: string + totalRows: number } interface PaginatedSearchResponse extends SearchResponse { @@ -503,8 +504,10 @@ async function runQuery( } const json = await response.json() - let output: any = { + let output: SearchResponse = { rows: [], + + totalRows: 0, } if (json.rows != null && json.rows.length > 0) { output.rows = json.rows.map((row: any) => row.doc) @@ -512,6 +515,9 @@ async function runQuery( if (json.bookmark) { output.bookmark = json.bookmark } + if (json.total_rows) { + output.totalRows = json.total_rows + } return output } From 6c6d06055068dcb55563c36ea6064bbc3db2b0bc Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 15 Mar 2023 17:43:20 +0100 Subject: [PATCH 042/135] Remove view --- packages/backend-core/src/constants/db.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend-core/src/constants/db.ts b/packages/backend-core/src/constants/db.ts index 4fe7b1233a..d41098c405 100644 --- a/packages/backend-core/src/constants/db.ts +++ b/packages/backend-core/src/constants/db.ts @@ -22,7 +22,6 @@ export enum ViewName { PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", USER_BY_GROUP = "by_group_user", APP_BACKUP_BY_TRIGGER = "by_trigger", - SCIM_USERS = "scim_users", } export const DeprecatedViews = { From 06245fee984c8f2964978c04be00e14b96f65f72 Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 15 Mar 2023 20:02:48 +0100 Subject: [PATCH 043/135] Make includeDocs private --- packages/backend-core/src/db/lucene.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index a9f54cb79a..dd9de20400 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -52,7 +52,7 @@ export class QueryBuilder { bookmark?: string sortOrder: string sortType: string - includeDocs: boolean + #includeDocs: boolean version?: string indexBuilder?: () => Promise noEscaping = false @@ -78,7 +78,7 @@ export class QueryBuilder { this.limit = 50 this.sortOrder = "ascending" this.sortType = "string" - this.includeDocs = true + this.#includeDocs = true } disableEscaping() { @@ -139,7 +139,12 @@ export class QueryBuilder { } excludeDocs() { - this.includeDocs = false + this.#includeDocs = false + return this + } + + includeDocs() { + this.#includeDocs = true return this } @@ -449,7 +454,7 @@ export class QueryBuilder { let body: any = { q: this.buildSearchQuery(), limit: Math.min(this.limit, 200), - include_docs: this.includeDocs, + include_docs: this.#includeDocs, } if (this.bookmark) { body.bookmark = this.bookmark From a91e4b4da17cd488a96abdb8f9c286b1951b2518 Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 15 Mar 2023 20:05:58 +0100 Subject: [PATCH 044/135] Make QueryBuilder vars private --- packages/backend-core/src/db/lucene.ts | 158 +++++++++++++------------ 1 file changed, 80 insertions(+), 78 deletions(-) diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index dd9de20400..9614bd6df2 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -44,23 +44,23 @@ export function removeKeyNumbering(key: any): string { * Optionally takes a base lucene query object. */ export class QueryBuilder { - dbName: string - index: string - query: SearchFilters - limit: number - sort?: string - bookmark?: string - sortOrder: string - sortType: string + #dbName: string + #index: string + #query: SearchFilters + #limit: number + #sort?: string + #bookmark?: string + #sortOrder: string + #sortType: string #includeDocs: boolean - version?: string - indexBuilder?: () => Promise - noEscaping = false + #version?: string + #indexBuilder?: () => Promise + #noEscaping = false constructor(dbName: string, index: string, base?: SearchFilters) { - this.dbName = dbName - this.index = index - this.query = { + this.#dbName = dbName + this.#index = index + this.#query = { allOr: false, string: {}, fuzzy: {}, @@ -75,65 +75,65 @@ export class QueryBuilder { containsAny: {}, ...base, } - this.limit = 50 - this.sortOrder = "ascending" - this.sortType = "string" + this.#limit = 50 + this.#sortOrder = "ascending" + this.#sortType = "string" this.#includeDocs = true } disableEscaping() { - this.noEscaping = true + this.#noEscaping = true return this } setIndexBuilder(builderFn: () => Promise) { - this.indexBuilder = builderFn + this.#indexBuilder = builderFn return this } setVersion(version?: string) { if (version != null) { - this.version = version + this.#version = version } return this } setTable(tableId: string) { - this.query.equal!.tableId = tableId + this.#query.equal!.tableId = tableId return this } setLimit(limit?: number) { if (limit != null) { - this.limit = limit + this.#limit = limit } return this } setSort(sort?: string) { if (sort != null) { - this.sort = sort + this.#sort = sort } return this } setSortOrder(sortOrder?: string) { if (sortOrder != null) { - this.sortOrder = sortOrder + this.#sortOrder = sortOrder } return this } setSortType(sortType?: string) { if (sortType != null) { - this.sortType = sortType + this.#sortType = sortType } return this } setBookmark(bookmark?: string) { if (bookmark != null) { - this.bookmark = bookmark + this.#bookmark = bookmark } return this } @@ -149,17 +149,17 @@ export class QueryBuilder { } addString(key: string, partial: string) { - this.query.string![key] = partial + this.#query.string![key] = partial return this } addFuzzy(key: string, fuzzy: string) { - this.query.fuzzy![key] = fuzzy + this.#query.fuzzy![key] = fuzzy return this } addRange(key: string, low: string | number, high: string | number) { - this.query.range![key] = { + this.#query.range![key] = { low, high, } @@ -167,51 +167,51 @@ export class QueryBuilder { } addEqual(key: string, value: any) { - this.query.equal![key] = value + this.#query.equal![key] = value return this } addNotEqual(key: string, value: any) { - this.query.notEqual![key] = value + this.#query.notEqual![key] = value return this } addEmpty(key: string, value: any) { - this.query.empty![key] = value + this.#query.empty![key] = value return this } addNotEmpty(key: string, value: any) { - this.query.notEmpty![key] = value + this.#query.notEmpty![key] = value return this } addOneOf(key: string, value: any) { - this.query.oneOf![key] = value + this.#query.oneOf![key] = value return this } addContains(key: string, value: any) { - this.query.contains![key] = value + this.#query.contains![key] = value return this } addNotContains(key: string, value: any) { - this.query.notContains![key] = value + this.#query.notContains![key] = value return this } addContainsAny(key: string, value: any) { - this.query.containsAny![key] = value + this.#query.containsAny![key] = value return this } setAllOr() { - this.query.allOr = true + this.#query.allOr = true } handleSpaces(input: string) { - if (this.noEscaping) { + if (this.#noEscaping) { return input } else { return input.replace(/ /g, "_") @@ -226,7 +226,7 @@ export class QueryBuilder { * @returns {string|*} */ preprocess(value: any, { escape, lowercase, wrap, type }: any = {}) { - const hasVersion = !!this.version + const hasVersion = !!this.#version // Determine if type needs wrapped const originalType = typeof value // Convert to lowercase @@ -234,7 +234,7 @@ export class QueryBuilder { value = value.toLowerCase ? value.toLowerCase() : value } // Escape characters - if (!this.noEscaping && escape && originalType === "string") { + if (!this.#noEscaping && escape && originalType === "string") { value = `${value}`.replace(/[ #+\-&|!(){}\]^"~*?:\\]/g, "\\$&") } @@ -249,7 +249,7 @@ export class QueryBuilder { isMultiCondition() { let count = 0 - for (let filters of Object.values(this.query)) { + for (let filters of Object.values(this.#query)) { // not contains is one massive filter in allOr mode if (typeof filters === "object") { count += Object.keys(filters).length @@ -279,13 +279,13 @@ export class QueryBuilder { buildSearchQuery() { const builder = this - let allOr = this.query && this.query.allOr + let allOr = this.#query && this.#query.allOr let query = allOr ? "" : "*:*" const allPreProcessingOpts = { escape: true, lowercase: true, wrap: true } let tableId - if (this.query.equal!.tableId) { - tableId = this.query.equal!.tableId - delete this.query.equal!.tableId + if (this.#query.equal!.tableId) { + tableId = this.#query.equal!.tableId + delete this.#query.equal!.tableId } const equal = (key: string, value: any) => { @@ -370,8 +370,8 @@ export class QueryBuilder { } // Construct the actual lucene search query string from JSON structure - if (this.query.string) { - build(this.query.string, (key: string, value: any) => { + if (this.#query.string) { + build(this.#query.string, (key: string, value: any) => { if (!value) { return null } @@ -383,8 +383,8 @@ export class QueryBuilder { return `${key}:${value}*` }) } - if (this.query.range) { - build(this.query.range, (key: string, value: any) => { + if (this.#query.range) { + build(this.#query.range, (key: string, value: any) => { if (!value) { return null } @@ -399,8 +399,8 @@ export class QueryBuilder { return `${key}:[${low} TO ${high}]` }) } - if (this.query.fuzzy) { - build(this.query.fuzzy, (key: string, value: any) => { + if (this.#query.fuzzy) { + build(this.#query.fuzzy, (key: string, value: any) => { if (!value) { return null } @@ -412,34 +412,34 @@ export class QueryBuilder { return `${key}:${value}~` }) } - if (this.query.equal) { - build(this.query.equal, equal) + if (this.#query.equal) { + build(this.#query.equal, equal) } - if (this.query.notEqual) { - build(this.query.notEqual, (key: string, value: any) => { + if (this.#query.notEqual) { + build(this.#query.notEqual, (key: string, value: any) => { if (!value) { return null } return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}` }) } - if (this.query.empty) { - build(this.query.empty, (key: string) => `!${key}:["" TO *]`) + if (this.#query.empty) { + build(this.#query.empty, (key: string) => `!${key}:["" TO *]`) } - if (this.query.notEmpty) { - build(this.query.notEmpty, (key: string) => `${key}:["" TO *]`) + if (this.#query.notEmpty) { + build(this.#query.notEmpty, (key: string) => `${key}:["" TO *]`) } - if (this.query.oneOf) { - build(this.query.oneOf, oneOf) + if (this.#query.oneOf) { + build(this.#query.oneOf, oneOf) } - if (this.query.contains) { - build(this.query.contains, contains) + if (this.#query.contains) { + build(this.#query.contains, contains) } - if (this.query.notContains) { - build(this.compressFilters(this.query.notContains), notContains) + if (this.#query.notContains) { + build(this.compressFilters(this.#query.notContains), notContains) } - if (this.query.containsAny) { - build(this.query.containsAny, containsAny) + if (this.#query.containsAny) { + build(this.#query.containsAny, containsAny) } // make sure table ID is always added as an AND if (tableId) { @@ -453,29 +453,31 @@ export class QueryBuilder { buildSearchBody() { let body: any = { q: this.buildSearchQuery(), - limit: Math.min(this.limit, 200), + limit: Math.min(this.#limit, 200), include_docs: this.#includeDocs, } - if (this.bookmark) { - body.bookmark = this.bookmark + if (this.#bookmark) { + body.bookmark = this.#bookmark } - if (this.sort) { - const order = this.sortOrder === "descending" ? "-" : "" - const type = `<${this.sortType}>` - body.sort = `${order}${this.handleSpaces(this.sort)}${type}` + if (this.#sort) { + const order = this.#sortOrder === "descending" ? "-" : "" + const type = `<${this.#sortType}>` + body.sort = `${order}${this.handleSpaces(this.#sort)}${type}` } return body } async run() { const { url, cookie } = getCouchInfo() - const fullPath = `${url}/${this.dbName}/_design/database/_search/${this.index}` + const fullPath = `${url}/${this.#dbName}/_design/database/_search/${ + this.#index + }` const body = this.buildSearchBody() try { return await runQuery(fullPath, body, cookie) } catch (err: any) { - if (err.status === 404 && this.indexBuilder) { - await this.indexBuilder() + if (err.status === 404 && this.#indexBuilder) { + await this.#indexBuilder() return await runQuery(fullPath, body, cookie) } else { throw err From ad2a23d1139bd103038fe2f01b33614527924e1a Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 15 Mar 2023 20:48:34 +0100 Subject: [PATCH 045/135] Test filtering by user name --- packages/types/src/documents/global/user.ts | 2 +- packages/worker/package.json | 2 ++ .../routes/global/tests/scim/users.spec.ts | 22 +++++++++++++++++++ packages/worker/src/tests/api/scim/users.ts | 9 +++++++- packages/worker/yarn.lock | 5 +++++ 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index 39f0de9507..64c1d42f39 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -54,7 +54,7 @@ export interface User extends Document { userGroups?: string[] onboardedAt?: string scimInfo?: { - username: string + userName: string externalId: string isSync: boolean firstSync: number diff --git a/packages/worker/package.json b/packages/worker/package.json index 0015f39ac1..73cfc0cc4a 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -79,6 +79,7 @@ "@types/jsonwebtoken": "8.5.1", "@types/koa": "2.13.4", "@types/koa__router": "8.0.8", + "@types/lodash": "^4.14.191", "@types/node": "14.18.20", "@types/node-fetch": "2.6.1", "@types/pouchdb": "6.4.0", @@ -89,6 +90,7 @@ "copyfiles": "2.4.1", "eslint": "6.8.0", "jest": "28.1.1", + "lodash": "4.17.21", "nodemon": "2.0.15", "pouchdb-adapter-memory": "7.2.2", "prettier": "2.3.1", diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index dea9f02886..32d95203b6 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -1,4 +1,5 @@ import tk from "timekeeper" +import _ from "lodash" import { mocks, structures } from "@budibase/backend-core/tests" import { ScimCreateUserRequest, @@ -7,6 +8,8 @@ import { } from "@budibase/types" import { TestConfiguration } from "../../../../../tests" +mocks.licenses.useScimIntegration() + function createScimCreateUserRequest(userData?: { externalId?: string email?: string @@ -152,6 +155,25 @@ describe("/api/global/scim/v2/users", () => { totalResults: userCount, }) }) + + it("can filter by user name", async () => { + // '/api/global/scim/v2/Users?filter=userName+eq+%2212e18327-eee2-4a12-961e-bceff00f6b92%22' + const userToFetch = _.sample(users) + + const response = await getScimUsers({ + params: { + filter: `userName+eq+%22${userToFetch?.userName}%22`, + }, + }) + + expect(response).toEqual({ + Resources: [userToFetch], + itemsPerPage: 20, + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + startIndex: 1, + totalResults: 1, + }) + }) }) }) diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index 06acc99593..85e38e9978 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -49,7 +49,11 @@ export class ScimUsersAPI extends TestAPI { get = async ( requestSettings?: Partial & { - params?: { startIndex?: number; pageSize?: number } + params?: { + startIndex?: number + pageSize?: number + filter?: string + } } ) => { let url = `/api/global/scim/v2/users?` @@ -60,6 +64,9 @@ export class ScimUsersAPI extends TestAPI { if (params?.startIndex) { url += `startIndex=${params.startIndex}&` } + if (params?.filter) { + url += `filter=${params.filter}&` + } const res = await this.#createRequest(url, "get", requestSettings) return res.body as ScimUserListResponse } diff --git a/packages/worker/yarn.lock b/packages/worker/yarn.lock index e656126105..0f1f92e5d6 100644 --- a/packages/worker/yarn.lock +++ b/packages/worker/yarn.lock @@ -1606,6 +1606,11 @@ dependencies: "@types/koa" "*" +"@types/lodash@^4.14.191": + version "4.14.191" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" + integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== + "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" From 05b7467076aab6145bcd523f33f404035fbe0783 Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 15 Mar 2023 21:38:48 +0100 Subject: [PATCH 046/135] Test filter by external id --- .../routes/global/tests/scim/users.spec.ts | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 32d95203b6..a6d9bb70de 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -157,12 +157,29 @@ describe("/api/global/scim/v2/users", () => { }) it("can filter by user name", async () => { - // '/api/global/scim/v2/Users?filter=userName+eq+%2212e18327-eee2-4a12-961e-bceff00f6b92%22' const userToFetch = _.sample(users) const response = await getScimUsers({ params: { - filter: `userName+eq+%22${userToFetch?.userName}%22`, + filter: encodeURI(`userName eq "${userToFetch?.userName}"`), + }, + }) + + expect(response).toEqual({ + Resources: [userToFetch], + itemsPerPage: 20, + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + startIndex: 1, + totalResults: 1, + }) + }) + + it("can filter by external id", async () => { + const userToFetch = _.sample(users) + + const response = await getScimUsers({ + params: { + filter: encodeURI(`externalId eq "${userToFetch?.externalId}"`), }, }) From 3b07f0e1a2ea26bc6f3fc207e192cd657f20d7af Mon Sep 17 00:00:00 2001 From: adrinr Date: Wed, 15 Mar 2023 21:53:20 +0100 Subject: [PATCH 047/135] filter by email --- packages/types/src/documents/global/user.ts | 1 - .../routes/global/tests/scim/users.spec.ts | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/types/src/documents/global/user.ts b/packages/types/src/documents/global/user.ts index 64c1d42f39..56edab7012 100644 --- a/packages/types/src/documents/global/user.ts +++ b/packages/types/src/documents/global/user.ts @@ -57,7 +57,6 @@ export interface User extends Document { userName: string externalId: string isSync: boolean - firstSync: number } } diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index a6d9bb70de..eff82ab766 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -191,6 +191,26 @@ describe("/api/global/scim/v2/users", () => { totalResults: 1, }) }) + + it("can filter by email", async () => { + const userToFetch = _.sample(users) + + const response = await getScimUsers({ + params: { + filter: encodeURI( + `emails[type eq "work"].value eq "${userToFetch?.emails[0].value}"` + ), + }, + }) + + expect(response).toEqual({ + Resources: [userToFetch], + itemsPerPage: 20, + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + startIndex: 1, + totalResults: 1, + }) + }) }) }) From bf32801917c9a0f19809518e93973163febcc61a Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 09:59:10 +0100 Subject: [PATCH 048/135] Handle skip on execution --- packages/backend-core/src/db/lucene.ts | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index 9614bd6df2..fee89fcd7e 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -56,6 +56,7 @@ export class QueryBuilder { #version?: string #indexBuilder?: () => Promise #noEscaping = false + #skip?: number constructor(dbName: string, index: string, base?: SearchFilters) { this.#dbName = dbName @@ -138,6 +139,11 @@ export class QueryBuilder { return this } + setSkip(skip: number | undefined) { + this.#skip = skip + return this + } + excludeDocs() { this.#includeDocs = false return this @@ -468,6 +474,34 @@ export class QueryBuilder { } async run() { + if (this.#skip) { + await this.#skipPages(this.#skip) + } + return await this.#execute() + } + + async #skipPages(skip: number) { + // Lucene does not support pagination. + // Handle pagination by finding the right bookmark + const prevIncludeDocs = this.#includeDocs + const prevLimit = this.#limit + + this.excludeDocs() + const maxPageSize = 1000 + let skipRemaining = skip + do { + const toSkip = Math.min(maxPageSize, skipRemaining) + this.setLimit(toSkip) + const { bookmark } = await this.#execute() + this.setBookmark(bookmark) + skipRemaining -= toSkip + } while (skipRemaining > 0) + + this.#includeDocs = prevIncludeDocs + this.#limit = prevLimit + } + + async #execute() { const { url, cookie } = getCouchInfo() const fullPath = `${url}/${this.#dbName}/_design/database/_search/${ this.#index From 41537cd00cb91f9917bbf5a109ebfc690342406d Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 10:00:40 +0100 Subject: [PATCH 049/135] Add comments --- packages/backend-core/src/db/lucene.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index fee89fcd7e..f0058200b1 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -480,6 +480,11 @@ export class QueryBuilder { return await this.#execute() } + /** + * Lucene queries do not support pagination and use bookmarks instead. + * For the given builder, walk through pages using bookmarks until the desired + * page has been met. + */ async #skipPages(skip: number) { // Lucene does not support pagination. // Handle pagination by finding the right bookmark From 5ffa51d1f4c914f9e12e4b6773e92ce82f0073db Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 10:08:20 +0100 Subject: [PATCH 050/135] Rename --- packages/backend-core/src/db/lucene.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index f0058200b1..d8ea223402 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -475,7 +475,7 @@ export class QueryBuilder { async run() { if (this.#skip) { - await this.#skipPages(this.#skip) + await this.#skipItems(this.#skip) } return await this.#execute() } @@ -485,7 +485,7 @@ export class QueryBuilder { * For the given builder, walk through pages using bookmarks until the desired * page has been met. */ - async #skipPages(skip: number) { + async #skipItems(skip: number) { // Lucene does not support pagination. // Handle pagination by finding the right bookmark const prevIncludeDocs = this.#includeDocs From f181cb02d03fa91ce9427c129639799862f60a9b Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 10:09:03 +0100 Subject: [PATCH 051/135] Remove comment --- packages/backend-core/src/db/searchIndexes/searchIndexes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-core/src/db/searchIndexes/searchIndexes.ts b/packages/backend-core/src/db/searchIndexes/searchIndexes.ts index 2a7db95945..a7cfd92e21 100644 --- a/packages/backend-core/src/db/searchIndexes/searchIndexes.ts +++ b/packages/backend-core/src/db/searchIndexes/searchIndexes.ts @@ -11,7 +11,7 @@ export async function createUserIndex() { designDoc = { _id: "_design/database" } } } - // this is a very specific function given that it is only for audit logs + const fn = function (user: User) { if (user._id && !user._id.startsWith("us_")) { return From 47cc2915515103500e6a13c33e25242156db88fd Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 10:34:46 +0100 Subject: [PATCH 052/135] Exclude session fields --- .../src/db/searchIndexes/searchIndexes.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/backend-core/src/db/searchIndexes/searchIndexes.ts b/packages/backend-core/src/db/searchIndexes/searchIndexes.ts index a7cfd92e21..f03259b47f 100644 --- a/packages/backend-core/src/db/searchIndexes/searchIndexes.ts +++ b/packages/backend-core/src/db/searchIndexes/searchIndexes.ts @@ -16,7 +16,17 @@ export async function createUserIndex() { if (user._id && !user._id.startsWith("us_")) { return } - const ignoredFields = ["_id", "_rev", "password"] + const ignoredFields = [ + "_id", + "_rev", + "password", + "account", + "license", + "budibaseAccess", + "accountPortalAccess", + "csrfToken", + ] + function idx(input: Record, prev?: string) { for (let key of Object.keys(input)) { if (ignoredFields.includes(key)) { From 30c66748af62f4ef3d3cb79f7946658163b06cdc Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 13:09:11 +0100 Subject: [PATCH 053/135] Add skip tests --- .../backend-core/src/db/tests/lucene.spec.ts | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/packages/backend-core/src/db/tests/lucene.spec.ts b/packages/backend-core/src/db/tests/lucene.spec.ts index 52017cc94c..3ac9b5febe 100644 --- a/packages/backend-core/src/db/tests/lucene.spec.ts +++ b/packages/backend-core/src/db/tests/lucene.spec.ts @@ -136,6 +136,67 @@ describe("lucene", () => { const resp = await builder.run() expect(resp.rows.length).toBe(2) }) + + describe("skip", () => { + const skipDbName = `db-${newid()}` + let docs: { + _id: string + property: string + array: string[] + }[] + + beforeAll(async () => { + const db = getDB(skipDbName) + + docs = Array(1500) + .fill(0) + .map((_, i) => ({ + _id: i.toString().padStart(4, "0"), + property: `value${i}`, + array: [], + })) + await db.bulkDocs(docs) + + await db.put({ + _id: "_design/database", + indexes: { + [INDEX_NAME]: { + index: index, + analyzer: "standard", + }, + }, + }) + }) + + it("should be able to apply skip", async () => { + const builder = new QueryBuilder(skipDbName, INDEX_NAME) + const firstResponse = await builder.run() + builder.setSkip(40) + const secondResponse = await builder.run() + + // Return the default limit + expect(firstResponse.rows.length).toBe(50) + expect(secondResponse.rows.length).toBe(50) + + // Should have the expected overlap + expect(firstResponse.rows.slice(40)).toEqual( + secondResponse.rows.slice(0, 10) + ) + }) + + it("should handle limits", async () => { + const builder = new QueryBuilder(skipDbName, INDEX_NAME) + builder.setLimit(10) + builder.setSkip(50) + builder.setSort("_id") + + const resp = await builder.run() + expect(resp.rows.length).toBe(10) + expect(resp.rows).toEqual( + docs.slice(50, 60).map(expect.objectContaining) + ) + }) + }) }) describe("paginated search", () => { From 9a2eaaad42223ecd824f5e0df7e33b35121c5e62 Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 13:37:17 +0100 Subject: [PATCH 054/135] Test deep pagination --- packages/backend-core/src/db/lucene.ts | 19 ++++++++++--------- .../backend-core/src/db/tests/lucene.spec.ts | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index d8ea223402..b9463b12fb 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -58,6 +58,8 @@ export class QueryBuilder { #noEscaping = false #skip?: number + static readonly maxLimit = 200 + constructor(dbName: string, index: string, base?: SearchFilters) { this.#dbName = dbName this.#index = index @@ -459,7 +461,7 @@ export class QueryBuilder { buildSearchBody() { let body: any = { q: this.buildSearchQuery(), - limit: Math.min(this.#limit, 200), + limit: Math.min(this.#limit, QueryBuilder.maxLimit), include_docs: this.#includeDocs, } if (this.#bookmark) { @@ -492,14 +494,13 @@ export class QueryBuilder { const prevLimit = this.#limit this.excludeDocs() - const maxPageSize = 1000 let skipRemaining = skip do { - const toSkip = Math.min(maxPageSize, skipRemaining) + const toSkip = Math.min(QueryBuilder.maxLimit, skipRemaining) this.setLimit(toSkip) - const { bookmark } = await this.#execute() + const { bookmark, rows } = await this.#execute() this.setBookmark(bookmark) - skipRemaining -= toSkip + skipRemaining -= rows.length } while (skipRemaining > 0) this.#includeDocs = prevIncludeDocs @@ -596,8 +597,8 @@ async function recursiveSearch( if (rows.length >= params.limit) { return rows } - let pageSize = 200 - if (rows.length > params.limit - 200) { + let pageSize = QueryBuilder.maxLimit + if (rows.length > params.limit - QueryBuilder.maxLimit) { pageSize = params.limit - rows.length } const page = await new QueryBuilder(dbName, index, query) @@ -612,7 +613,7 @@ async function recursiveSearch( if (!page.rows.length) { return rows } - if (page.rows.length < 200) { + if (page.rows.length < QueryBuilder.maxLimit) { return [...rows, ...page.rows] } const newParams = { @@ -650,7 +651,7 @@ export async function paginatedSearch( if (limit == null || isNaN(limit) || limit < 0) { limit = 50 } - limit = Math.min(limit, 200) + limit = Math.min(limit, QueryBuilder.maxLimit) const search = new QueryBuilder(dbName, index, query) if (params.version) { search.setVersion(params.version) diff --git a/packages/backend-core/src/db/tests/lucene.spec.ts b/packages/backend-core/src/db/tests/lucene.spec.ts index 3ac9b5febe..2e2fa9f2e7 100644 --- a/packages/backend-core/src/db/tests/lucene.spec.ts +++ b/packages/backend-core/src/db/tests/lucene.spec.ts @@ -148,7 +148,7 @@ describe("lucene", () => { beforeAll(async () => { const db = getDB(skipDbName) - docs = Array(1500) + docs = Array(QueryBuilder.maxLimit * 2.5) .fill(0) .map((_, i) => ({ _id: i.toString().padStart(4, "0"), @@ -196,6 +196,20 @@ describe("lucene", () => { docs.slice(50, 60).map(expect.objectContaining) ) }) + + it("should be able to skip searching through multiple responses", async () => { + const builder = new QueryBuilder(skipDbName, INDEX_NAME) + // Skipping 2 max limits plus a little bit more + const skip = QueryBuilder.maxLimit * 2 + 37 + builder.setSkip(skip) + builder.setSort("_id") + const resp = await builder.run() + + expect(resp.rows.length).toBe(50) + expect(resp.rows).toEqual( + docs.slice(skip, skip + resp.rows.length).map(expect.objectContaining) + ) + }) }) }) From 348b06948b85587ff5e51eaea1bcc0871c3042b8 Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 13:44:03 +0100 Subject: [PATCH 055/135] Test limits --- packages/backend-core/src/db/lucene.ts | 4 +++- packages/backend-core/src/db/tests/lucene.spec.ts | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index b9463b12fb..50ec35f955 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -495,13 +495,15 @@ export class QueryBuilder { this.excludeDocs() let skipRemaining = skip + let iterationFetched = 0 do { const toSkip = Math.min(QueryBuilder.maxLimit, skipRemaining) this.setLimit(toSkip) const { bookmark, rows } = await this.#execute() this.setBookmark(bookmark) + iterationFetched = rows.length skipRemaining -= rows.length - } while (skipRemaining > 0) + } while (skipRemaining > 0 && iterationFetched > 0) this.#includeDocs = prevIncludeDocs this.#limit = prevLimit diff --git a/packages/backend-core/src/db/tests/lucene.spec.ts b/packages/backend-core/src/db/tests/lucene.spec.ts index 2e2fa9f2e7..5a773a4817 100644 --- a/packages/backend-core/src/db/tests/lucene.spec.ts +++ b/packages/backend-core/src/db/tests/lucene.spec.ts @@ -210,6 +210,17 @@ describe("lucene", () => { docs.slice(skip, skip + resp.rows.length).map(expect.objectContaining) ) }) + + it("should not return results if skipping all docs", async () => { + const builder = new QueryBuilder(skipDbName, INDEX_NAME) + // Skipping 2 max limits plus a little bit more + const skip = docs.length + 1 + builder.setSkip(skip) + + const resp = await builder.run() + + expect(resp.rows.length).toBe(0) + }) }) }) From 43c25436c8e7961c36c18e25f3af6741edb5f1fd Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 13:51:22 +0100 Subject: [PATCH 056/135] Test skip with filters --- .../backend-core/src/db/tests/lucene.spec.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/backend-core/src/db/tests/lucene.spec.ts b/packages/backend-core/src/db/tests/lucene.spec.ts index 5a773a4817..26ce316a9d 100644 --- a/packages/backend-core/src/db/tests/lucene.spec.ts +++ b/packages/backend-core/src/db/tests/lucene.spec.ts @@ -151,8 +151,8 @@ describe("lucene", () => { docs = Array(QueryBuilder.maxLimit * 2.5) .fill(0) .map((_, i) => ({ - _id: i.toString().padStart(4, "0"), - property: `value${i}`, + _id: i.toString().padStart(3, "0"), + property: `value_${i.toString().padStart(3, "0")}`, array: [], })) await db.bulkDocs(docs) @@ -221,6 +221,20 @@ describe("lucene", () => { expect(resp.rows.length).toBe(0) }) + + it("skip should respect with filters", async () => { + const builder = new QueryBuilder(skipDbName, INDEX_NAME) + builder.setLimit(10) + builder.setSkip(50) + builder.addString("property", "value_1") + builder.setSort("property") + + const resp = await builder.run() + expect(resp.rows.length).toBe(10) + expect(resp.rows).toEqual( + docs.slice(150, 160).map(expect.objectContaining) + ) + }) }) }) From d452f5cf0d3d050658a4b50c01daa5aaa68a58e5 Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 15:04:10 +0100 Subject: [PATCH 057/135] Handle string boolean requests --- .../types/src/api/web/global/scim/users.ts | 6 ++++-- .../api/routes/global/tests/scim/users.spec.ts | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/types/src/api/web/global/scim/users.ts b/packages/types/src/api/web/global/scim/users.ts index 8748b4e8e6..72b7078bb4 100644 --- a/packages/types/src/api/web/global/scim/users.ts +++ b/packages/types/src/api/web/global/scim/users.ts @@ -1,5 +1,7 @@ import { ScimResource, ScimMeta, ScimPatchOperation } from "scim-patch" +type BooleanString = boolean | "True" | "False" + export interface ScimUserResponse extends ScimResource { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"] id: string @@ -13,7 +15,7 @@ export interface ScimUserResponse extends ScimResource { familyName: string givenName: string } - active: boolean + active: BooleanString emails: [ { value: string @@ -30,7 +32,7 @@ export interface ScimCreateUserRequest { ] externalId: string userName: string - active: boolean + active: BooleanString emails: [ { primary: true diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index eff82ab766..d0c15ca532 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -395,6 +395,24 @@ describe("/api/global/scim/v2/users", () => { const persistedUser = await config.api.scimUsersAPI.find(user.id) expect(persistedUser).toEqual(expectedScimUser) }) + + it("can deactive an active user", async () => { + const body: ScimUpdateRequest = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [{ op: "Replace", path: "active", value: "False" }], + } + + const response = await patchScimUser({ id: user.id, body }) + + const expectedScimUser: ScimUserResponse = { + ...user, + active: false, + } + expect(response).toEqual(expectedScimUser) + + const persistedUser = await config.api.scimUsersAPI.find(user.id) + expect(persistedUser).toEqual(expectedScimUser) + }) }) describe("DELETE /api/global/scim/v2/users/:id", () => { From de716ba869f2c74195d7c2b4b3c4510e34092e4e Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 15:07:03 +0100 Subject: [PATCH 058/135] Add extra tests --- .../routes/global/tests/scim/users.spec.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index d0c15ca532..5921ae0aeb 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -413,6 +413,34 @@ describe("/api/global/scim/v2/users", () => { const persistedUser = await config.api.scimUsersAPI.find(user.id) expect(persistedUser).toEqual(expectedScimUser) }) + + it("supports updating unmapped fields", async () => { + const body: ScimUpdateRequest = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [ + { + op: "Add", + path: "displayName", + value: structures.generator.name(), + }, + { + op: "Add", + path: "preferredLanguage", + value: structures.generator.letter(), + }, + ], + } + + const response = await patchScimUser({ id: user.id, body }) + + const expectedScimUser: ScimUserResponse = { + ...user, + } + expect(response).toEqual(expectedScimUser) + + const persistedUser = await config.api.scimUsersAPI.find(user.id) + expect(persistedUser).toEqual(expectedScimUser) + }) }) describe("DELETE /api/global/scim/v2/users/:id", () => { From e222381a6cff01f45edd37ba205dce8b6d8d05ba Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 15:16:36 +0100 Subject: [PATCH 059/135] Test extra cases --- .../types/src/api/web/global/scim/users.ts | 2 +- .../routes/global/tests/scim/users.spec.ts | 35 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/types/src/api/web/global/scim/users.ts b/packages/types/src/api/web/global/scim/users.ts index 72b7078bb4..143419829c 100644 --- a/packages/types/src/api/web/global/scim/users.ts +++ b/packages/types/src/api/web/global/scim/users.ts @@ -1,6 +1,6 @@ import { ScimResource, ScimMeta, ScimPatchOperation } from "scim-patch" -type BooleanString = boolean | "True" | "False" +type BooleanString = boolean | "True" | "true" | "False" | "false" export interface ScimUserResponse extends ScimResource { schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"] diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 5921ae0aeb..12910e63fe 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -396,23 +396,26 @@ describe("/api/global/scim/v2/users", () => { expect(persistedUser).toEqual(expectedScimUser) }) - it("can deactive an active user", async () => { - const body: ScimUpdateRequest = { - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], - Operations: [{ op: "Replace", path: "active", value: "False" }], + it.each([false, "false", "False"])( + "can deactive an active user (sending %s)", + async activeValue => { + const body: ScimUpdateRequest = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [{ op: "Replace", path: "active", value: activeValue }], + } + + const response = await patchScimUser({ id: user.id, body }) + + const expectedScimUser: ScimUserResponse = { + ...user, + active: false, + } + expect(response).toEqual(expectedScimUser) + + const persistedUser = await config.api.scimUsersAPI.find(user.id) + expect(persistedUser).toEqual(expectedScimUser) } - - const response = await patchScimUser({ id: user.id, body }) - - const expectedScimUser: ScimUserResponse = { - ...user, - active: false, - } - expect(response).toEqual(expectedScimUser) - - const persistedUser = await config.api.scimUsersAPI.find(user.id) - expect(persistedUser).toEqual(expectedScimUser) - }) + ) it("supports updating unmapped fields", async () => { const body: ScimUpdateRequest = { From 51ebad2b14f709dd3cf568ea0a9cc46e6b8676b4 Mon Sep 17 00:00:00 2001 From: adrinr Date: Thu, 16 Mar 2023 15:19:16 +0100 Subject: [PATCH 060/135] Test activations --- .../routes/global/tests/scim/users.spec.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 12910e63fe..0cca16c432 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -417,6 +417,36 @@ describe("/api/global/scim/v2/users", () => { } ) + it.each([true, "true", "True"])( + "can activate an inactive user (sending %s)", + async activeValue => { + // Deactivate user + await patchScimUser({ + id: user.id, + body: { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [{ op: "Replace", path: "active", value: true }], + }, + }) + + const body: ScimUpdateRequest = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [{ op: "Replace", path: "active", value: activeValue }], + } + + const response = await patchScimUser({ id: user.id, body }) + + const expectedScimUser: ScimUserResponse = { + ...user, + active: true, + } + expect(response).toEqual(expectedScimUser) + + const persistedUser = await config.api.scimUsersAPI.find(user.id) + expect(persistedUser).toEqual(expectedScimUser) + } + ) + it("supports updating unmapped fields", async () => { const body: ScimUpdateRequest = { schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], From 4e82957e52f178af68dfda57a60c2255ed3ffd53 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 17 Mar 2023 11:17:41 +0100 Subject: [PATCH 061/135] Fix test --- packages/worker/src/api/routes/global/tests/scim/users.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 0cca16c432..5d5ff3f17b 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -78,7 +78,6 @@ describe("/api/global/scim/v2/users", () => { error: { code: "feature_disabled", featureName: "scimIntegration", - type: "license_error", }, message: "scimIntegration is not currently enabled", status: 400, From fd0c88afacbf3f889d515f74649d196154714efd Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 17 Mar 2023 10:11:28 +0100 Subject: [PATCH 062/135] Add types --- .../types/src/api/web/global/scim/groups.ts | 27 +++++++++++++++++++ .../types/src/api/web/global/scim/index.ts | 1 + .../types/src/api/web/global/scim/shared.ts | 7 +++++ .../types/src/api/web/global/scim/users.ts | 9 +------ 4 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 packages/types/src/api/web/global/scim/groups.ts create mode 100644 packages/types/src/api/web/global/scim/shared.ts diff --git a/packages/types/src/api/web/global/scim/groups.ts b/packages/types/src/api/web/global/scim/groups.ts new file mode 100644 index 0000000000..e2f161f1a6 --- /dev/null +++ b/packages/types/src/api/web/global/scim/groups.ts @@ -0,0 +1,27 @@ +import { ScimResource, ScimMeta } from "scim-patch" +import { ScimListResponse } from "./shared" + +export interface ScimGroupResponse extends ScimResource { + schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"] + id: string + externalId: string + meta: ScimMeta & { + resourceType: "Group" + } + displayName: string +} + +export interface ScimCreateGroupRequest { + schemas: [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "http://schemas.microsoft.com/2006/11/ResourceManagement/ADSCIM/2.0/Group" + ] + externalId: string + displayName: string + meta: { + resourceType: "Group" + } +} + +export interface ScimGroupListResponse + extends ScimListResponse {} diff --git a/packages/types/src/api/web/global/scim/index.ts b/packages/types/src/api/web/global/scim/index.ts index 056d6e5675..e084a0dd1e 100644 --- a/packages/types/src/api/web/global/scim/index.ts +++ b/packages/types/src/api/web/global/scim/index.ts @@ -1 +1,2 @@ export * from "./users" +export * from "./groups" diff --git a/packages/types/src/api/web/global/scim/shared.ts b/packages/types/src/api/web/global/scim/shared.ts new file mode 100644 index 0000000000..18b9519850 --- /dev/null +++ b/packages/types/src/api/web/global/scim/shared.ts @@ -0,0 +1,7 @@ +export interface ScimListResponse { + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"] + totalResults: number + Resources: T[] + startIndex: number + itemsPerPage: number +} diff --git a/packages/types/src/api/web/global/scim/users.ts b/packages/types/src/api/web/global/scim/users.ts index 143419829c..edc9b8e37e 100644 --- a/packages/types/src/api/web/global/scim/users.ts +++ b/packages/types/src/api/web/global/scim/users.ts @@ -1,4 +1,5 @@ import { ScimResource, ScimMeta, ScimPatchOperation } from "scim-patch" +import { ScimListResponse } from "./shared" type BooleanString = boolean | "True" | "true" | "False" | "false" @@ -56,13 +57,5 @@ export interface ScimUpdateRequest { Operations: ScimPatchOperation[] } -interface ScimListResponse { - schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"] - totalResults: number - Resources: T[] - startIndex: number - itemsPerPage: number -} - export interface ScimUserListResponse extends ScimListResponse {} From 8196277a01d61e61d784314b693e95d41d65b19e Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 17 Mar 2023 10:17:12 +0100 Subject: [PATCH 063/135] Add scim group api test tools --- packages/worker/src/tests/api/index.ts | 3 ++ packages/worker/src/tests/api/scim/groups.ts | 55 ++++++++++++++++++++ packages/worker/src/tests/api/scim/shared.ts | 43 +++++++++++++++ packages/worker/src/tests/api/scim/users.ts | 49 +++-------------- 4 files changed, 108 insertions(+), 42 deletions(-) create mode 100644 packages/worker/src/tests/api/scim/groups.ts create mode 100644 packages/worker/src/tests/api/scim/shared.ts diff --git a/packages/worker/src/tests/api/index.ts b/packages/worker/src/tests/api/index.ts index 89f38d63fb..fa4e54184c 100644 --- a/packages/worker/src/tests/api/index.ts +++ b/packages/worker/src/tests/api/index.ts @@ -16,6 +16,7 @@ import { TemplatesAPI } from "./templates" import { LicenseAPI } from "./license" import { AuditLogAPI } from "./auditLogs" import { ScimUsersAPI } from "./scim/users" +import { ScimGroupsAPI } from "./scim/groups" export default class API { accounts: AccountAPI @@ -35,6 +36,7 @@ export default class API { license: LicenseAPI auditLogs: AuditLogAPI scimUsersAPI: ScimUsersAPI + scimGroupsAPI: ScimGroupsAPI constructor(config: TestConfiguration) { this.accounts = new AccountAPI(config) @@ -54,5 +56,6 @@ export default class API { this.license = new LicenseAPI(config) this.auditLogs = new AuditLogAPI(config) this.scimUsersAPI = new ScimUsersAPI(config) + this.scimGroupsAPI = new ScimGroupsAPI(config) } } diff --git a/packages/worker/src/tests/api/scim/groups.ts b/packages/worker/src/tests/api/scim/groups.ts new file mode 100644 index 0000000000..2f0da03fe7 --- /dev/null +++ b/packages/worker/src/tests/api/scim/groups.ts @@ -0,0 +1,55 @@ +import { + ScimCreateGroupRequest, + ScimGroupListResponse, + ScimGroupResponse, +} from "@budibase/types" +import TestConfiguration from "../../TestConfiguration" +import { RequestSettings, ScimTestAPI } from "./shared" + +export class ScimGroupsAPI extends ScimTestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + get = async ( + requestSettings?: Partial & { + params?: { + startIndex?: number + pageSize?: number + filter?: string + } + } + ) => { + let url = `/api/global/scim/v2/groups?` + const params = requestSettings?.params + if (params?.pageSize) { + url += `count=${params.pageSize}&` + } + if (params?.startIndex) { + url += `startIndex=${params.startIndex}&` + } + if (params?.filter) { + url += `filter=${params.filter}&` + } + const res = await this.call(url, "get", requestSettings) + return res.body as ScimGroupListResponse + } + + post = async ( + { + body, + }: { + body: ScimCreateGroupRequest + }, + requestSettings?: Partial + ) => { + const res = await this.call( + `/api/global/scim/v2/groups`, + "post", + requestSettings, + body + ) + + return res.body as ScimGroupResponse + } +} diff --git a/packages/worker/src/tests/api/scim/shared.ts b/packages/worker/src/tests/api/scim/shared.ts new file mode 100644 index 0000000000..d99f4c20dc --- /dev/null +++ b/packages/worker/src/tests/api/scim/shared.ts @@ -0,0 +1,43 @@ +import TestConfiguration from "../../TestConfiguration" +import { TestAPI } from "../base" + +const defaultConfig = { + expect: 200, + setHeaders: true, +} + +export type RequestSettings = typeof defaultConfig + +export abstract class ScimTestAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + call = ( + url: string, + method: "get" | "post" | "patch" | "delete", + requestSettings?: Partial, + body?: object + ) => { + const { expect, setHeaders } = { ...defaultConfig, ...requestSettings } + let request = this.request[method](url).expect(expect) + + request = request.set( + "content-type", + "application/scim+json; charset=utf-8" + ) + + if (method !== "delete") { + request = request.expect("Content-Type", /json/) + } + + if (body) { + request = request.send(body) + } + + if (setHeaders) { + request = request.set(this.config.bearerAPIHeaders()) + } + return request + } +} diff --git a/packages/worker/src/tests/api/scim/users.ts b/packages/worker/src/tests/api/scim/users.ts index 85e38e9978..318492d378 100644 --- a/packages/worker/src/tests/api/scim/users.ts +++ b/packages/worker/src/tests/api/scim/users.ts @@ -5,48 +5,13 @@ import { ScimUpdateRequest, } from "@budibase/types" import TestConfiguration from "../../TestConfiguration" -import { TestAPI } from "../base" +import { RequestSettings, ScimTestAPI } from "./shared" -const defaultConfig = { - expect: 200, - setHeaders: true, -} - -type RequestSettings = typeof defaultConfig - -export class ScimUsersAPI extends TestAPI { +export class ScimUsersAPI extends ScimTestAPI { constructor(config: TestConfiguration) { super(config) } - #createRequest = ( - url: string, - method: "get" | "post" | "patch" | "delete", - requestSettings?: Partial, - body?: object - ) => { - const { expect, setHeaders } = { ...defaultConfig, ...requestSettings } - let request = this.request[method](url).expect(expect) - - request = request.set( - "content-type", - "application/scim+json; charset=utf-8" - ) - - if (method !== "delete") { - request = request.expect("Content-Type", /json/) - } - - if (body) { - request = request.send(body) - } - - if (setHeaders) { - request = request.set(this.config.bearerAPIHeaders()) - } - return request - } - get = async ( requestSettings?: Partial & { params?: { @@ -67,12 +32,12 @@ export class ScimUsersAPI extends TestAPI { if (params?.filter) { url += `filter=${params.filter}&` } - const res = await this.#createRequest(url, "get", requestSettings) + const res = await this.call(url, "get", requestSettings) return res.body as ScimUserListResponse } find = async (id: string, requestSettings?: Partial) => { - const res = await this.#createRequest( + const res = await this.call( `/api/global/scim/v2/users/${id}`, "get", requestSettings @@ -88,7 +53,7 @@ export class ScimUsersAPI extends TestAPI { }, requestSettings?: Partial ) => { - const res = await this.#createRequest( + const res = await this.call( `/api/global/scim/v2/users`, "post", requestSettings, @@ -108,7 +73,7 @@ export class ScimUsersAPI extends TestAPI { }, requestSettings?: Partial ) => { - const res = await this.#createRequest( + const res = await this.call( `/api/global/scim/v2/users/${id}`, "patch", requestSettings, @@ -119,7 +84,7 @@ export class ScimUsersAPI extends TestAPI { } delete = async (id: string, requestSettings?: Partial) => { - const res = await this.#createRequest( + const res = await this.call( `/api/global/scim/v2/users/${id}`, "delete", requestSettings From 8f3488707bd1343bcf84b015bd0946af2b1eddc3 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 17 Mar 2023 10:43:19 +0100 Subject: [PATCH 064/135] Add get and create tests --- .../types/src/documents/global/userGroup.ts | 4 + .../routes/global/tests/scim/groups.spec.ts | 183 ++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 packages/worker/src/api/routes/global/tests/scim/groups.spec.ts diff --git a/packages/types/src/documents/global/userGroup.ts b/packages/types/src/documents/global/userGroup.ts index cda74b0536..fedd8426f0 100644 --- a/packages/types/src/documents/global/userGroup.ts +++ b/packages/types/src/documents/global/userGroup.ts @@ -7,6 +7,10 @@ export interface UserGroup extends Document { users?: GroupUser[] roles?: UserGroupRoles createdAt?: number + scimInfo?: { + externalId: string + isSync: boolean + } } export interface GroupUser { diff --git a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts new file mode 100644 index 0000000000..5f8d7cc11b --- /dev/null +++ b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts @@ -0,0 +1,183 @@ +import tk from "timekeeper" +import _ from "lodash" +import { mocks, structures } from "@budibase/backend-core/tests" +import { ScimCreateGroupRequest, ScimGroupResponse } from "@budibase/types" +import { TestConfiguration } from "../../../../../tests" + +mocks.licenses.useScimIntegration() + +function createScimCreateGroupRequest(groupData?: { + externalId?: string + displayName?: string +}) { + const { + externalId = structures.uuid(), + displayName = structures.generator.word(), + } = groupData || {} + + const group: ScimCreateGroupRequest = { + schemas: [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "http://schemas.microsoft.com/2006/11/ResourceManagement/ADSCIM/2.0/Group", + ], + externalId: externalId, + displayName: displayName, + meta: { + resourceType: "Group", + }, + } + return group +} + +describe("/api/global/scim/v2/groups", () => { + let mockedTime = new Date(structures.generator.timestamp()) + + beforeEach(() => { + tk.reset() + mockedTime = new Date(structures.generator.timestamp()) + tk.freeze(mockedTime) + + mocks.licenses.useScimIntegration() + }) + + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + const featureDisabledResponse = { + error: { + code: "feature_disabled", + featureName: "scimIntegration", + }, + message: "scimIntegration is not currently enabled", + status: 400, + } + + describe("GET /api/global/scim/v2/groups", () => { + const getScimGroups = config.api.scimGroupsAPI.get + + it("unauthorised calls are not allowed", async () => { + const response = await getScimGroups({ + setHeaders: false, + expect: 403, + }) + + expect(response).toEqual({ message: "Tenant id not set", status: 403 }) + }) + + it("cannot be called when feature is disabled", async () => { + mocks.licenses.useCloudFree() + const response = await getScimGroups({ expect: 400 }) + + expect(response).toEqual(featureDisabledResponse) + }) + + describe("no groups exist", () => { + it("should retrieve empty list", async () => { + const response = await getScimGroups() + + expect(response).toEqual({ + Resources: [], + itemsPerPage: 0, + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + startIndex: 1, + totalResults: 0, + }) + }) + }) + + describe("multiple groups exist", () => { + const groupCount = 25 + let groups: ScimGroupResponse[] + + beforeAll(async () => { + groups = [] + + for (let i = 0; i < groupCount; i++) { + const body = createScimCreateGroupRequest() + groups.push(await config.api.scimGroupsAPI.post({ body })) + } + + groups = groups.sort((a, b) => (a.id > b.id ? 1 : -1)) + }) + + it("can fetch all groups without filters", async () => { + const response = await getScimGroups() + + expect(response).toEqual({ + Resources: expect.arrayContaining(groups), + itemsPerPage: 25, + schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"], + startIndex: 1, + totalResults: groupCount, + }) + }) + }) + }) + + describe("POST /api/global/scim/v2/groups", () => { + const postScimGroup = config.api.scimGroupsAPI.post + + beforeAll(async () => { + await config.useNewTenant() + }) + + it("unauthorised calls are not allowed", async () => { + const response = await postScimGroup( + { body: {} as any }, + { + setHeaders: false, + expect: 403, + } + ) + + expect(response).toEqual({ message: "Tenant id not set", status: 403 }) + }) + + it("cannot be called when feature is disabled", async () => { + mocks.licenses.useCloudFree() + const response = await postScimGroup({ body: {} as any }, { expect: 400 }) + + expect(response).toEqual(featureDisabledResponse) + }) + + describe("no groups exist", () => { + it("a new group can be created and persisted", async () => { + const groupData = { + externalId: structures.uuid(), + displayName: structures.generator.word(), + } + const body = createScimCreateGroupRequest(groupData) + + const response = await postScimGroup({ body }) + + const expectedScimGroup = { + schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"], + id: expect.any(String), + externalId: groupData.externalId, + displayName: groupData.displayName, + meta: { + resourceType: "Group", + created: mockedTime.toISOString(), + lastModified: mockedTime.toISOString(), + }, + } + expect(response).toEqual(expectedScimGroup) + + const persistedGroups = await config.api.scimGroupsAPI.get() + expect(persistedGroups).toEqual( + expect.objectContaining({ + totalResults: 1, + Resources: [expectedScimGroup], + }) + ) + }) + }) + }) +}) From 5dbbdf3f825783b547f0cd5a5167cba3ac9cd9a1 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 20 Mar 2023 16:11:39 +0000 Subject: [PATCH 065/135] Use generic mock dates --- .../src/api/routes/global/tests/scim/groups.spec.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts index 5f8d7cc11b..04785f16e4 100644 --- a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts @@ -30,12 +30,8 @@ function createScimCreateGroupRequest(groupData?: { } describe("/api/global/scim/v2/groups", () => { - let mockedTime = new Date(structures.generator.timestamp()) - beforeEach(() => { - tk.reset() - mockedTime = new Date(structures.generator.timestamp()) - tk.freeze(mockedTime) + tk.freeze(mocks.date.MOCK_DATE) mocks.licenses.useScimIntegration() }) @@ -149,6 +145,9 @@ describe("/api/global/scim/v2/groups", () => { describe("no groups exist", () => { it("a new group can be created and persisted", async () => { + const mockedTime = new Date(structures.generator.timestamp()) + tk.freeze(mockedTime) + const groupData = { externalId: structures.uuid(), displayName: structures.generator.word(), From beb4118582c60a1aae71f16852c26286e4b2c47c Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 20 Mar 2023 16:12:55 +0000 Subject: [PATCH 066/135] Renames --- packages/backend-core/tests/utilities/mocks/licenses.ts | 2 +- packages/types/src/sdk/licensing/feature.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-core/tests/utilities/mocks/licenses.ts b/packages/backend-core/tests/utilities/mocks/licenses.ts index 85bf9c8a35..839b22e5f9 100644 --- a/packages/backend-core/tests/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/utilities/mocks/licenses.ts @@ -87,7 +87,7 @@ export const useAuditLogs = () => { } export const useScimIntegration = () => { - return useFeature(Feature.SCIM_INTEGRATION) + return useFeature(Feature.SCIM) } // QUOTAS diff --git a/packages/types/src/sdk/licensing/feature.ts b/packages/types/src/sdk/licensing/feature.ts index 7af42e9d28..8274a95d34 100644 --- a/packages/types/src/sdk/licensing/feature.ts +++ b/packages/types/src/sdk/licensing/feature.ts @@ -5,5 +5,5 @@ export enum Feature { AUDIT_LOGS = "auditLogs", ENFORCEABLE_SSO = "enforceableSSO", BRANDING = "branding", - SCIM_INTEGRATION = "scimIntegration", + SCIM = "scim", } From 10e465e07d17bebc2cae456a92a813e72dcc22aa Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 20 Mar 2023 16:28:35 +0000 Subject: [PATCH 067/135] Fix merge conflicts --- .../worker/src/api/routes/global/tests/scim/groups.spec.ts | 4 ++-- .../worker/src/api/routes/global/tests/scim/users.spec.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts index 04785f16e4..1ef2825254 100644 --- a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts @@ -49,9 +49,9 @@ describe("/api/global/scim/v2/groups", () => { const featureDisabledResponse = { error: { code: "feature_disabled", - featureName: "scimIntegration", + featureName: "scim", }, - message: "scimIntegration is not currently enabled", + message: "scim is not currently enabled", status: 400, } diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 5d5ff3f17b..c8753af405 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -77,9 +77,9 @@ describe("/api/global/scim/v2/users", () => { const featureDisabledResponse = { error: { code: "feature_disabled", - featureName: "scimIntegration", + featureName: "scim", }, - message: "scimIntegration is not currently enabled", + message: "scim is not currently enabled", status: 400, } From 771e3b886242cadf48413ee351f36be951d03de6 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Mar 2023 11:20:17 +0000 Subject: [PATCH 068/135] Bookmark optional --- packages/backend-core/src/db/lucene.ts | 1 - packages/types/src/api/web/pagination.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend-core/src/db/lucene.ts b/packages/backend-core/src/db/lucene.ts index 50ec35f955..6f2f4fc991 100644 --- a/packages/backend-core/src/db/lucene.ts +++ b/packages/backend-core/src/db/lucene.ts @@ -555,7 +555,6 @@ async function runQuery( let output: SearchResponse = { rows: [], - totalRows: 0, } if (json.rows != null && json.rows.length > 0) { diff --git a/packages/types/src/api/web/pagination.ts b/packages/types/src/api/web/pagination.ts index ae4c56971a..c61f2306ca 100644 --- a/packages/types/src/api/web/pagination.ts +++ b/packages/types/src/api/web/pagination.ts @@ -22,6 +22,6 @@ export interface PaginationRequest extends BasicPaginationRequest { } export interface PaginationResponse { - bookmark: string + bookmark: string | undefined hasNextPage: boolean } From 6de4588fc1c5ff4bd2d264e3af45ae49575fcf9c Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 17 Mar 2023 11:50:22 +0100 Subject: [PATCH 069/135] Implement find endpoint --- .../routes/global/tests/scim/groups.spec.ts | 43 +++++++++++++++++++ packages/worker/src/tests/api/scim/groups.ts | 9 ++++ 2 files changed, 52 insertions(+) diff --git a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts index 1ef2825254..f2d13cbc7c 100644 --- a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts @@ -179,4 +179,47 @@ describe("/api/global/scim/v2/groups", () => { }) }) }) + + describe("GET /api/global/scim/v2/groups/:id", () => { + let group: ScimGroupResponse + + beforeEach(async () => { + const body = createScimCreateGroupRequest() + + group = await config.api.scimGroupsAPI.post({ body }) + }) + + const findScimGroup = config.api.scimGroupsAPI.find + + it("unauthorised calls are not allowed", async () => { + const response = await findScimGroup(group.id, { + setHeaders: false, + expect: 403, + }) + + expect(response).toEqual({ message: "Tenant id not set", status: 403 }) + }) + + it("cannot be called when feature is disabled", async () => { + mocks.licenses.useCloudFree() + const response = await findScimGroup(group.id, { expect: 400 }) + + expect(response).toEqual(featureDisabledResponse) + }) + + it("should return existing group", async () => { + const response = await findScimGroup(group.id) + + expect(response).toEqual(group) + }) + + it("should return 404 when requesting unexisting group id", async () => { + const response = await findScimGroup(structures.uuid(), { expect: 404 }) + + expect(response).toEqual({ + message: "missing", + status: 404, + }) + }) + }) }) diff --git a/packages/worker/src/tests/api/scim/groups.ts b/packages/worker/src/tests/api/scim/groups.ts index 2f0da03fe7..7f3bc4803b 100644 --- a/packages/worker/src/tests/api/scim/groups.ts +++ b/packages/worker/src/tests/api/scim/groups.ts @@ -52,4 +52,13 @@ export class ScimGroupsAPI extends ScimTestAPI { return res.body as ScimGroupResponse } + + find = async (id: string, requestSettings?: Partial) => { + const res = await this.call( + `/api/global/scim/v2/groups/${id}`, + "get", + requestSettings + ) + return res.body as ScimGroupResponse + } } From baca156a174b84ceacc09a34ea97f7f39ae705d6 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 17 Mar 2023 12:23:24 +0100 Subject: [PATCH 070/135] Implement delete endpoint --- .../routes/global/tests/scim/groups.spec.ts | 40 +++++++++++++++++++ packages/worker/src/tests/api/scim/groups.ts | 9 +++++ 2 files changed, 49 insertions(+) diff --git a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts index f2d13cbc7c..4076ab3477 100644 --- a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts @@ -222,4 +222,44 @@ describe("/api/global/scim/v2/groups", () => { }) }) }) + + describe("DELETE /api/global/scim/v2/groups/:id", () => { + const deleteScimGroup = config.api.scimGroupsAPI.delete + + let group: ScimGroupResponse + + beforeEach(async () => { + const body = createScimCreateGroupRequest() + + group = await config.api.scimGroupsAPI.post({ body }) + }) + + it("unauthorised calls are not allowed", async () => { + const response = await deleteScimGroup(group.id, { + setHeaders: false, + expect: 403, + }) + + expect(response).toEqual({ message: "Tenant id not set", status: 403 }) + }) + + it("cannot be called when feature is disabled", async () => { + mocks.licenses.useCloudFree() + const response = await deleteScimGroup(group.id, { expect: 400 }) + + expect(response).toEqual(featureDisabledResponse) + }) + + it("an existing group can be deleted", async () => { + const response = await deleteScimGroup(group.id, { expect: 204 }) + + expect(response).toEqual({}) + + await config.api.scimGroupsAPI.find(group.id, { expect: 404 }) + }) + + it("an non existing group can not be deleted", async () => { + await deleteScimGroup(structures.uuid(), { expect: 404 }) + }) + }) }) diff --git a/packages/worker/src/tests/api/scim/groups.ts b/packages/worker/src/tests/api/scim/groups.ts index 7f3bc4803b..413e85b96a 100644 --- a/packages/worker/src/tests/api/scim/groups.ts +++ b/packages/worker/src/tests/api/scim/groups.ts @@ -61,4 +61,13 @@ export class ScimGroupsAPI extends ScimTestAPI { ) return res.body as ScimGroupResponse } + + delete = async (id: string, requestSettings?: Partial) => { + const res = await this.call( + `/api/global/scim/v2/groups/${id}`, + "delete", + requestSettings + ) + return res.body as ScimGroupResponse + } } From 586275ed896b9d041f05cdabb29574e40afad630 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 17 Mar 2023 12:37:29 +0100 Subject: [PATCH 071/135] Allow fields edit --- .../types/src/api/web/global/scim/index.ts | 1 + .../types/src/api/web/global/scim/shared.ts | 7 +++ .../types/src/api/web/global/scim/users.ts | 5 -- .../routes/global/tests/scim/groups.spec.ts | 60 ++++++++++++++++++- packages/worker/src/tests/api/scim/groups.ts | 21 +++++++ 5 files changed, 88 insertions(+), 6 deletions(-) diff --git a/packages/types/src/api/web/global/scim/index.ts b/packages/types/src/api/web/global/scim/index.ts index e084a0dd1e..167d09b7c7 100644 --- a/packages/types/src/api/web/global/scim/index.ts +++ b/packages/types/src/api/web/global/scim/index.ts @@ -1,2 +1,3 @@ export * from "./users" export * from "./groups" +export * from "./shared" diff --git a/packages/types/src/api/web/global/scim/shared.ts b/packages/types/src/api/web/global/scim/shared.ts index 18b9519850..c01c9c8590 100644 --- a/packages/types/src/api/web/global/scim/shared.ts +++ b/packages/types/src/api/web/global/scim/shared.ts @@ -1,3 +1,5 @@ +import { ScimPatchOperation } from "scim-patch" + export interface ScimListResponse { schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"] totalResults: number @@ -5,3 +7,8 @@ export interface ScimListResponse { startIndex: number itemsPerPage: number } + +export interface ScimUpdateRequest { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"] + Operations: ScimPatchOperation[] +} diff --git a/packages/types/src/api/web/global/scim/users.ts b/packages/types/src/api/web/global/scim/users.ts index edc9b8e37e..5dddee1f14 100644 --- a/packages/types/src/api/web/global/scim/users.ts +++ b/packages/types/src/api/web/global/scim/users.ts @@ -52,10 +52,5 @@ export interface ScimCreateUserRequest { roles: [] } -export interface ScimUpdateRequest { - schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"] - Operations: ScimPatchOperation[] -} - export interface ScimUserListResponse extends ScimListResponse {} diff --git a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts index 4076ab3477..904c20cf92 100644 --- a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts @@ -1,7 +1,11 @@ import tk from "timekeeper" import _ from "lodash" import { mocks, structures } from "@budibase/backend-core/tests" -import { ScimCreateGroupRequest, ScimGroupResponse } from "@budibase/types" +import { + ScimCreateGroupRequest, + ScimGroupResponse, + ScimUpdateRequest, +} from "@budibase/types" import { TestConfiguration } from "../../../../../tests" mocks.licenses.useScimIntegration() @@ -262,4 +266,58 @@ describe("/api/global/scim/v2/groups", () => { await deleteScimGroup(structures.uuid(), { expect: 404 }) }) }) + + describe("PATCH /api/global/scim/v2/groups/:id", () => { + const patchScimGroup = config.api.scimGroupsAPI.patch + + let group: ScimGroupResponse + + beforeEach(async () => { + const body = createScimCreateGroupRequest() + + group = await config.api.scimGroupsAPI.post({ body }) + }) + + it("unauthorised calls are not allowed", async () => { + const response = await patchScimGroup({} as any, { + setHeaders: false, + expect: 403, + }) + + expect(response).toEqual({ message: "Tenant id not set", status: 403 }) + }) + + it("cannot be called when feature is disabled", async () => { + mocks.licenses.useCloudFree() + const response = await patchScimGroup({} as any, { expect: 400 }) + + expect(response).toEqual(featureDisabledResponse) + }) + + it("an existing group can be updated", async () => { + const newDisplayName = structures.generator.word() + + const body: ScimUpdateRequest = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [ + { + op: "Replace", + path: "displayName", + value: newDisplayName, + }, + ], + } + + const response = await patchScimGroup({ id: group.id, body }) + + const expectedScimGroup: ScimGroupResponse = { + ...group, + displayName: newDisplayName, + } + expect(response).toEqual(expectedScimGroup) + + const persistedGroup = await config.api.scimGroupsAPI.find(group.id) + expect(persistedGroup).toEqual(expectedScimGroup) + }) + }) }) diff --git a/packages/worker/src/tests/api/scim/groups.ts b/packages/worker/src/tests/api/scim/groups.ts index 413e85b96a..96ff9aeb67 100644 --- a/packages/worker/src/tests/api/scim/groups.ts +++ b/packages/worker/src/tests/api/scim/groups.ts @@ -2,6 +2,7 @@ import { ScimCreateGroupRequest, ScimGroupListResponse, ScimGroupResponse, + ScimUpdateRequest, } from "@budibase/types" import TestConfiguration from "../../TestConfiguration" import { RequestSettings, ScimTestAPI } from "./shared" @@ -70,4 +71,24 @@ export class ScimGroupsAPI extends ScimTestAPI { ) return res.body as ScimGroupResponse } + + patch = async ( + { + id, + body, + }: { + id: string + body: ScimUpdateRequest + }, + requestSettings?: Partial + ) => { + const res = await this.call( + `/api/global/scim/v2/groups/${id}`, + "patch", + requestSettings, + body + ) + + return res.body as ScimGroupResponse + } } From 4ac682a3c25d71111083eb291d4a58a96a3c56c7 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 17 Mar 2023 16:35:12 +0100 Subject: [PATCH 072/135] Move creators to structures --- .../tests/utilities/structures/index.ts | 1 + .../tests/utilities/structures/scim.ts | 67 +++++++++++++++++++ .../routes/global/tests/scim/groups.spec.ts | 44 +++++------- .../routes/global/tests/scim/users.spec.ts | 53 ++------------- 4 files changed, 88 insertions(+), 77 deletions(-) create mode 100644 packages/backend-core/tests/utilities/structures/scim.ts diff --git a/packages/backend-core/tests/utilities/structures/index.ts b/packages/backend-core/tests/utilities/structures/index.ts index ca77f476d0..c6e3195ebf 100644 --- a/packages/backend-core/tests/utilities/structures/index.ts +++ b/packages/backend-core/tests/utilities/structures/index.ts @@ -9,3 +9,4 @@ export * as sso from "./sso" export * as tenant from "./tenants" export * as users from "./users" export { generator } from "./generator" +export * as scim from "./scim" diff --git a/packages/backend-core/tests/utilities/structures/scim.ts b/packages/backend-core/tests/utilities/structures/scim.ts new file mode 100644 index 0000000000..2c9ad6c622 --- /dev/null +++ b/packages/backend-core/tests/utilities/structures/scim.ts @@ -0,0 +1,67 @@ +import { ScimCreateGroupRequest, ScimCreateUserRequest } from "@budibase/types" +import { uuid } from "./common" +import { generator } from "./generator" + +export function createUserRequest(userData?: { + externalId?: string + email?: string + firstName?: string + lastName?: string + username?: string +}) { + const { + externalId = uuid(), + email = generator.email(), + firstName = generator.first(), + lastName = generator.last(), + username = generator.name(), + } = userData || {} + + const user: ScimCreateUserRequest = { + schemas: [ + "urn:ietf:params:scim:schemas:core:2.0:User", + "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", + ], + externalId, + userName: username, + active: true, + emails: [ + { + primary: true, + type: "work", + value: email, + }, + ], + meta: { + resourceType: "User", + }, + name: { + formatted: generator.name(), + familyName: lastName, + givenName: firstName, + }, + roles: [], + } + return user +} + +export function createGroupRequest(groupData?: { + externalId?: string + displayName?: string +}) { + const { externalId = uuid(), displayName = generator.word() } = + groupData || {} + + const group: ScimCreateGroupRequest = { + schemas: [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + "http://schemas.microsoft.com/2006/11/ResourceManagement/ADSCIM/2.0/Group", + ], + externalId: externalId, + displayName: displayName, + meta: { + resourceType: "Group", + }, + } + return group +} diff --git a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts index 904c20cf92..a553fbb7dd 100644 --- a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts @@ -2,38 +2,17 @@ import tk from "timekeeper" import _ from "lodash" import { mocks, structures } from "@budibase/backend-core/tests" import { - ScimCreateGroupRequest, ScimGroupResponse, ScimUpdateRequest, + ScimUserResponse, } from "@budibase/types" import { TestConfiguration } from "../../../../../tests" mocks.licenses.useScimIntegration() -function createScimCreateGroupRequest(groupData?: { - externalId?: string - displayName?: string -}) { - const { - externalId = structures.uuid(), - displayName = structures.generator.word(), - } = groupData || {} - - const group: ScimCreateGroupRequest = { - schemas: [ - "urn:ietf:params:scim:schemas:core:2.0:Group", - "http://schemas.microsoft.com/2006/11/ResourceManagement/ADSCIM/2.0/Group", - ], - externalId: externalId, - displayName: displayName, - meta: { - resourceType: "Group", - }, - } - return group -} - describe("/api/global/scim/v2/groups", () => { + let users: ScimUserResponse[] + beforeEach(() => { tk.freeze(mocks.date.MOCK_DATE) @@ -44,6 +23,13 @@ describe("/api/global/scim/v2/groups", () => { beforeAll(async () => { await config.beforeAll() + + for (let i = 0; i < 30; i++) { + const body = structures.scim.createUserRequest() + users.push(await config.api.scimUsersAPI.post({ body })) + } + + users = users.sort((a, b) => (a.id > b.id ? 1 : -1)) }) afterAll(async () => { @@ -100,7 +86,7 @@ describe("/api/global/scim/v2/groups", () => { groups = [] for (let i = 0; i < groupCount; i++) { - const body = createScimCreateGroupRequest() + const body = structures.scim.createGroupRequest() groups.push(await config.api.scimGroupsAPI.post({ body })) } @@ -156,7 +142,7 @@ describe("/api/global/scim/v2/groups", () => { externalId: structures.uuid(), displayName: structures.generator.word(), } - const body = createScimCreateGroupRequest(groupData) + const body = structures.scim.createGroupRequest(groupData) const response = await postScimGroup({ body }) @@ -188,7 +174,7 @@ describe("/api/global/scim/v2/groups", () => { let group: ScimGroupResponse beforeEach(async () => { - const body = createScimCreateGroupRequest() + const body = structures.scim.createGroupRequest() group = await config.api.scimGroupsAPI.post({ body }) }) @@ -233,7 +219,7 @@ describe("/api/global/scim/v2/groups", () => { let group: ScimGroupResponse beforeEach(async () => { - const body = createScimCreateGroupRequest() + const body = structures.scim.createGroupRequest() group = await config.api.scimGroupsAPI.post({ body }) }) @@ -273,7 +259,7 @@ describe("/api/global/scim/v2/groups", () => { let group: ScimGroupResponse beforeEach(async () => { - const body = createScimCreateGroupRequest() + const body = structures.scim.createGroupRequest() group = await config.api.scimGroupsAPI.post({ body }) }) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index c8753af405..a26be36e62 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -10,49 +10,6 @@ import { TestConfiguration } from "../../../../../tests" mocks.licenses.useScimIntegration() -function createScimCreateUserRequest(userData?: { - externalId?: string - email?: string - firstName?: string - lastName?: string - username?: string -}) { - const { - externalId = structures.uuid(), - email = structures.generator.email(), - firstName = structures.generator.first(), - lastName = structures.generator.last(), - username = structures.generator.name(), - } = userData || {} - - const user: ScimCreateUserRequest = { - schemas: [ - "urn:ietf:params:scim:schemas:core:2.0:User", - "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", - ], - externalId, - userName: username, - active: true, - emails: [ - { - primary: true, - type: "work", - value: email, - }, - ], - meta: { - resourceType: "User", - }, - name: { - formatted: structures.generator.name(), - familyName: lastName, - givenName: firstName, - }, - roles: [], - } - return user -} - describe("/api/global/scim/v2/users", () => { let mockedTime = new Date(structures.generator.timestamp()) @@ -124,7 +81,7 @@ describe("/api/global/scim/v2/users", () => { users = [] for (let i = 0; i < userCount; i++) { - const body = createScimCreateUserRequest() + const body = structures.scim.createUserRequest() users.push(await config.api.scimUsersAPI.post({ body })) } @@ -248,7 +205,7 @@ describe("/api/global/scim/v2/users", () => { lastName: structures.generator.last(), username: structures.generator.name(), } - const body = createScimCreateUserRequest(userData) + const body = structures.scim.createUserRequest(userData) const response = await postScimUser({ body }) @@ -293,7 +250,7 @@ describe("/api/global/scim/v2/users", () => { let user: ScimUserResponse beforeEach(async () => { - const body = createScimCreateUserRequest() + const body = structures.scim.createUserRequest() user = await config.api.scimUsersAPI.post({ body }) }) @@ -338,7 +295,7 @@ describe("/api/global/scim/v2/users", () => { let user: ScimUserResponse beforeEach(async () => { - const body = createScimCreateUserRequest() + const body = structures.scim.createUserRequest() user = await config.api.scimUsersAPI.post({ body }) }) @@ -481,7 +438,7 @@ describe("/api/global/scim/v2/users", () => { let user: ScimUserResponse beforeEach(async () => { - const body = createScimCreateUserRequest() + const body = structures.scim.createUserRequest() user = await config.api.scimUsersAPI.post({ body }) }) From 9d0aff96e41bd4419d9898a10a5bb374aac5dc16 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 17 Mar 2023 18:40:42 +0100 Subject: [PATCH 073/135] Add members to group --- packages/types/src/api/web/global/scim/groups.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/types/src/api/web/global/scim/groups.ts b/packages/types/src/api/web/global/scim/groups.ts index e2f161f1a6..21836bccd2 100644 --- a/packages/types/src/api/web/global/scim/groups.ts +++ b/packages/types/src/api/web/global/scim/groups.ts @@ -5,10 +5,13 @@ export interface ScimGroupResponse extends ScimResource { schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"] id: string externalId: string + displayName: string meta: ScimMeta & { resourceType: "Group" } - displayName: string + members?: { + value: string + }[] } export interface ScimCreateGroupRequest { From 76cb3e60613392166f95ca6f081f95685f1f0fa3 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 17 Mar 2023 18:41:05 +0100 Subject: [PATCH 074/135] Test adding user --- .../routes/global/tests/scim/groups.spec.ts | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts index a553fbb7dd..0f0706632a 100644 --- a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts @@ -23,13 +23,6 @@ describe("/api/global/scim/v2/groups", () => { beforeAll(async () => { await config.beforeAll() - - for (let i = 0; i < 30; i++) { - const body = structures.scim.createUserRequest() - users.push(await config.api.scimUsersAPI.post({ body })) - } - - users = users.sort((a, b) => (a.id > b.id ? 1 : -1)) }) afterAll(async () => { @@ -156,6 +149,7 @@ describe("/api/global/scim/v2/groups", () => { created: mockedTime.toISOString(), lastModified: mockedTime.toISOString(), }, + members: [], } expect(response).toEqual(expectedScimGroup) @@ -257,6 +251,17 @@ describe("/api/global/scim/v2/groups", () => { const patchScimGroup = config.api.scimGroupsAPI.patch let group: ScimGroupResponse + let users: ScimUserResponse[] + + beforeAll(async () => { + users = [] + for (let i = 0; i < 30; i++) { + const body = structures.scim.createUserRequest() + users.push(await config.api.scimUsersAPI.post({ body })) + } + + users = users.sort((a, b) => (a.id > b.id ? 1 : -1)) + }) beforeEach(async () => { const body = structures.scim.createGroupRequest() @@ -305,5 +310,42 @@ describe("/api/global/scim/v2/groups", () => { const persistedGroup = await config.api.scimGroupsAPI.find(group.id) expect(persistedGroup).toEqual(expectedScimGroup) }) + + describe("adding users", () => { + it("new users can be added to an existing group", async () => { + const userToAdd = _.sample(users)! + + const body: ScimUpdateRequest = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [ + { + op: "Add", + path: "members", + value: [ + { + $ref: null, + value: userToAdd.id, + }, + ], + }, + ], + } + + const response = await patchScimGroup({ id: group.id, body }) + + const expectedScimGroup: ScimGroupResponse = { + ...group, + members: [ + { + value: userToAdd.id, + }, + ], + } + expect(response).toEqual(expectedScimGroup) + + const persistedGroup = await config.api.scimGroupsAPI.find(group.id) + expect(persistedGroup).toEqual(expectedScimGroup) + }) + }) }) }) From cd202839b726fdb3e876c26c7c8d2aba1160014a Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 17 Mar 2023 18:47:34 +0100 Subject: [PATCH 075/135] Add multiple users tests --- .../routes/global/tests/scim/groups.spec.ts | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts index 0f0706632a..7412eed832 100644 --- a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts @@ -312,7 +312,7 @@ describe("/api/global/scim/v2/groups", () => { }) describe("adding users", () => { - it("new users can be added to an existing group", async () => { + it("a new user can be added to an existing group", async () => { const userToAdd = _.sample(users)! const body: ScimUpdateRequest = { @@ -346,6 +346,48 @@ describe("/api/global/scim/v2/groups", () => { const persistedGroup = await config.api.scimGroupsAPI.find(group.id) expect(persistedGroup).toEqual(expectedScimGroup) }) + + it("multiple users can be added to an existing group", async () => { + const [user1ToAdd, user2ToAdd] = _.sampleSize(users, 2) + + const body: ScimUpdateRequest = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [ + { + op: "Add", + path: "members", + value: [ + { + $ref: null, + value: user1ToAdd.id, + }, + { + $ref: null, + value: user2ToAdd.id, + }, + ], + }, + ], + } + + const response = await patchScimGroup({ id: group.id, body }) + + const expectedScimGroup: ScimGroupResponse = { + ...group, + members: expect.arrayContaining([ + { + value: user1ToAdd.id, + }, + { + value: user2ToAdd.id, + }, + ]), + } + expect(response).toEqual(expectedScimGroup) + + const persistedGroup = await config.api.scimGroupsAPI.find(group.id) + expect(persistedGroup).toEqual(expectedScimGroup) + }) }) }) }) From 7bfdd31daacdf90d0639a6fd1fccc2aa5941e5b5 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 20 Mar 2023 12:22:45 +0100 Subject: [PATCH 076/135] Test different patch use cases --- .../types/src/api/web/global/scim/groups.ts | 2 +- .../routes/global/tests/scim/groups.spec.ts | 175 ++++++++++++++++-- 2 files changed, 163 insertions(+), 14 deletions(-) diff --git a/packages/types/src/api/web/global/scim/groups.ts b/packages/types/src/api/web/global/scim/groups.ts index 21836bccd2..6669026852 100644 --- a/packages/types/src/api/web/global/scim/groups.ts +++ b/packages/types/src/api/web/global/scim/groups.ts @@ -21,7 +21,7 @@ export interface ScimCreateGroupRequest { ] externalId: string displayName: string - meta: { + meta: ScimMeta & { resourceType: "Group" } } diff --git a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts index 7412eed832..d44a2a4887 100644 --- a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts @@ -11,8 +11,6 @@ import { TestConfiguration } from "../../../../../tests" mocks.licenses.useScimIntegration() describe("/api/global/scim/v2/groups", () => { - let users: ScimUserResponse[] - beforeEach(() => { tk.freeze(mocks.date.MOCK_DATE) @@ -261,11 +259,8 @@ describe("/api/global/scim/v2/groups", () => { } users = users.sort((a, b) => (a.id > b.id ? 1 : -1)) - }) - beforeEach(async () => { const body = structures.scim.createGroupRequest() - group = await config.api.scimGroupsAPI.post({ body }) }) @@ -301,9 +296,13 @@ describe("/api/global/scim/v2/groups", () => { const response = await patchScimGroup({ id: group.id, body }) - const expectedScimGroup: ScimGroupResponse = { + const expectedScimGroup = { ...group, displayName: newDisplayName, + meta: { + ...group.meta, + lastModified: mockedTime.toISOString(), + }, } expect(response).toEqual(expectedScimGroup) @@ -312,8 +311,13 @@ describe("/api/global/scim/v2/groups", () => { }) describe("adding users", () => { + beforeAll(async () => { + const body = structures.scim.createGroupRequest() + group = await config.api.scimGroupsAPI.post({ body }) + }) + it("a new user can be added to an existing group", async () => { - const userToAdd = _.sample(users)! + const userToAdd = users[0] const body: ScimUpdateRequest = { schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], @@ -348,8 +352,6 @@ describe("/api/global/scim/v2/groups", () => { }) it("multiple users can be added to an existing group", async () => { - const [user1ToAdd, user2ToAdd] = _.sampleSize(users, 2) - const body: ScimUpdateRequest = { schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], Operations: [ @@ -359,11 +361,61 @@ describe("/api/global/scim/v2/groups", () => { value: [ { $ref: null, - value: user1ToAdd.id, + value: users[1].id, }, { $ref: null, - value: user2ToAdd.id, + value: users[2].id, + }, + { + $ref: null, + value: users[3].id, + }, + ], + }, + ], + } + + const response = await patchScimGroup({ id: group.id, body }) + + const expectedScimGroup: ScimGroupResponse = { + ...group, + members: [ + { + value: users[0].id, + }, + { + value: users[1].id, + }, + { + value: users[2].id, + }, + { + value: users[3].id, + }, + ], + } + expect(response).toEqual(expectedScimGroup) + + const persistedGroup = await config.api.scimGroupsAPI.find(group.id) + expect(persistedGroup).toEqual(expectedScimGroup) + }) + + it("existing users can be removed from to an existing group", async () => { + const body: ScimUpdateRequest = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [ + { + op: "Remove", + path: "members", + value: [ + { + $ref: null, + value: users[0].id, + }, + { + $ref: null, + value: users[2].id, }, ], }, @@ -376,10 +428,107 @@ describe("/api/global/scim/v2/groups", () => { ...group, members: expect.arrayContaining([ { - value: user1ToAdd.id, + value: users[1].id, }, { - value: user2ToAdd.id, + value: users[3].id, + }, + ]), + } + expect(response).toEqual(expectedScimGroup) + + const persistedGroup = await config.api.scimGroupsAPI.find(group.id) + expect(persistedGroup).toEqual(expectedScimGroup) + }) + + it("adding and removing can be added in a single operation", async () => { + const body: ScimUpdateRequest = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [ + { + op: "Remove", + path: "members", + value: [ + { + $ref: null, + value: users[1].id, + }, + ], + }, + { + op: "Add", + path: "members", + value: [ + { + $ref: null, + value: users[4].id, + }, + ], + }, + ], + } + + const response = await patchScimGroup({ id: group.id, body }) + + const expectedScimGroup: ScimGroupResponse = { + ...group, + members: expect.arrayContaining([ + { + value: users[3].id, + }, + { + value: users[4].id, + }, + ]), + } + expect(response).toEqual(expectedScimGroup) + + const persistedGroup = await config.api.scimGroupsAPI.find(group.id) + expect(persistedGroup).toEqual(expectedScimGroup) + }) + + it("adding members and updating fields can performed in a single operation", async () => { + const newDisplayName = structures.generator.word() + + const body: ScimUpdateRequest = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [ + { + op: "Replace", + path: "displayName", + value: newDisplayName, + }, + { + op: "Add", + path: "members", + value: [ + { + $ref: null, + value: users[5].id, + }, + ], + }, + ], + } + + const response = await patchScimGroup({ id: group.id, body }) + + const expectedScimGroup: ScimGroupResponse = { + ...group, + displayName: newDisplayName, + meta: { + ...group.meta, + lastModified: mockedTime.toISOString() as any, + }, + members: expect.arrayContaining([ + { + value: users[3].id, + }, + { + value: users[4].id, + }, + { + value: users[5].id, }, ]), } From 25276bafb28b6296a5e7fc4f61d3d0b6ab2c70d4 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 20 Mar 2023 12:26:16 +0100 Subject: [PATCH 077/135] Dry tests --- .../src/api/routes/global/tests/scim/groups.spec.ts | 8 -------- .../worker/src/api/routes/global/tests/scim/users.spec.ts | 8 ++------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts index d44a2a4887..e288d5edd1 100644 --- a/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/groups.spec.ts @@ -299,10 +299,6 @@ describe("/api/global/scim/v2/groups", () => { const expectedScimGroup = { ...group, displayName: newDisplayName, - meta: { - ...group.meta, - lastModified: mockedTime.toISOString(), - }, } expect(response).toEqual(expectedScimGroup) @@ -516,10 +512,6 @@ describe("/api/global/scim/v2/groups", () => { const expectedScimGroup: ScimGroupResponse = { ...group, displayName: newDisplayName, - meta: { - ...group.meta, - lastModified: mockedTime.toISOString() as any, - }, members: expect.arrayContaining([ { value: users[3].id, diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index a26be36e62..5499c6c068 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -1,17 +1,13 @@ import tk from "timekeeper" import _ from "lodash" import { mocks, structures } from "@budibase/backend-core/tests" -import { - ScimCreateUserRequest, - ScimUpdateRequest, - ScimUserResponse, -} from "@budibase/types" +import { ScimUpdateRequest, ScimUserResponse } from "@budibase/types" import { TestConfiguration } from "../../../../../tests" mocks.licenses.useScimIntegration() describe("/api/global/scim/v2/users", () => { - let mockedTime = new Date(structures.generator.timestamp()) + let mockedTime: Date beforeEach(() => { tk.reset() From 2476b641638971fa107c07f640528b4e50d8cc16 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 20 Mar 2023 16:37:58 +0000 Subject: [PATCH 078/135] Updates --- .../src/api/routes/global/tests/scim/users.spec.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 5499c6c068..5682f08771 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -7,13 +7,7 @@ import { TestConfiguration } from "../../../../../tests" mocks.licenses.useScimIntegration() describe("/api/global/scim/v2/users", () => { - let mockedTime: Date - beforeEach(() => { - tk.reset() - mockedTime = new Date(structures.generator.timestamp()) - tk.freeze(mockedTime) - mocks.licenses.useScimIntegration() }) @@ -194,6 +188,9 @@ describe("/api/global/scim/v2/users", () => { describe("no users exist", () => { it("a new user can be created and persisted", async () => { + const mockedTime = new Date(structures.generator.timestamp()) + tk.freeze(mockedTime) + const userData = { externalId: structures.uuid(), email: structures.generator.email(), From 344a34ac7cbec5a610c0fe420b4ab98d701b8ebf Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 27 Mar 2023 09:58:57 +0100 Subject: [PATCH 079/135] Fix build errors --- packages/backend-core/tests/utilities/structures/scim.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend-core/tests/utilities/structures/scim.ts b/packages/backend-core/tests/utilities/structures/scim.ts index 2c9ad6c622..6657bb90b5 100644 --- a/packages/backend-core/tests/utilities/structures/scim.ts +++ b/packages/backend-core/tests/utilities/structures/scim.ts @@ -61,6 +61,8 @@ export function createGroupRequest(groupData?: { displayName: displayName, meta: { resourceType: "Group", + created: new Date(), + lastModified: new Date(), }, } return group From e6ff0a44fb54e7b42673729e2d5ec0e9fe38fd59 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 24 Mar 2023 14:20:04 +0000 Subject: [PATCH 080/135] Dispatch event on scim user created --- .../backend-core/src/events/publishers/index.ts | 1 + .../backend-core/src/events/publishers/scim.ts | 16 ++++++++++++++++ packages/types/src/sdk/events/event.ts | 6 ++++++ packages/types/src/sdk/events/index.ts | 1 + packages/types/src/sdk/events/scim.ts | 5 +++++ 5 files changed, 29 insertions(+) create mode 100644 packages/backend-core/src/events/publishers/scim.ts create mode 100644 packages/types/src/sdk/events/scim.ts diff --git a/packages/backend-core/src/events/publishers/index.ts b/packages/backend-core/src/events/publishers/index.ts index 87a34bf3f1..055d798979 100644 --- a/packages/backend-core/src/events/publishers/index.ts +++ b/packages/backend-core/src/events/publishers/index.ts @@ -22,3 +22,4 @@ export { default as plugin } from "./plugin" export { default as backup } from "./backup" export { default as environmentVariable } from "./environmentVariable" export { default as auditLog } from "./auditLog" +export { default as scim } from "./scim" diff --git a/packages/backend-core/src/events/publishers/scim.ts b/packages/backend-core/src/events/publishers/scim.ts new file mode 100644 index 0000000000..992fd4a3e0 --- /dev/null +++ b/packages/backend-core/src/events/publishers/scim.ts @@ -0,0 +1,16 @@ +import { publishEvent } from "../events" +import { Event, ScimUserCreatedEvent } from "@budibase/types" + +async function SCIMUserCreated(props: { + email: string + timestamp?: string | number +}) { + const properties: ScimUserCreatedEvent = { + email: props.email, + } + await publishEvent(Event.SCIM_USER_CREATED, properties, props.timestamp) +} + +export default { + SCIMUserCreated, +} diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts index 3d0d3122b5..0267847ac1 100644 --- a/packages/types/src/sdk/events/event.ts +++ b/packages/types/src/sdk/events/event.ts @@ -184,6 +184,9 @@ export enum Event { // AUDIT LOG AUDIT_LOGS_FILTERED = "audit_log:filtered", AUDIT_LOGS_DOWNLOADED = "audit_log:downloaded", + + // SCIM + SCIM_USER_CREATED = "scim:user:created", } // all events that are not audited have been added to this record as undefined, this means @@ -364,6 +367,9 @@ export const AuditedEventFriendlyName: Record = { // AUDIT LOG - NOT AUDITED [Event.AUDIT_LOGS_FILTERED]: undefined, [Event.AUDIT_LOGS_DOWNLOADED]: undefined, + + // SCIM + [Event.SCIM_USER_CREATED]: `SCIM user "{{ email }}" created`, } // properties added at the final stage of the event pipeline diff --git a/packages/types/src/sdk/events/index.ts b/packages/types/src/sdk/events/index.ts index 745f84d2a3..5088dd21cf 100644 --- a/packages/types/src/sdk/events/index.ts +++ b/packages/types/src/sdk/events/index.ts @@ -23,3 +23,4 @@ export * from "./plugin" export * from "./backup" export * from "./environmentVariable" export * from "./auditLog" +export * from "./scim" diff --git a/packages/types/src/sdk/events/scim.ts b/packages/types/src/sdk/events/scim.ts new file mode 100644 index 0000000000..00e38e34cd --- /dev/null +++ b/packages/types/src/sdk/events/scim.ts @@ -0,0 +1,5 @@ +import { BaseEvent } from "./event" + +export interface ScimUserCreatedEvent extends BaseEvent { + email: string +} From 7840470d83cf14518b3dabc96e9bab839cddd8c2 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 24 Mar 2023 14:25:35 +0000 Subject: [PATCH 081/135] Add create test --- .../backend-core/tests/utilities/mocks/events.ts | 2 ++ .../src/api/routes/global/tests/scim/users.spec.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/backend-core/tests/utilities/mocks/events.ts b/packages/backend-core/tests/utilities/mocks/events.ts index ab0aaa93a6..dc901f3a2c 100644 --- a/packages/backend-core/tests/utilities/mocks/events.ts +++ b/packages/backend-core/tests/utilities/mocks/events.ts @@ -120,3 +120,5 @@ jest.spyOn(events.view, "calculationDeleted") jest.spyOn(events.plugin, "init") jest.spyOn(events.plugin, "imported") jest.spyOn(events.plugin, "deleted") + +jest.spyOn(events.scim, "SCIMUserCreated") diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 5682f08771..733cd90ce0 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -1,5 +1,6 @@ import tk from "timekeeper" import _ from "lodash" +import { events } from "@budibase/backend-core" import { mocks, structures } from "@budibase/backend-core/tests" import { ScimUpdateRequest, ScimUserResponse } from "@budibase/types" import { TestConfiguration } from "../../../../../tests" @@ -236,6 +237,19 @@ describe("/api/global/scim/v2/users", () => { }) ) }) + + it("an event is dispatched", async () => { + const email = structures.email() + const body = createScimCreateUserRequest({ email }) + + await postScimUser({ body }) + + expect(events.scim.SCIMUserCreated).toBeCalledTimes(1) + expect(events.scim.SCIMUserCreated).toBeCalledWith({ + email, + timestamp: mockedTime.toISOString(), + }) + }) }) }) From cbadf69a2963c18bfb439680ea7d28cc79472366 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 24 Mar 2023 14:26:38 +0000 Subject: [PATCH 082/135] Dispatch event on user update --- .../src/events/publishers/scim.ts | 17 ++++++++++++++- .../tests/utilities/mocks/events.ts | 1 + packages/types/src/sdk/events/event.ts | 2 ++ packages/types/src/sdk/events/scim.ts | 4 ++++ .../routes/global/tests/scim/users.spec.ts | 21 +++++++++++++++++++ 5 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/backend-core/src/events/publishers/scim.ts b/packages/backend-core/src/events/publishers/scim.ts index 992fd4a3e0..e35cc52730 100644 --- a/packages/backend-core/src/events/publishers/scim.ts +++ b/packages/backend-core/src/events/publishers/scim.ts @@ -1,5 +1,9 @@ import { publishEvent } from "../events" -import { Event, ScimUserCreatedEvent } from "@budibase/types" +import { + Event, + ScimUserCreatedEvent, + ScimUserUpdatedEvent, +} from "@budibase/types" async function SCIMUserCreated(props: { email: string @@ -11,6 +15,17 @@ async function SCIMUserCreated(props: { await publishEvent(Event.SCIM_USER_CREATED, properties, props.timestamp) } +async function SCIMUserUpdated(props: { + email: string + timestamp?: string | number +}) { + const properties: ScimUserUpdatedEvent = { + email: props.email, + } + await publishEvent(Event.SCIM_USER_UPDATED, properties, props.timestamp) +} + export default { SCIMUserCreated, + SCIMUserUpdated, } diff --git a/packages/backend-core/tests/utilities/mocks/events.ts b/packages/backend-core/tests/utilities/mocks/events.ts index dc901f3a2c..5626fd9a01 100644 --- a/packages/backend-core/tests/utilities/mocks/events.ts +++ b/packages/backend-core/tests/utilities/mocks/events.ts @@ -122,3 +122,4 @@ jest.spyOn(events.plugin, "imported") jest.spyOn(events.plugin, "deleted") jest.spyOn(events.scim, "SCIMUserCreated") +jest.spyOn(events.scim, "SCIMUserUpdated") diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts index 0267847ac1..79361d2b18 100644 --- a/packages/types/src/sdk/events/event.ts +++ b/packages/types/src/sdk/events/event.ts @@ -187,6 +187,7 @@ export enum Event { // SCIM SCIM_USER_CREATED = "scim:user:created", + SCIM_USER_UPDATED = "scim:user:updated", } // all events that are not audited have been added to this record as undefined, this means @@ -370,6 +371,7 @@ export const AuditedEventFriendlyName: Record = { // SCIM [Event.SCIM_USER_CREATED]: `SCIM user "{{ email }}" created`, + [Event.SCIM_USER_UPDATED]: `SCIM user "{{ email }}" updated`, } // properties added at the final stage of the event pipeline diff --git a/packages/types/src/sdk/events/scim.ts b/packages/types/src/sdk/events/scim.ts index 00e38e34cd..e8e34f1e04 100644 --- a/packages/types/src/sdk/events/scim.ts +++ b/packages/types/src/sdk/events/scim.ts @@ -3,3 +3,7 @@ import { BaseEvent } from "./event" export interface ScimUserCreatedEvent extends BaseEvent { email: string } + +export interface ScimUserUpdatedEvent extends BaseEvent { + email: string +} diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 733cd90ce0..4437ee77c6 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -437,6 +437,27 @@ describe("/api/global/scim/v2/users", () => { const persistedUser = await config.api.scimUsersAPI.find(user.id) expect(persistedUser).toEqual(expectedScimUser) }) + + it.only("an event is dispatched", async () => { + const body: ScimUpdateRequest = { + schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations: [ + { + op: "Replace", + path: "userName", + value: structures.generator.name(), + }, + ], + } + + await patchScimUser({ id: user.id, body }) + + expect(events.scim.SCIMUserUpdated).toBeCalledTimes(1) + expect(events.scim.SCIMUserUpdated).toBeCalledWith({ + email: user.emails[0].value, + timestamp: mockedTime.toISOString(), + }) + }) }) describe("DELETE /api/global/scim/v2/users/:id", () => { From 26f077cc1cd872f59e8731c29206a4f07cb56db2 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 24 Mar 2023 14:31:12 +0000 Subject: [PATCH 083/135] Event on delete --- packages/backend-core/src/events/publishers/scim.ts | 9 +++++++++ packages/backend-core/tests/utilities/mocks/events.ts | 1 + packages/types/src/sdk/events/event.ts | 2 ++ packages/types/src/sdk/events/scim.ts | 3 +++ .../src/api/routes/global/tests/scim/users.spec.ts | 11 ++++++++++- 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/backend-core/src/events/publishers/scim.ts b/packages/backend-core/src/events/publishers/scim.ts index e35cc52730..6339f81953 100644 --- a/packages/backend-core/src/events/publishers/scim.ts +++ b/packages/backend-core/src/events/publishers/scim.ts @@ -2,6 +2,7 @@ import { publishEvent } from "../events" import { Event, ScimUserCreatedEvent, + ScimUserDeletedEvent, ScimUserUpdatedEvent, } from "@budibase/types" @@ -25,7 +26,15 @@ async function SCIMUserUpdated(props: { await publishEvent(Event.SCIM_USER_UPDATED, properties, props.timestamp) } +async function SCIMUserDeleted(props: { userId: string }) { + const properties: ScimUserDeletedEvent = { + userId: props.userId, + } + await publishEvent(Event.SCIM_USER_DELETED, properties) +} + export default { SCIMUserCreated, SCIMUserUpdated, + SCIMUserDeleted, } diff --git a/packages/backend-core/tests/utilities/mocks/events.ts b/packages/backend-core/tests/utilities/mocks/events.ts index 5626fd9a01..28e37a8d62 100644 --- a/packages/backend-core/tests/utilities/mocks/events.ts +++ b/packages/backend-core/tests/utilities/mocks/events.ts @@ -123,3 +123,4 @@ jest.spyOn(events.plugin, "deleted") jest.spyOn(events.scim, "SCIMUserCreated") jest.spyOn(events.scim, "SCIMUserUpdated") +jest.spyOn(events.scim, "SCIMUserDeleted") diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts index 79361d2b18..7f0c171af5 100644 --- a/packages/types/src/sdk/events/event.ts +++ b/packages/types/src/sdk/events/event.ts @@ -188,6 +188,7 @@ export enum Event { // SCIM SCIM_USER_CREATED = "scim:user:created", SCIM_USER_UPDATED = "scim:user:updated", + SCIM_USER_DELETED = "scim:user:deleted", } // all events that are not audited have been added to this record as undefined, this means @@ -372,6 +373,7 @@ export const AuditedEventFriendlyName: Record = { // SCIM [Event.SCIM_USER_CREATED]: `SCIM user "{{ email }}" created`, [Event.SCIM_USER_UPDATED]: `SCIM user "{{ email }}" updated`, + [Event.SCIM_USER_DELETED]: `SCIM user "{{ email }}" deleted`, } // properties added at the final stage of the event pipeline diff --git a/packages/types/src/sdk/events/scim.ts b/packages/types/src/sdk/events/scim.ts index e8e34f1e04..2bc348558b 100644 --- a/packages/types/src/sdk/events/scim.ts +++ b/packages/types/src/sdk/events/scim.ts @@ -7,3 +7,6 @@ export interface ScimUserCreatedEvent extends BaseEvent { export interface ScimUserUpdatedEvent extends BaseEvent { email: string } +export interface ScimUserDeletedEvent extends BaseEvent { + userId: string +} diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 4437ee77c6..32b20c7c5e 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -438,7 +438,7 @@ describe("/api/global/scim/v2/users", () => { expect(persistedUser).toEqual(expectedScimUser) }) - it.only("an event is dispatched", async () => { + it("an event is dispatched", async () => { const body: ScimUpdateRequest = { schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], Operations: [ @@ -498,5 +498,14 @@ describe("/api/global/scim/v2/users", () => { it("an non existing user can not be deleted", async () => { await deleteScimUser(structures.uuid(), { expect: 404 }) }) + + it("an event is dispatched", async () => { + await deleteScimUser(user.id, { expect: 204 }) + + expect(events.scim.SCIMUserDeleted).toBeCalledTimes(1) + expect(events.scim.SCIMUserDeleted).toBeCalledWith({ + userId: user.id, + }) + }) }) }) From 924c103cccc977da1a6c10ce59c198fac0f35c4e Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 24 Mar 2023 14:34:40 +0000 Subject: [PATCH 084/135] Use ids instead of email on the events --- packages/backend-core/src/events/publishers/scim.ts | 8 ++++---- packages/types/src/sdk/events/scim.ts | 4 ++-- .../src/api/routes/global/tests/scim/users.spec.ts | 9 ++++----- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/backend-core/src/events/publishers/scim.ts b/packages/backend-core/src/events/publishers/scim.ts index 6339f81953..f38fce1088 100644 --- a/packages/backend-core/src/events/publishers/scim.ts +++ b/packages/backend-core/src/events/publishers/scim.ts @@ -7,21 +7,21 @@ import { } from "@budibase/types" async function SCIMUserCreated(props: { - email: string + userId: string timestamp?: string | number }) { const properties: ScimUserCreatedEvent = { - email: props.email, + userId: props.userId, } await publishEvent(Event.SCIM_USER_CREATED, properties, props.timestamp) } async function SCIMUserUpdated(props: { - email: string + userId: string timestamp?: string | number }) { const properties: ScimUserUpdatedEvent = { - email: props.email, + userId: props.userId, } await publishEvent(Event.SCIM_USER_UPDATED, properties, props.timestamp) } diff --git a/packages/types/src/sdk/events/scim.ts b/packages/types/src/sdk/events/scim.ts index 2bc348558b..19bee3b27f 100644 --- a/packages/types/src/sdk/events/scim.ts +++ b/packages/types/src/sdk/events/scim.ts @@ -1,11 +1,11 @@ import { BaseEvent } from "./event" export interface ScimUserCreatedEvent extends BaseEvent { - email: string + userId: string } export interface ScimUserUpdatedEvent extends BaseEvent { - email: string + userId: string } export interface ScimUserDeletedEvent extends BaseEvent { userId: string diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 32b20c7c5e..b86d5175ff 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -239,14 +239,13 @@ describe("/api/global/scim/v2/users", () => { }) it("an event is dispatched", async () => { - const email = structures.email() - const body = createScimCreateUserRequest({ email }) + const body = createScimCreateUserRequest() - await postScimUser({ body }) + const res = await postScimUser({ body }) expect(events.scim.SCIMUserCreated).toBeCalledTimes(1) expect(events.scim.SCIMUserCreated).toBeCalledWith({ - email, + userId: res.id, timestamp: mockedTime.toISOString(), }) }) @@ -454,7 +453,7 @@ describe("/api/global/scim/v2/users", () => { expect(events.scim.SCIMUserUpdated).toBeCalledTimes(1) expect(events.scim.SCIMUserUpdated).toBeCalledWith({ - email: user.emails[0].value, + userId: user.id, timestamp: mockedTime.toISOString(), }) }) From 31eaa36883c51de5600896baea02a0986b46533f Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 24 Mar 2023 15:38:07 +0000 Subject: [PATCH 085/135] Unify create events --- .../backend-core/src/events/publishers/scim.ts | 12 ------------ .../backend-core/src/events/publishers/user.ts | 1 + .../backend-core/tests/utilities/mocks/events.ts | 1 - packages/types/src/sdk/events/event.ts | 4 +--- packages/types/src/sdk/events/scim.ts | 4 ---- packages/types/src/sdk/events/user.ts | 1 + .../api/routes/global/tests/scim/users.spec.ts | 15 ++++++++++----- 7 files changed, 13 insertions(+), 25 deletions(-) diff --git a/packages/backend-core/src/events/publishers/scim.ts b/packages/backend-core/src/events/publishers/scim.ts index f38fce1088..ff163fe2cc 100644 --- a/packages/backend-core/src/events/publishers/scim.ts +++ b/packages/backend-core/src/events/publishers/scim.ts @@ -1,21 +1,10 @@ import { publishEvent } from "../events" import { Event, - ScimUserCreatedEvent, ScimUserDeletedEvent, ScimUserUpdatedEvent, } from "@budibase/types" -async function SCIMUserCreated(props: { - userId: string - timestamp?: string | number -}) { - const properties: ScimUserCreatedEvent = { - userId: props.userId, - } - await publishEvent(Event.SCIM_USER_CREATED, properties, props.timestamp) -} - async function SCIMUserUpdated(props: { userId: string timestamp?: string | number @@ -34,7 +23,6 @@ async function SCIMUserDeleted(props: { userId: string }) { } export default { - SCIMUserCreated, SCIMUserUpdated, SCIMUserDeleted, } diff --git a/packages/backend-core/src/events/publishers/user.ts b/packages/backend-core/src/events/publishers/user.ts index 8dbc494d1e..81fbb02a12 100644 --- a/packages/backend-core/src/events/publishers/user.ts +++ b/packages/backend-core/src/events/publishers/user.ts @@ -21,6 +21,7 @@ async function created(user: User, timestamp?: number) { userId: user._id as string, audited: { email: user.email, + scim: !!user.scimInfo?.isSync, }, } await publishEvent(Event.USER_CREATED, properties, timestamp) diff --git a/packages/backend-core/tests/utilities/mocks/events.ts b/packages/backend-core/tests/utilities/mocks/events.ts index 28e37a8d62..5f597bf46d 100644 --- a/packages/backend-core/tests/utilities/mocks/events.ts +++ b/packages/backend-core/tests/utilities/mocks/events.ts @@ -121,6 +121,5 @@ jest.spyOn(events.plugin, "init") jest.spyOn(events.plugin, "imported") jest.spyOn(events.plugin, "deleted") -jest.spyOn(events.scim, "SCIMUserCreated") jest.spyOn(events.scim, "SCIMUserUpdated") jest.spyOn(events.scim, "SCIMUserDeleted") diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts index 7f0c171af5..7b02eef6a5 100644 --- a/packages/types/src/sdk/events/event.ts +++ b/packages/types/src/sdk/events/event.ts @@ -186,7 +186,6 @@ export enum Event { AUDIT_LOGS_DOWNLOADED = "audit_log:downloaded", // SCIM - SCIM_USER_CREATED = "scim:user:created", SCIM_USER_UPDATED = "scim:user:updated", SCIM_USER_DELETED = "scim:user:deleted", } @@ -199,7 +198,7 @@ export enum Event { // a user facing event or not. export const AuditedEventFriendlyName: Record = { // USER - [Event.USER_CREATED]: `User "{{ email }}" created`, + [Event.USER_CREATED]: `User "{{ email }}" created{{#if scim}} via SCIM{{/if}}`, [Event.USER_UPDATED]: `User "{{ email }}" updated`, [Event.USER_DELETED]: `User "{{ email }}" deleted`, [Event.USER_PERMISSION_ADMIN_ASSIGNED]: `User "{{ email }}" admin role assigned`, @@ -371,7 +370,6 @@ export const AuditedEventFriendlyName: Record = { [Event.AUDIT_LOGS_DOWNLOADED]: undefined, // SCIM - [Event.SCIM_USER_CREATED]: `SCIM user "{{ email }}" created`, [Event.SCIM_USER_UPDATED]: `SCIM user "{{ email }}" updated`, [Event.SCIM_USER_DELETED]: `SCIM user "{{ email }}" deleted`, } diff --git a/packages/types/src/sdk/events/scim.ts b/packages/types/src/sdk/events/scim.ts index 19bee3b27f..ee603a0504 100644 --- a/packages/types/src/sdk/events/scim.ts +++ b/packages/types/src/sdk/events/scim.ts @@ -1,9 +1,5 @@ import { BaseEvent } from "./event" -export interface ScimUserCreatedEvent extends BaseEvent { - userId: string -} - export interface ScimUserUpdatedEvent extends BaseEvent { userId: string } diff --git a/packages/types/src/sdk/events/user.ts b/packages/types/src/sdk/events/user.ts index ab4b4d9724..2014a526a3 100644 --- a/packages/types/src/sdk/events/user.ts +++ b/packages/types/src/sdk/events/user.ts @@ -4,6 +4,7 @@ export interface UserCreatedEvent extends BaseEvent { userId: string audited: { email: string + scim: boolean } } diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index b86d5175ff..336e52e380 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -243,11 +243,16 @@ describe("/api/global/scim/v2/users", () => { const res = await postScimUser({ body }) - expect(events.scim.SCIMUserCreated).toBeCalledTimes(1) - expect(events.scim.SCIMUserCreated).toBeCalledWith({ - userId: res.id, - timestamp: mockedTime.toISOString(), - }) + expect(events.user.created).toBeCalledTimes(1) + expect(events.user.created).toBeCalledWith( + expect.objectContaining({ + _id: res.id, + createdAt: mockedTime.toISOString(), + scimInfo: expect.objectContaining({ + isSync: true, + }), + }) + ) }) }) }) From 6df08799bb35c402815aa072812721e1c1a7cb90 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 24 Mar 2023 16:46:03 +0000 Subject: [PATCH 086/135] Add isScim to user created event --- packages/backend-core/src/context/identity.ts | 8 +++++++- packages/backend-core/src/events/publishers/user.ts | 5 +++-- packages/backend-core/src/middleware/authenticated.ts | 5 ++++- packages/backend-core/src/utils/endpointUtils.ts | 6 ++++++ packages/backend-core/src/utils/index.ts | 1 + packages/types/src/sdk/context.ts | 1 + packages/types/src/sdk/events/event.ts | 2 +- packages/types/src/sdk/events/user.ts | 2 +- 8 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 packages/backend-core/src/utils/endpointUtils.ts diff --git a/packages/backend-core/src/context/identity.ts b/packages/backend-core/src/context/identity.ts index 84de3b68c9..76540c262e 100644 --- a/packages/backend-core/src/context/identity.ts +++ b/packages/backend-core/src/context/identity.ts @@ -19,7 +19,12 @@ export function doInIdentityContext(identity: IdentityContext, task: any) { } // used in server/worker -export function doInUserContext(user: User, ctx: Ctx, task: any) { +export function doInUserContext( + user: User, + ctx: Ctx, + task: any, + isScim: boolean +) { const userContext: UserContext = { ...user, _id: user._id as string, @@ -29,6 +34,7 @@ export function doInUserContext(user: User, ctx: Ctx, task: any) { // filled in by koa-useragent package userAgent: ctx.userAgent._agent.source, }, + isScimCall: isScim, } return doInIdentityContext(userContext, task) } diff --git a/packages/backend-core/src/events/publishers/user.ts b/packages/backend-core/src/events/publishers/user.ts index 81fbb02a12..47c2b1fd26 100644 --- a/packages/backend-core/src/events/publishers/user.ts +++ b/packages/backend-core/src/events/publishers/user.ts @@ -15,13 +15,14 @@ import { UserUpdatedEvent, UserOnboardingEvent, } from "@budibase/types" +import { context } from "../.." async function created(user: User, timestamp?: number) { const properties: UserCreatedEvent = { userId: user._id as string, audited: { email: user.email, - scim: !!user.scimInfo?.isSync, + viaScim: !!(context.getIdentity() as any)?.isScimCall, }, } await publishEvent(Event.USER_CREATED, properties, timestamp) @@ -31,7 +32,7 @@ async function updated(user: User) { const properties: UserUpdatedEvent = { userId: user._id as string, audited: { - email: user.email, + email: user.email }, } await publishEvent(Event.USER_UPDATED, properties) diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index f877985ee0..be854aded5 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -4,6 +4,7 @@ import { clearCookie, openJwt, isValidInternalAPIKey, + isScimEndpoint, } from "../utils" import { getUser } from "../cache/user" import { getSession, updateSessionTTL } from "../security/sessions" @@ -105,6 +106,8 @@ export default function ( apiKey = ctx.request.headers[Header.AUTHORIZATION].split(" ")[1] } + const isScimCall = isScimEndpoint(ctx) + const tenantId = ctx.request.headers[Header.TENANT_ID] let authenticated = false, user = null, @@ -168,7 +171,7 @@ export default function ( finalise(ctx, { authenticated, user, internal, version, publicEndpoint }) if (user && user.email) { - return identity.doInUserContext(user, ctx, next) + return identity.doInUserContext(user, ctx, next, isScimCall) } else { return next() } diff --git a/packages/backend-core/src/utils/endpointUtils.ts b/packages/backend-core/src/utils/endpointUtils.ts new file mode 100644 index 0000000000..b92ef846d8 --- /dev/null +++ b/packages/backend-core/src/utils/endpointUtils.ts @@ -0,0 +1,6 @@ +import { Ctx } from "@budibase/types" + +const SCIM_ENDPOINTS = new RegExp(["scim/"].join("|")) +export function isScimEndpoint(ctx: Ctx): boolean { + return SCIM_ENDPOINTS.test(ctx.request.url) +} diff --git a/packages/backend-core/src/utils/index.ts b/packages/backend-core/src/utils/index.ts index 8e663bce52..5495f2c403 100644 --- a/packages/backend-core/src/utils/index.ts +++ b/packages/backend-core/src/utils/index.ts @@ -1,2 +1,3 @@ export * from "./hashing" export * from "./utils" +export * from "./endpointUtils" diff --git a/packages/types/src/sdk/context.ts b/packages/types/src/sdk/context.ts index c8345de196..1db6d8e24e 100644 --- a/packages/types/src/sdk/context.ts +++ b/packages/types/src/sdk/context.ts @@ -17,6 +17,7 @@ export interface UserContext extends BaseContext, User { tenantId: string account?: Account hostInfo: HostInfo + isScimCall?: boolean } export type IdentityContext = BaseContext | AccountUserContext | UserContext diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts index 7b02eef6a5..d36dbb8d5a 100644 --- a/packages/types/src/sdk/events/event.ts +++ b/packages/types/src/sdk/events/event.ts @@ -198,7 +198,7 @@ export enum Event { // a user facing event or not. export const AuditedEventFriendlyName: Record = { // USER - [Event.USER_CREATED]: `User "{{ email }}" created{{#if scim}} via SCIM{{/if}}`, + [Event.USER_CREATED]: `User "{{ email }}" created{{#if viaScim}} via SCIM{{/if}}`, [Event.USER_UPDATED]: `User "{{ email }}" updated`, [Event.USER_DELETED]: `User "{{ email }}" deleted`, [Event.USER_PERMISSION_ADMIN_ASSIGNED]: `User "{{ email }}" admin role assigned`, diff --git a/packages/types/src/sdk/events/user.ts b/packages/types/src/sdk/events/user.ts index 2014a526a3..1eb66bb0e1 100644 --- a/packages/types/src/sdk/events/user.ts +++ b/packages/types/src/sdk/events/user.ts @@ -4,7 +4,7 @@ export interface UserCreatedEvent extends BaseEvent { userId: string audited: { email: string - scim: boolean + viaScim: boolean } } From 648247b10e0bf01376dd40e65e9c542448d3e531 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 24 Mar 2023 16:54:13 +0000 Subject: [PATCH 087/135] Add scim info in update/delete user events --- packages/backend-core/src/events/publishers/user.ts | 8 ++++++-- packages/types/src/sdk/events/event.ts | 4 ++-- packages/types/src/sdk/events/user.ts | 8 +++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/backend-core/src/events/publishers/user.ts b/packages/backend-core/src/events/publishers/user.ts index 47c2b1fd26..f9110f2765 100644 --- a/packages/backend-core/src/events/publishers/user.ts +++ b/packages/backend-core/src/events/publishers/user.ts @@ -17,12 +17,14 @@ import { } from "@budibase/types" import { context } from "../.." +const isScim = () => (context.getIdentity() as any)?.isScimCall + async function created(user: User, timestamp?: number) { const properties: UserCreatedEvent = { userId: user._id as string, audited: { email: user.email, - viaScim: !!(context.getIdentity() as any)?.isScimCall, + viaScim: isScim(), }, } await publishEvent(Event.USER_CREATED, properties, timestamp) @@ -32,7 +34,8 @@ async function updated(user: User) { const properties: UserUpdatedEvent = { userId: user._id as string, audited: { - email: user.email + email: user.email, + viaScim: isScim(), }, } await publishEvent(Event.USER_UPDATED, properties) @@ -43,6 +46,7 @@ async function deleted(user: User) { userId: user._id as string, audited: { email: user.email, + viaScim: isScim(), }, } await publishEvent(Event.USER_DELETED, properties) diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts index d36dbb8d5a..ebe3d68deb 100644 --- a/packages/types/src/sdk/events/event.ts +++ b/packages/types/src/sdk/events/event.ts @@ -199,8 +199,8 @@ export enum Event { export const AuditedEventFriendlyName: Record = { // USER [Event.USER_CREATED]: `User "{{ email }}" created{{#if viaScim}} via SCIM{{/if}}`, - [Event.USER_UPDATED]: `User "{{ email }}" updated`, - [Event.USER_DELETED]: `User "{{ email }}" deleted`, + [Event.USER_UPDATED]: `User "{{ email }}" updated{{#if viaScim}} via SCIM{{/if}}`, + [Event.USER_DELETED]: `User "{{ email }}" deleted{{#if viaScim}} via SCIM{{/if}}`, [Event.USER_PERMISSION_ADMIN_ASSIGNED]: `User "{{ email }}" admin role assigned`, [Event.USER_PERMISSION_ADMIN_REMOVED]: `User "{{ email }}" admin role removed`, [Event.USER_PERMISSION_BUILDER_ASSIGNED]: `User "{{ email }}" builder role assigned`, diff --git a/packages/types/src/sdk/events/user.ts b/packages/types/src/sdk/events/user.ts index 1eb66bb0e1..6d74f25aa8 100644 --- a/packages/types/src/sdk/events/user.ts +++ b/packages/types/src/sdk/events/user.ts @@ -4,21 +4,23 @@ export interface UserCreatedEvent extends BaseEvent { userId: string audited: { email: string - viaScim: boolean + viaScim?: boolean } } export interface UserUpdatedEvent extends BaseEvent { userId: string audited: { - email: string + email: string, + viaScim?: boolean } } export interface UserDeletedEvent extends BaseEvent { userId: string audited: { - email: string + email: string, + viaScim?: boolean } } From 62cd6a43f203d7f783898975adfbe0c92db91566 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 24 Mar 2023 16:57:26 +0000 Subject: [PATCH 088/135] Remove "duplicated" events --- .../src/events/publishers/index.ts | 1 - .../src/events/publishers/scim.ts | 28 ------------------- .../tests/utilities/mocks/events.ts | 3 -- packages/types/src/sdk/events/event.ts | 8 ------ .../routes/global/tests/scim/users.spec.ts | 22 ++------------- 5 files changed, 3 insertions(+), 59 deletions(-) delete mode 100644 packages/backend-core/src/events/publishers/scim.ts diff --git a/packages/backend-core/src/events/publishers/index.ts b/packages/backend-core/src/events/publishers/index.ts index 055d798979..87a34bf3f1 100644 --- a/packages/backend-core/src/events/publishers/index.ts +++ b/packages/backend-core/src/events/publishers/index.ts @@ -22,4 +22,3 @@ export { default as plugin } from "./plugin" export { default as backup } from "./backup" export { default as environmentVariable } from "./environmentVariable" export { default as auditLog } from "./auditLog" -export { default as scim } from "./scim" diff --git a/packages/backend-core/src/events/publishers/scim.ts b/packages/backend-core/src/events/publishers/scim.ts deleted file mode 100644 index ff163fe2cc..0000000000 --- a/packages/backend-core/src/events/publishers/scim.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { publishEvent } from "../events" -import { - Event, - ScimUserDeletedEvent, - ScimUserUpdatedEvent, -} from "@budibase/types" - -async function SCIMUserUpdated(props: { - userId: string - timestamp?: string | number -}) { - const properties: ScimUserUpdatedEvent = { - userId: props.userId, - } - await publishEvent(Event.SCIM_USER_UPDATED, properties, props.timestamp) -} - -async function SCIMUserDeleted(props: { userId: string }) { - const properties: ScimUserDeletedEvent = { - userId: props.userId, - } - await publishEvent(Event.SCIM_USER_DELETED, properties) -} - -export default { - SCIMUserUpdated, - SCIMUserDeleted, -} diff --git a/packages/backend-core/tests/utilities/mocks/events.ts b/packages/backend-core/tests/utilities/mocks/events.ts index 5f597bf46d..ab0aaa93a6 100644 --- a/packages/backend-core/tests/utilities/mocks/events.ts +++ b/packages/backend-core/tests/utilities/mocks/events.ts @@ -120,6 +120,3 @@ jest.spyOn(events.view, "calculationDeleted") jest.spyOn(events.plugin, "init") jest.spyOn(events.plugin, "imported") jest.spyOn(events.plugin, "deleted") - -jest.spyOn(events.scim, "SCIMUserUpdated") -jest.spyOn(events.scim, "SCIMUserDeleted") diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts index ebe3d68deb..92268da763 100644 --- a/packages/types/src/sdk/events/event.ts +++ b/packages/types/src/sdk/events/event.ts @@ -184,10 +184,6 @@ export enum Event { // AUDIT LOG AUDIT_LOGS_FILTERED = "audit_log:filtered", AUDIT_LOGS_DOWNLOADED = "audit_log:downloaded", - - // SCIM - SCIM_USER_UPDATED = "scim:user:updated", - SCIM_USER_DELETED = "scim:user:deleted", } // all events that are not audited have been added to this record as undefined, this means @@ -368,10 +364,6 @@ export const AuditedEventFriendlyName: Record = { // AUDIT LOG - NOT AUDITED [Event.AUDIT_LOGS_FILTERED]: undefined, [Event.AUDIT_LOGS_DOWNLOADED]: undefined, - - // SCIM - [Event.SCIM_USER_UPDATED]: `SCIM user "{{ email }}" updated`, - [Event.SCIM_USER_DELETED]: `SCIM user "{{ email }}" deleted`, } // properties added at the final stage of the event pipeline diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 336e52e380..730f6b12f6 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -241,18 +241,9 @@ describe("/api/global/scim/v2/users", () => { it("an event is dispatched", async () => { const body = createScimCreateUserRequest() - const res = await postScimUser({ body }) + await postScimUser({ body }) expect(events.user.created).toBeCalledTimes(1) - expect(events.user.created).toBeCalledWith( - expect.objectContaining({ - _id: res.id, - createdAt: mockedTime.toISOString(), - scimInfo: expect.objectContaining({ - isSync: true, - }), - }) - ) }) }) }) @@ -456,11 +447,7 @@ describe("/api/global/scim/v2/users", () => { await patchScimUser({ id: user.id, body }) - expect(events.scim.SCIMUserUpdated).toBeCalledTimes(1) - expect(events.scim.SCIMUserUpdated).toBeCalledWith({ - userId: user.id, - timestamp: mockedTime.toISOString(), - }) + expect(events.user.updated).toBeCalledTimes(1) }) }) @@ -506,10 +493,7 @@ describe("/api/global/scim/v2/users", () => { it("an event is dispatched", async () => { await deleteScimUser(user.id, { expect: 204 }) - expect(events.scim.SCIMUserDeleted).toBeCalledTimes(1) - expect(events.scim.SCIMUserDeleted).toBeCalledWith({ - userId: user.id, - }) + expect(events.user.deleted).toBeCalledTimes(1) }) }) }) From a5f6fddbdb265536e73477c45c89a3aad5af0d92 Mon Sep 17 00:00:00 2001 From: adrinr Date: Fri, 24 Mar 2023 17:00:56 +0000 Subject: [PATCH 089/135] Fix merge conflicts --- packages/worker/src/api/routes/global/tests/scim/users.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index 730f6b12f6..fae041cc0a 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -239,7 +239,7 @@ describe("/api/global/scim/v2/users", () => { }) it("an event is dispatched", async () => { - const body = createScimCreateUserRequest() + const body = structures.scim.createUserRequest() await postScimUser({ body }) From 04bd9dda9c4a8fe6128c99aed25386cc5295d52f Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 27 Mar 2023 11:10:07 +0100 Subject: [PATCH 090/135] Use new scim context functions --- packages/backend-core/src/context/mainContext.ts | 13 +++++++++++++ packages/backend-core/src/context/types.ts | 1 + packages/backend-core/src/events/publishers/user.ts | 8 +++----- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index 02ba16aa8c..8ea6324817 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -214,6 +214,13 @@ export function doInEnvironmentContext( return newContext(updates, task) } +export function doInScimContext(task: any) { + const updates: ContextMap = { + scimCall: true, + } + return newContext(updates, task) +} + export function getEnvironmentVariables() { const context = Context.get() if (!context.environmentVariables) { @@ -270,3 +277,9 @@ export function getDevAppDB(opts?: any): Database { } return getDB(conversions.getDevelopmentAppID(appId), opts) } + +export function isScimCall(): boolean { + const context = Context.get() + const scimCall = context?.scimCall + return !!scimCall +} diff --git a/packages/backend-core/src/context/types.ts b/packages/backend-core/src/context/types.ts index 78197ed528..0d8958dbd3 100644 --- a/packages/backend-core/src/context/types.ts +++ b/packages/backend-core/src/context/types.ts @@ -6,4 +6,5 @@ export type ContextMap = { appId?: string identity?: IdentityContext environmentVariables?: Record + scimCall?: boolean } diff --git a/packages/backend-core/src/events/publishers/user.ts b/packages/backend-core/src/events/publishers/user.ts index f9110f2765..cf5f041c1d 100644 --- a/packages/backend-core/src/events/publishers/user.ts +++ b/packages/backend-core/src/events/publishers/user.ts @@ -17,14 +17,12 @@ import { } from "@budibase/types" import { context } from "../.." -const isScim = () => (context.getIdentity() as any)?.isScimCall - async function created(user: User, timestamp?: number) { const properties: UserCreatedEvent = { userId: user._id as string, audited: { email: user.email, - viaScim: isScim(), + viaScim: context.isScimCall(), }, } await publishEvent(Event.USER_CREATED, properties, timestamp) @@ -35,7 +33,7 @@ async function updated(user: User) { userId: user._id as string, audited: { email: user.email, - viaScim: isScim(), + viaScim: context.isScimCall(), }, } await publishEvent(Event.USER_UPDATED, properties) @@ -46,7 +44,7 @@ async function deleted(user: User) { userId: user._id as string, audited: { email: user.email, - viaScim: isScim(), + viaScim: context.isScimCall(), }, } await publishEvent(Event.USER_DELETED, properties) From 099cc145bf52c480a5958db7afaa482f6b00b6c3 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 27 Mar 2023 11:10:30 +0100 Subject: [PATCH 091/135] Clean code --- packages/backend-core/src/context/identity.ts | 8 +------- packages/backend-core/src/middleware/authenticated.ts | 5 +---- packages/backend-core/src/utils/endpointUtils.ts | 6 ------ packages/backend-core/src/utils/index.ts | 1 - packages/types/src/sdk/context.ts | 1 - 5 files changed, 2 insertions(+), 19 deletions(-) delete mode 100644 packages/backend-core/src/utils/endpointUtils.ts diff --git a/packages/backend-core/src/context/identity.ts b/packages/backend-core/src/context/identity.ts index 76540c262e..84de3b68c9 100644 --- a/packages/backend-core/src/context/identity.ts +++ b/packages/backend-core/src/context/identity.ts @@ -19,12 +19,7 @@ export function doInIdentityContext(identity: IdentityContext, task: any) { } // used in server/worker -export function doInUserContext( - user: User, - ctx: Ctx, - task: any, - isScim: boolean -) { +export function doInUserContext(user: User, ctx: Ctx, task: any) { const userContext: UserContext = { ...user, _id: user._id as string, @@ -34,7 +29,6 @@ export function doInUserContext( // filled in by koa-useragent package userAgent: ctx.userAgent._agent.source, }, - isScimCall: isScim, } return doInIdentityContext(userContext, task) } diff --git a/packages/backend-core/src/middleware/authenticated.ts b/packages/backend-core/src/middleware/authenticated.ts index be854aded5..f877985ee0 100644 --- a/packages/backend-core/src/middleware/authenticated.ts +++ b/packages/backend-core/src/middleware/authenticated.ts @@ -4,7 +4,6 @@ import { clearCookie, openJwt, isValidInternalAPIKey, - isScimEndpoint, } from "../utils" import { getUser } from "../cache/user" import { getSession, updateSessionTTL } from "../security/sessions" @@ -106,8 +105,6 @@ export default function ( apiKey = ctx.request.headers[Header.AUTHORIZATION].split(" ")[1] } - const isScimCall = isScimEndpoint(ctx) - const tenantId = ctx.request.headers[Header.TENANT_ID] let authenticated = false, user = null, @@ -171,7 +168,7 @@ export default function ( finalise(ctx, { authenticated, user, internal, version, publicEndpoint }) if (user && user.email) { - return identity.doInUserContext(user, ctx, next, isScimCall) + return identity.doInUserContext(user, ctx, next) } else { return next() } diff --git a/packages/backend-core/src/utils/endpointUtils.ts b/packages/backend-core/src/utils/endpointUtils.ts deleted file mode 100644 index b92ef846d8..0000000000 --- a/packages/backend-core/src/utils/endpointUtils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Ctx } from "@budibase/types" - -const SCIM_ENDPOINTS = new RegExp(["scim/"].join("|")) -export function isScimEndpoint(ctx: Ctx): boolean { - return SCIM_ENDPOINTS.test(ctx.request.url) -} diff --git a/packages/backend-core/src/utils/index.ts b/packages/backend-core/src/utils/index.ts index 5495f2c403..8e663bce52 100644 --- a/packages/backend-core/src/utils/index.ts +++ b/packages/backend-core/src/utils/index.ts @@ -1,3 +1,2 @@ export * from "./hashing" export * from "./utils" -export * from "./endpointUtils" diff --git a/packages/types/src/sdk/context.ts b/packages/types/src/sdk/context.ts index 1db6d8e24e..c8345de196 100644 --- a/packages/types/src/sdk/context.ts +++ b/packages/types/src/sdk/context.ts @@ -17,7 +17,6 @@ export interface UserContext extends BaseContext, User { tenantId: string account?: Account hostInfo: HostInfo - isScimCall?: boolean } export type IdentityContext = BaseContext | AccountUserContext | UserContext From 74573a16250e20755151f930c5447f9c757bd716 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 27 Mar 2023 11:33:49 +0100 Subject: [PATCH 092/135] Rename routes --- packages/worker/src/api/routes/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/worker/src/api/routes/index.ts b/packages/worker/src/api/routes/index.ts index 6f0bbc7cca..4131f14c74 100644 --- a/packages/worker/src/api/routes/index.ts +++ b/packages/worker/src/api/routes/index.ts @@ -36,5 +36,5 @@ export const routes: Router[] = [ accountRoutes, restoreRoutes, eventRoutes, - ...pro.scimRoutes, + pro.scim, ] From d1c224ed4200d848f676ee3876b8a5409bed5624 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 27 Mar 2023 11:35:40 +0100 Subject: [PATCH 093/135] Fix tests --- packages/worker/src/api/routes/global/tests/scim/users.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts index fae041cc0a..589a052540 100644 --- a/packages/worker/src/api/routes/global/tests/scim/users.spec.ts +++ b/packages/worker/src/api/routes/global/tests/scim/users.spec.ts @@ -9,6 +9,7 @@ mocks.licenses.useScimIntegration() describe("/api/global/scim/v2/users", () => { beforeEach(() => { + jest.resetAllMocks() mocks.licenses.useScimIntegration() }) From 7821c637c655d04fd28fd4f89a442046c83b50dc Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 27 Mar 2023 12:28:40 +0100 Subject: [PATCH 094/135] Move event one level up --- packages/backend-core/src/events/publishers/user.ts | 6 +++--- packages/types/src/sdk/events/user.ts | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/backend-core/src/events/publishers/user.ts b/packages/backend-core/src/events/publishers/user.ts index cf5f041c1d..a81bb64096 100644 --- a/packages/backend-core/src/events/publishers/user.ts +++ b/packages/backend-core/src/events/publishers/user.ts @@ -20,9 +20,9 @@ import { context } from "../.." async function created(user: User, timestamp?: number) { const properties: UserCreatedEvent = { userId: user._id as string, + viaScim: context.isScimCall(), audited: { email: user.email, - viaScim: context.isScimCall(), }, } await publishEvent(Event.USER_CREATED, properties, timestamp) @@ -31,9 +31,9 @@ async function created(user: User, timestamp?: number) { async function updated(user: User) { const properties: UserUpdatedEvent = { userId: user._id as string, + viaScim: context.isScimCall(), audited: { email: user.email, - viaScim: context.isScimCall(), }, } await publishEvent(Event.USER_UPDATED, properties) @@ -42,9 +42,9 @@ async function updated(user: User) { async function deleted(user: User) { const properties: UserDeletedEvent = { userId: user._id as string, + viaScim: context.isScimCall(), audited: { email: user.email, - viaScim: context.isScimCall(), }, } await publishEvent(Event.USER_DELETED, properties) diff --git a/packages/types/src/sdk/events/user.ts b/packages/types/src/sdk/events/user.ts index 6d74f25aa8..955f198732 100644 --- a/packages/types/src/sdk/events/user.ts +++ b/packages/types/src/sdk/events/user.ts @@ -2,25 +2,25 @@ import { BaseEvent } from "./event" export interface UserCreatedEvent extends BaseEvent { userId: string + viaScim?: boolean audited: { email: string - viaScim?: boolean } } export interface UserUpdatedEvent extends BaseEvent { userId: string + viaScim?: boolean audited: { - email: string, - viaScim?: boolean + email: string } } export interface UserDeletedEvent extends BaseEvent { userId: string + viaScim?: boolean audited: { - email: string, - viaScim?: boolean + email: string } } From 8a6400c7d0e2a6de1b4cabc0328dd38cc01b5d24 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 27 Mar 2023 12:35:27 +0100 Subject: [PATCH 095/135] Add viaScim to group events --- packages/backend-core/src/events/publishers/group.ts | 7 ++++++- packages/types/src/sdk/events/userGroup.ts | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/backend-core/src/events/publishers/group.ts b/packages/backend-core/src/events/publishers/group.ts index a000b880a2..2748ec6a09 100644 --- a/packages/backend-core/src/events/publishers/group.ts +++ b/packages/backend-core/src/events/publishers/group.ts @@ -9,12 +9,13 @@ import { GroupUsersDeletedEvent, GroupAddedOnboardingEvent, GroupPermissionsEditedEvent, - UserGroupRoles, } from "@budibase/types" +import { context } from "../.." async function created(group: UserGroup, timestamp?: number) { const properties: GroupCreatedEvent = { groupId: group._id as string, + viaScim: context.isScimCall(), audited: { name: group.name, }, @@ -25,6 +26,7 @@ async function created(group: UserGroup, timestamp?: number) { async function updated(group: UserGroup) { const properties: GroupUpdatedEvent = { groupId: group._id as string, + viaScim: context.isScimCall(), audited: { name: group.name, }, @@ -35,6 +37,7 @@ async function updated(group: UserGroup) { async function deleted(group: UserGroup) { const properties: GroupDeletedEvent = { groupId: group._id as string, + viaScim: context.isScimCall(), audited: { name: group.name, }, @@ -46,6 +49,7 @@ async function usersAdded(count: number, group: UserGroup) { const properties: GroupUsersAddedEvent = { count, groupId: group._id as string, + viaScim: context.isScimCall(), audited: { name: group.name, }, @@ -57,6 +61,7 @@ async function usersDeleted(count: number, group: UserGroup) { const properties: GroupUsersDeletedEvent = { count, groupId: group._id as string, + viaScim: context.isScimCall(), audited: { name: group.name, }, diff --git a/packages/types/src/sdk/events/userGroup.ts b/packages/types/src/sdk/events/userGroup.ts index d82ab70b4c..ea1f554cff 100644 --- a/packages/types/src/sdk/events/userGroup.ts +++ b/packages/types/src/sdk/events/userGroup.ts @@ -2,6 +2,7 @@ import { BaseEvent } from "./event" export interface GroupCreatedEvent extends BaseEvent { groupId: string + viaScim?: boolean audited: { name: string } @@ -9,6 +10,7 @@ export interface GroupCreatedEvent extends BaseEvent { export interface GroupUpdatedEvent extends BaseEvent { groupId: string + viaScim?: boolean audited: { name: string } @@ -16,6 +18,7 @@ export interface GroupUpdatedEvent extends BaseEvent { export interface GroupDeletedEvent extends BaseEvent { groupId: string + viaScim?: boolean audited: { name: string } @@ -24,6 +27,7 @@ export interface GroupDeletedEvent extends BaseEvent { export interface GroupUsersAddedEvent extends BaseEvent { count: number groupId: string + viaScim?: boolean audited: { name: string } @@ -32,6 +36,7 @@ export interface GroupUsersAddedEvent extends BaseEvent { export interface GroupUsersDeletedEvent extends BaseEvent { count: number groupId: string + viaScim?: boolean audited: { name: string } From c48952d05699e06dd6ebda17b649856beb101cd1 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 27 Mar 2023 12:42:53 +0100 Subject: [PATCH 096/135] Add group event scim messages --- packages/types/src/sdk/events/event.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/types/src/sdk/events/event.ts b/packages/types/src/sdk/events/event.ts index 92268da763..0d59576435 100644 --- a/packages/types/src/sdk/events/event.ts +++ b/packages/types/src/sdk/events/event.ts @@ -206,11 +206,11 @@ export const AuditedEventFriendlyName: Record = { [Event.USER_PASSWORD_UPDATED]: `User "{{ email }}" password updated`, [Event.USER_PASSWORD_RESET_REQUESTED]: `User "{{ email }}" password reset requested`, [Event.USER_PASSWORD_RESET]: `User "{{ email }}" password reset`, - [Event.USER_GROUP_CREATED]: `User group "{{ name }}" created`, - [Event.USER_GROUP_UPDATED]: `User group "{{ name }}" updated`, - [Event.USER_GROUP_DELETED]: `User group "{{ name }}" deleted`, - [Event.USER_GROUP_USERS_ADDED]: `User group "{{ name }}" {{ count }} users added`, - [Event.USER_GROUP_USERS_REMOVED]: `User group "{{ name }}" {{ count }} users removed`, + [Event.USER_GROUP_CREATED]: `User group "{{ name }}" created{{#if viaScim}} via SCIM{{/if}}`, + [Event.USER_GROUP_UPDATED]: `User group "{{ name }}" updated{{#if viaScim}} via SCIM{{/if}}`, + [Event.USER_GROUP_DELETED]: `User group "{{ name }}" deleted{{#if viaScim}} via SCIM{{/if}}`, + [Event.USER_GROUP_USERS_ADDED]: `User group "{{ name }}" {{ count }} users added{{#if viaScim}} via SCIM{{/if}}`, + [Event.USER_GROUP_USERS_REMOVED]: `User group "{{ name }}" {{ count }} users removed{{#if viaScim}} via SCIM{{/if}}`, [Event.USER_GROUP_PERMISSIONS_EDITED]: `User group "{{ name }}" permissions edited`, [Event.USER_PASSWORD_FORCE_RESET]: undefined, [Event.USER_GROUP_ONBOARDING]: undefined, From 54265816cca51fcf7c183acd276b69c1b9f4c44d Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 27 Mar 2023 15:26:07 +0100 Subject: [PATCH 097/135] Renames --- packages/backend-core/src/context/mainContext.ts | 6 +++--- packages/backend-core/src/context/types.ts | 2 +- packages/backend-core/src/events/publishers/group.ts | 10 +++++----- packages/backend-core/src/events/publishers/user.ts | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/backend-core/src/context/mainContext.ts b/packages/backend-core/src/context/mainContext.ts index 8ea6324817..e1bd535b78 100644 --- a/packages/backend-core/src/context/mainContext.ts +++ b/packages/backend-core/src/context/mainContext.ts @@ -216,7 +216,7 @@ export function doInEnvironmentContext( export function doInScimContext(task: any) { const updates: ContextMap = { - scimCall: true, + isScim: true, } return newContext(updates, task) } @@ -278,8 +278,8 @@ export function getDevAppDB(opts?: any): Database { return getDB(conversions.getDevelopmentAppID(appId), opts) } -export function isScimCall(): boolean { +export function isScim(): boolean { const context = Context.get() - const scimCall = context?.scimCall + const scimCall = context?.isScim return !!scimCall } diff --git a/packages/backend-core/src/context/types.ts b/packages/backend-core/src/context/types.ts index 0d8958dbd3..727dad80bc 100644 --- a/packages/backend-core/src/context/types.ts +++ b/packages/backend-core/src/context/types.ts @@ -6,5 +6,5 @@ export type ContextMap = { appId?: string identity?: IdentityContext environmentVariables?: Record - scimCall?: boolean + isScim?: boolean } diff --git a/packages/backend-core/src/events/publishers/group.ts b/packages/backend-core/src/events/publishers/group.ts index 2748ec6a09..93bb6117ed 100644 --- a/packages/backend-core/src/events/publishers/group.ts +++ b/packages/backend-core/src/events/publishers/group.ts @@ -15,7 +15,7 @@ import { context } from "../.." async function created(group: UserGroup, timestamp?: number) { const properties: GroupCreatedEvent = { groupId: group._id as string, - viaScim: context.isScimCall(), + viaScim: context.isScim(), audited: { name: group.name, }, @@ -26,7 +26,7 @@ async function created(group: UserGroup, timestamp?: number) { async function updated(group: UserGroup) { const properties: GroupUpdatedEvent = { groupId: group._id as string, - viaScim: context.isScimCall(), + viaScim: context.isScim(), audited: { name: group.name, }, @@ -37,7 +37,7 @@ async function updated(group: UserGroup) { async function deleted(group: UserGroup) { const properties: GroupDeletedEvent = { groupId: group._id as string, - viaScim: context.isScimCall(), + viaScim: context.isScim(), audited: { name: group.name, }, @@ -49,7 +49,7 @@ async function usersAdded(count: number, group: UserGroup) { const properties: GroupUsersAddedEvent = { count, groupId: group._id as string, - viaScim: context.isScimCall(), + viaScim: context.isScim(), audited: { name: group.name, }, @@ -61,7 +61,7 @@ async function usersDeleted(count: number, group: UserGroup) { const properties: GroupUsersDeletedEvent = { count, groupId: group._id as string, - viaScim: context.isScimCall(), + viaScim: context.isScim(), audited: { name: group.name, }, diff --git a/packages/backend-core/src/events/publishers/user.ts b/packages/backend-core/src/events/publishers/user.ts index a81bb64096..18e4d64ada 100644 --- a/packages/backend-core/src/events/publishers/user.ts +++ b/packages/backend-core/src/events/publishers/user.ts @@ -20,7 +20,7 @@ import { context } from "../.." async function created(user: User, timestamp?: number) { const properties: UserCreatedEvent = { userId: user._id as string, - viaScim: context.isScimCall(), + viaScim: context.isScim(), audited: { email: user.email, }, @@ -31,7 +31,7 @@ async function created(user: User, timestamp?: number) { async function updated(user: User) { const properties: UserUpdatedEvent = { userId: user._id as string, - viaScim: context.isScimCall(), + viaScim: context.isScim(), audited: { email: user.email, }, @@ -42,7 +42,7 @@ async function updated(user: User) { async function deleted(user: User) { const properties: UserDeletedEvent = { userId: user._id as string, - viaScim: context.isScimCall(), + viaScim: context.isScim(), audited: { email: user.email, }, From d9ff01b5f06b2f832db0fdd3877e0fae95be34aa Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 27 Mar 2023 15:28:03 +0100 Subject: [PATCH 098/135] Clean code --- packages/types/src/sdk/events/scim.ts | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 packages/types/src/sdk/events/scim.ts diff --git a/packages/types/src/sdk/events/scim.ts b/packages/types/src/sdk/events/scim.ts deleted file mode 100644 index ee603a0504..0000000000 --- a/packages/types/src/sdk/events/scim.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { BaseEvent } from "./event" - -export interface ScimUserUpdatedEvent extends BaseEvent { - userId: string -} -export interface ScimUserDeletedEvent extends BaseEvent { - userId: string -} From 1838f75dbc5babaf0fb2364c84f13bd68dc40d35 Mon Sep 17 00:00:00 2001 From: adrinr Date: Mon, 27 Mar 2023 15:32:36 +0100 Subject: [PATCH 099/135] Add test --- .../src/context/tests/index.spec.ts | 17 +++++++++++++++-- packages/types/src/sdk/events/index.ts | 1 - 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/backend-core/src/context/tests/index.spec.ts b/packages/backend-core/src/context/tests/index.spec.ts index 5c8ce6fc19..3f760d4a49 100644 --- a/packages/backend-core/src/context/tests/index.spec.ts +++ b/packages/backend-core/src/context/tests/index.spec.ts @@ -1,6 +1,6 @@ import { testEnv } from "../../../tests" -const context = require("../") -const { DEFAULT_TENANT_ID } = require("../../constants") +import * as context from "../" +import { DEFAULT_TENANT_ID } from "../../constants" describe("context", () => { describe("doInTenant", () => { @@ -131,4 +131,17 @@ describe("context", () => { }) }) }) + + describe("doInScimContext", () => { + it("returns true when set", () => { + context.doInScimContext(() => { + const isScim = context.isScim() + expect(isScim).toBe(true) + }) + }) + it("returns false when not set", () => { + const isScim = context.isScim() + expect(isScim).toBe(false) + }) + }) }) diff --git a/packages/types/src/sdk/events/index.ts b/packages/types/src/sdk/events/index.ts index 5088dd21cf..745f84d2a3 100644 --- a/packages/types/src/sdk/events/index.ts +++ b/packages/types/src/sdk/events/index.ts @@ -23,4 +23,3 @@ export * from "./plugin" export * from "./backup" export * from "./environmentVariable" export * from "./auditLog" -export * from "./scim" From 0c5d33a6420945058f76f3588c9375cea03f2fc5 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Mar 2023 12:06:16 +0000 Subject: [PATCH 100/135] Show SCIM banner if SCIM enabled --- .../builder/portal/users/users/index.svelte | 29 ++++++++++++++----- .../builder/src/stores/portal/licensing.js | 3 ++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/users/users/index.svelte b/packages/builder/src/pages/builder/portal/users/users/index.svelte index 4f80e1a942..90159a531a 100644 --- a/packages/builder/src/pages/builder/portal/users/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/users/users/index.svelte @@ -11,6 +11,7 @@ notifications, Pagination, Divider, + Icon, } from "@budibase/bbui" import AddUserModal from "./_components/AddUserModal.svelte" import { users, groups, auth, licensing, organisation } from "stores/portal" @@ -230,14 +231,21 @@
- - - - + {#if $licensing.scimEnabled} + + + + + {:else} +
+ + Users are synced from your AD +
+ {/if}
{#if selectedRows.length > 0} @@ -322,4 +330,9 @@ .controls-right :global(.spectrum-Search) { width: 200px; } + + .scim-banner { + display: flex; + gap: var(--spacing-s); + } diff --git a/packages/builder/src/stores/portal/licensing.js b/packages/builder/src/stores/portal/licensing.js index 8640df5ef6..467299b2fd 100644 --- a/packages/builder/src/stores/portal/licensing.js +++ b/packages/builder/src/stores/portal/licensing.js @@ -18,6 +18,7 @@ export const createLicensingStore = () => { groupsEnabled: false, backupsEnabled: false, brandingEnabled: false, + scimEnabled: false, // the currently used quotas from the db quotaUsage: undefined, // derived quota metrics for percentages used @@ -66,6 +67,7 @@ export const createLicensingStore = () => { const backupsEnabled = license.features.includes( Constants.Features.BACKUPS ) + const scimEnabled = license.features.includes(Constants.Features.SCIM) const environmentVariablesEnabled = license.features.includes( Constants.Features.ENVIRONMENT_VARIABLES ) @@ -88,6 +90,7 @@ export const createLicensingStore = () => { groupsEnabled, backupsEnabled, brandingEnabled, + scimEnabled, environmentVariablesEnabled, auditLogsEnabled, enforceableSSO, From 934a2f09d75dcf39840597e452aedcca088daf7e Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Mar 2023 12:13:19 +0000 Subject: [PATCH 101/135] Right checks --- .../builder/src/pages/builder/portal/users/users/index.svelte | 2 +- packages/frontend-core/src/constants.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/pages/builder/portal/users/users/index.svelte b/packages/builder/src/pages/builder/portal/users/users/index.svelte index 90159a531a..b1e4148762 100644 --- a/packages/builder/src/pages/builder/portal/users/users/index.svelte +++ b/packages/builder/src/pages/builder/portal/users/users/index.svelte @@ -231,7 +231,7 @@
- {#if $licensing.scimEnabled} + {#if !$licensing.scimEnabled}
- Details +
+ Details + {#if scimEnabled} +
+ + Users are synced from your AD +
+ {/if} +
@@ -404,4 +414,13 @@ width: 100%; text-align: center; } + .details-title { + display: flex; + justify-content: space-between; + align-items: center; + } + .scim-banner { + display: flex; + gap: var(--spacing-s); + } From 15ed91ef8501e657143ec8754a37ad3ac9694e17 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Mar 2023 12:28:14 +0000 Subject: [PATCH 103/135] Make fields readonly if scim is enabled --- .../src/pages/builder/portal/users/users/[userId].svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte index 952a63d79d..de1757aef4 100644 --- a/packages/builder/src/pages/builder/portal/users/users/[userId].svelte +++ b/packages/builder/src/pages/builder/portal/users/users/[userId].svelte @@ -84,8 +84,7 @@ const scimEnabled = $licensing.scimEnabled $: isSSO = !!user?.provider - $: readonly = !$auth.isAdmin - $: fullName = user?.firstName ? user?.firstName + " " + user?.lastName : "" + $: readonly = !$auth.isAdmin || scimEnabled $: privileged = user?.admin?.global || user?.builder?.global $: nameLabel = getNameLabel(user) $: initials = getInitials(nameLabel) @@ -294,10 +293,11 @@
{#if userId !== $auth.user._id} +
+
+ +
copyToClipboard(setting.value)}> + +
+
+
+ {/each} + {/if}
+ + + From 9094d3c9fd907eb70e22224ccb49234d10eef4a3 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Mar 2023 16:39:50 +0000 Subject: [PATCH 113/135] Display right provisioning url --- .../src/pages/builder/portal/settings/auth/scim.svelte | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/builder/src/pages/builder/portal/settings/auth/scim.svelte b/packages/builder/src/pages/builder/portal/settings/auth/scim.svelte index 3207e2549c..b2eb234a72 100644 --- a/packages/builder/src/pages/builder/portal/settings/auth/scim.svelte +++ b/packages/builder/src/pages/builder/portal/settings/auth/scim.svelte @@ -13,6 +13,7 @@ } from "@budibase/bbui" import { onMount } from "svelte" import { API } from "api" + import { organisation } from "stores/portal" const configType = "scim" @@ -54,7 +55,10 @@ } const settings = [ - { title: "Provisioning URL", value: "url" }, + { + title: "Provisioning URL", + value: `${$organisation.platformUrl}/api/global/scim/v2`, + }, { title: "Provisioning Token", value: "token" }, ] From 2fb6f810945c9b34c353f320763fe738eaff2248 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Mar 2023 16:46:05 +0000 Subject: [PATCH 114/135] Display api key --- .../builder/portal/settings/auth/scim.svelte | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/settings/auth/scim.svelte b/packages/builder/src/pages/builder/portal/settings/auth/scim.svelte index b2eb234a72..f9ebe4b94a 100644 --- a/packages/builder/src/pages/builder/portal/settings/auth/scim.svelte +++ b/packages/builder/src/pages/builder/portal/settings/auth/scim.svelte @@ -13,11 +13,12 @@ } from "@budibase/bbui" import { onMount } from "svelte" import { API } from "api" - import { organisation } from "stores/portal" + import { organisation, auth } from "stores/portal" const configType = "scim" $: scimEnabled = true + $: apiKey = null async function saveConfig(config) { // Delete unsupported fields @@ -39,7 +40,7 @@ } } - onMount(async () => { + async function fetchConfig() { try { const scimConfig = await API.getConfig(configType) scimEnabled = scimConfig?.enabled @@ -47,6 +48,18 @@ console.error(error) notifications.error("Error fetching SCIM config") } + } + + async function fetchAPIKey() { + try { + apiKey = await auth.fetchAPIKey() + } catch (err) { + notifications.error("Unable to fetch API key") + } + } + + onMount(async () => { + await Promise.all(fetchConfig(), fetchAPIKey()) }) const copyToClipboard = async value => { @@ -54,12 +67,12 @@ notifications.success("Copied") } - const settings = [ + $: settings = [ { title: "Provisioning URL", value: `${$organisation.platformUrl}/api/global/scim/v2`, }, - { title: "Provisioning Token", value: "token" }, + { title: "Provisioning Token", value: apiKey }, ] From 045af06edbdbb79eb07e10f3c4118a33a6a0c025 Mon Sep 17 00:00:00 2001 From: adrinr Date: Tue, 21 Mar 2023 16:53:09 +0000 Subject: [PATCH 115/135] Use feature flag --- .../src/pages/builder/portal/settings/auth/index.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/pages/builder/portal/settings/auth/index.svelte b/packages/builder/src/pages/builder/portal/settings/auth/index.svelte index 3e984e0473..60f84049a3 100644 --- a/packages/builder/src/pages/builder/portal/settings/auth/index.svelte +++ b/packages/builder/src/pages/builder/portal/settings/auth/index.svelte @@ -614,8 +614,10 @@
{/if} - - + {#if $licensing.scimEnabled} + + + {/if}