From 0bc340052c394c0ad20ca557246a61259f79ed46 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 23 Feb 2023 11:28:18 +0000 Subject: [PATCH 1/2] Adding the ability to cleanup users from get functions (default is old behaviour). --- packages/backend-core/src/users.ts | 67 +++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index ef76af390d..4c1c9ad464 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -10,14 +10,35 @@ import { BulkDocsResponse, User } from "@budibase/types" import { getGlobalDB } from "./context" import * as context from "./context" -export const bulkGetGlobalUsersById = async (userIds: string[]) => { +type GetOpts = { cleanup?: boolean } + +function cleanupUsers(users: User | User[]) { + if (Array.isArray(users)) { + return users.map(user => { + delete user.password + return user + }) + } else { + delete users.password + return users + } +} + +export const bulkGetGlobalUsersById = async ( + userIds: string[], + opts?: GetOpts +) => { const db = getGlobalDB() - return ( + let users = ( await db.allDocs({ keys: userIds, include_docs: true, }) ).rows.map(row => row.doc) as User[] + if (opts?.cleanup) { + users = cleanupUsers(users) as User[] + } + return users } export const bulkUpdateGlobalUsers = async (users: User[]) => { @@ -25,9 +46,13 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => { return (await db.bulkDocs(users)) as BulkDocsResponse } -export async function getById(id: string): Promise { +export async function getById(id: string, opts?: GetOpts): Promise { const db = context.getGlobalDB() - return db.get(id) + let user = await db.get(id) + if (opts?.cleanup) { + user = cleanupUsers(user) + } + return user } /** @@ -36,7 +61,8 @@ export async function getById(id: string): Promise { * @param {string} email the email to lookup the user by. */ export const getGlobalUserByEmail = async ( - email: String + email: String, + opts?: GetOpts ): Promise => { if (email == null) { throw "Must supply an email address to view" @@ -52,10 +78,19 @@ export const getGlobalUserByEmail = async ( throw new Error(`Multiple users found with email address: ${email}`) } - return response + let user = response as User + if (opts?.cleanup) { + user = cleanupUsers(user) as User + } + + return user } -export const searchGlobalUsersByApp = async (appId: any, opts: any) => { +export const searchGlobalUsersByApp = async ( + appId: any, + opts: any, + getOpts?: GetOpts +) => { if (typeof appId !== "string") { throw new Error("Must provide a string based app ID") } @@ -67,7 +102,11 @@ export const searchGlobalUsersByApp = async (appId: any, opts: any) => { if (!response) { response = [] } - return Array.isArray(response) ? response : [response] + let users: User[] = Array.isArray(response) ? response : [response] + if (getOpts?.cleanup) { + users = cleanupUsers(users) as User[] + } + return users } export const getGlobalUserByAppPage = (appId: string, user: User) => { @@ -80,7 +119,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => { /** * Performs a starts with search on the global email view. */ -export const searchGlobalUsersByEmail = async (email: string, opts: any) => { +export const searchGlobalUsersByEmail = async ( + email: string, + opts: any, + getOpts?: GetOpts +) => { if (typeof email !== "string") { throw new Error("Must provide a string to search by") } @@ -95,5 +138,9 @@ export const searchGlobalUsersByEmail = async (email: string, opts: any) => { if (!response) { response = [] } - return Array.isArray(response) ? response : [response] + let users: User[] = Array.isArray(response) ? response : [response] + if (getOpts?.cleanup) { + users = cleanupUsers(users) as User[] + } + return users } From 0b48075688fd5bc82f266cdb506d47d9ee976a17 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Thu, 23 Feb 2023 17:23:06 +0000 Subject: [PATCH 2/2] Implementing a few basic tests to create and search the audit logs. --- packages/backend-core/src/users.ts | 9 +- .../tests/utilities/mocks/licenses.ts | 4 + .../api/routes/global/tests/auditLogs.spec.ts | 110 +++++++++++++++++- packages/worker/src/tests/api/auditLogs.ts | 26 +++++ packages/worker/src/tests/api/index.ts | 3 + 5 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 packages/worker/src/tests/api/auditLogs.ts diff --git a/packages/backend-core/src/users.ts b/packages/backend-core/src/users.ts index 4c1c9ad464..5100066ff9 100644 --- a/packages/backend-core/src/users.ts +++ b/packages/backend-core/src/users.ts @@ -15,13 +15,16 @@ type GetOpts = { cleanup?: boolean } function cleanupUsers(users: User | User[]) { if (Array.isArray(users)) { return users.map(user => { - delete user.password - return user + if (user) { + delete user.password + return user + } }) - } else { + } else if (users) { delete users.password return users } + return users } export const bulkGetGlobalUsersById = async ( diff --git a/packages/backend-core/tests/utilities/mocks/licenses.ts b/packages/backend-core/tests/utilities/mocks/licenses.ts index e374612f5f..210c03b900 100644 --- a/packages/backend-core/tests/utilities/mocks/licenses.ts +++ b/packages/backend-core/tests/utilities/mocks/licenses.ts @@ -78,6 +78,10 @@ export const useEnvironmentVariables = () => { return useFeature(Feature.ENVIRONMENT_VARIABLES) } +export const useAuditLogs = () => { + return useFeature(Feature.AUDIT_LOGS) +} + // QUOTAS export const setAutomationLogsQuota = (value: number) => { diff --git a/packages/worker/src/api/routes/global/tests/auditLogs.spec.ts b/packages/worker/src/api/routes/global/tests/auditLogs.spec.ts index 5e7ed31263..536c510129 100644 --- a/packages/worker/src/api/routes/global/tests/auditLogs.spec.ts +++ b/packages/worker/src/api/routes/global/tests/auditLogs.spec.ts @@ -1,5 +1,109 @@ -import { mocks } from "@budibase/backend-core/tests" +import { mocks, structures } from "@budibase/backend-core/tests" +import { context, events } from "@budibase/backend-core" +import { Event, IdentityType } from "@budibase/types" +import { TestConfiguration } from "../../../../tests" -mocks.licenses.useEnvironmentVariables() +mocks.licenses.useAuditLogs() -describe("/api/global/auditlogs", () => {}) +const BASE_IDENTITY = { + account: undefined, + type: IdentityType.USER, +} +const USER_AUDIT_LOG_COUNT = 3 +const APP_ID = "app_1" + +describe("/api/global/auditlogs", () => { + const config = new TestConfiguration() + + beforeAll(async () => { + await config.beforeAll() + }) + + afterAll(async () => { + await config.afterAll() + }) + + it("should be able to fire some events (create audit logs)", async () => { + await context.doInTenant(config.tenantId, async () => { + const userId = config.user!._id! + const identity = { + ...BASE_IDENTITY, + _id: userId, + tenantId: config.tenantId, + } + await context.doInIdentityContext(identity, async () => { + for (let i = 0; i < USER_AUDIT_LOG_COUNT; i++) { + await events.user.created(structures.users.user()) + } + await context.doInAppContext(APP_ID, async () => { + await events.app.created(structures.apps.app(APP_ID)) + }) + // fetch the user created events + const response = await config.api.auditLogs.search({ + events: [Event.USER_CREATED], + }) + expect(response.data).toBeDefined() + // there will be an initial event which comes from the default user creation + expect(response.data.length).toBe(USER_AUDIT_LOG_COUNT + 1) + }) + }) + }) + + it("should be able to search by event", async () => { + const response = await config.api.auditLogs.search({ + events: [Event.USER_CREATED], + }) + expect(response.data.length).toBeGreaterThan(0) + for (let log of response.data) { + expect(log.event).toBe(Event.USER_CREATED) + } + }) + + it("should be able to search by time range (frozen)", async () => { + // this is frozen, only need to add 1 and minus 1 + const now = new Date() + const start = new Date() + start.setSeconds(now.getSeconds() - 1) + const end = new Date() + end.setSeconds(now.getSeconds() + 1) + const response = await config.api.auditLogs.search({ + startDate: start.toISOString(), + endDate: end.toISOString(), + }) + expect(response.data.length).toBeGreaterThan(0) + for (let log of response.data) { + expect(log.timestamp).toBe(now.toISOString()) + } + }) + + it("should be able to search by user ID", async () => { + const userId = config.user!._id! + const response = await config.api.auditLogs.search({ + userIds: [userId], + }) + expect(response.data.length).toBeGreaterThan(0) + for (let log of response.data) { + expect(log.user._id).toBe(userId) + } + }) + + it("should be able to search by app ID", async () => { + const response = await config.api.auditLogs.search({ + appIds: [APP_ID], + }) + expect(response.data.length).toBeGreaterThan(0) + for (let log of response.data) { + expect(log.app?._id).toBe(APP_ID) + } + }) + + it("should be able to search by full string", async () => { + const response = await config.api.auditLogs.search({ + fullSearch: "User", + }) + expect(response.data.length).toBeGreaterThan(0) + for (let log of response.data) { + expect(log.name.includes("User")).toBe(true) + } + }) +}) diff --git a/packages/worker/src/tests/api/auditLogs.ts b/packages/worker/src/tests/api/auditLogs.ts new file mode 100644 index 0000000000..d7bc4d99fb --- /dev/null +++ b/packages/worker/src/tests/api/auditLogs.ts @@ -0,0 +1,26 @@ +import { AuditLogSearchParams, SearchAuditLogsResponse } from "@budibase/types" +import TestConfiguration from "../TestConfiguration" +import { TestAPI } from "./base" + +export class AuditLogAPI extends TestAPI { + constructor(config: TestConfiguration) { + super(config) + } + + search = async (search: AuditLogSearchParams) => { + const res = await this.request + .post("/api/global/auditlogs/search") + .send(search) + .set(this.config.defaultHeaders()) + .expect("Content-Type", /json/) + .expect(200) + return res.body as SearchAuditLogsResponse + } + + download = (search: AuditLogSearchParams) => { + const query = encodeURIComponent(JSON.stringify(search)) + return this.request + .get(`/api/global/auditlogs/download?query=${query}`) + .set(this.config.defaultHeaders()) + } +} diff --git a/packages/worker/src/tests/api/index.ts b/packages/worker/src/tests/api/index.ts index 0bd0308e2f..166996e792 100644 --- a/packages/worker/src/tests/api/index.ts +++ b/packages/worker/src/tests/api/index.ts @@ -14,6 +14,7 @@ import { GroupsAPI } from "./groups" import { RolesAPI } from "./roles" import { TemplatesAPI } from "./templates" import { LicenseAPI } from "./license" +import { AuditLogAPI } from "./auditLogs" export default class API { accounts: AccountAPI auth: AuthAPI @@ -30,6 +31,7 @@ export default class API { roles: RolesAPI templates: TemplatesAPI license: LicenseAPI + auditLogs: AuditLogAPI constructor(config: TestConfiguration) { this.accounts = new AccountAPI(config) @@ -47,5 +49,6 @@ export default class API { this.roles = new RolesAPI(config) this.templates = new TemplatesAPI(config) this.license = new LicenseAPI(config) + this.auditLogs = new AuditLogAPI(config) } }