Main body of PR comments.

This commit is contained in:
mike12345567 2023-02-24 13:32:45 +00:00
parent f070be5f65
commit 58fab29fb4
13 changed files with 113 additions and 128 deletions

View File

@ -239,7 +239,7 @@ export function getGlobalDB(): Database {
export function getAuditLogsDB(): Database { export function getAuditLogsDB(): Database {
if (!getTenantId()) { if (!getTenantId()) {
throw new Error("Audit log DB not found") throw new Error("No tenant ID found - cannot open audit log DB")
} }
return getDB(getAuditLogDBName()) return getDB(getAuditLogDBName())
} }

View File

@ -366,7 +366,7 @@ export async function getAllApps({
} }
} }
export async function getAppsById(appIds: string[]) { export async function getAppsByIDs(appIds: string[]) {
const settled = await Promise.allSettled( const settled = await Promise.allSettled(
appIds.map(appId => getAppMetadata(appId)) appIds.map(appId => getAppMetadata(appId))
) )

View File

@ -47,6 +47,8 @@ export default class PosthogProcessor implements EventProcessor {
return return
} }
properties = this.clearPIIProperties(properties)
properties.version = pkg.version properties.version = pkg.version
properties.service = env.SERVICE properties.service = env.SERVICE
properties.environment = identity.environment properties.environment = identity.environment
@ -79,6 +81,13 @@ export default class PosthogProcessor implements EventProcessor {
this.posthog.capture(payload) this.posthog.capture(payload)
} }
clearPIIProperties(properties: any) {
if (properties.email) {
delete properties.email
}
return properties
}
async identify(identity: Identity, timestamp?: string | number) { async identify(identity: Identity, timestamp?: string | number) {
const payload: any = { distinctId: identity.id, properties: identity } const payload: any = { distinctId: identity.id, properties: identity }
if (timestamp) { if (timestamp) {

View File

@ -1,26 +1,26 @@
import { import {
Event, Event,
AuditLogSearchParams, AuditLogSearchParams,
AuditLogFilterEvent, AuditLogFilteredEvent,
AuditLogDownloadEvent, AuditLogDownloadedEvent,
} from "@budibase/types" } from "@budibase/types"
import { publishEvent } from "../events" import { publishEvent } from "../events"
async function filtered(search: AuditLogSearchParams) { async function filtered(search: AuditLogSearchParams) {
const properties: AuditLogFilterEvent = { const properties: AuditLogFilteredEvent = {
filters: search, filters: search,
} }
await publishEvent(Event.AUDIT_LOG_FILTER, properties) await publishEvent(Event.AUDIT_LOGS_FILTERED, properties)
} }
async function download(search: AuditLogSearchParams) { async function downloaded(search: AuditLogSearchParams) {
const properties: AuditLogDownloadEvent = { const properties: AuditLogDownloadedEvent = {
filters: search, filters: search,
} }
await publishEvent(Event.AUDIT_LOG_DOWNLOAD, properties) await publishEvent(Event.AUDIT_LOGS_DOWNLOADED, properties)
} }
export default { export default {
filtered, filtered,
download, downloaded,
} }

View File

@ -7,6 +7,12 @@ import { Ctx } from "@budibase/types"
*/ */
export default function (ctx: Ctx, next: any) { export default function (ctx: Ctx, next: any) {
const queryString = ctx.request.query?.query as string | undefined const queryString = ctx.request.query?.query as string | undefined
if (ctx.request.method.toLowerCase() !== "get") {
ctx.throw(
500,
"Query to download middleware can only be used for get requests."
)
}
if (!queryString) { if (!queryString) {
return next() return next()
} }

View File

@ -12,7 +12,7 @@ import * as context from "./context"
type GetOpts = { cleanup?: boolean } type GetOpts = { cleanup?: boolean }
function cleanupUsers(users: User | User[]) { function removeUserPassword(users: User | User[]) {
if (Array.isArray(users)) { if (Array.isArray(users)) {
return users.map(user => { return users.map(user => {
if (user) { if (user) {
@ -39,7 +39,7 @@ export const bulkGetGlobalUsersById = async (
}) })
).rows.map(row => row.doc) as User[] ).rows.map(row => row.doc) as User[]
if (opts?.cleanup) { if (opts?.cleanup) {
users = cleanupUsers(users) as User[] users = removeUserPassword(users) as User[]
} }
return users return users
} }
@ -53,7 +53,7 @@ export async function getById(id: string, opts?: GetOpts): Promise<User> {
const db = context.getGlobalDB() const db = context.getGlobalDB()
let user = await db.get(id) let user = await db.get(id)
if (opts?.cleanup) { if (opts?.cleanup) {
user = cleanupUsers(user) user = removeUserPassword(user)
} }
return user return user
} }
@ -61,7 +61,6 @@ export async function getById(id: string, opts?: GetOpts): Promise<User> {
/** /**
* Given an email address this will use a view to search through * Given an email address this will use a view to search through
* all the users to find one with this email address. * all the users to find one with this email address.
* @param {string} email the email to lookup the user by.
*/ */
export const getGlobalUserByEmail = async ( export const getGlobalUserByEmail = async (
email: String, email: String,
@ -83,7 +82,7 @@ export const getGlobalUserByEmail = async (
let user = response as User let user = response as User
if (opts?.cleanup) { if (opts?.cleanup) {
user = cleanupUsers(user) as User user = removeUserPassword(user) as User
} }
return user return user
@ -107,7 +106,7 @@ export const searchGlobalUsersByApp = async (
} }
let users: User[] = Array.isArray(response) ? response : [response] let users: User[] = Array.isArray(response) ? response : [response]
if (getOpts?.cleanup) { if (getOpts?.cleanup) {
users = cleanupUsers(users) as User[] users = removeUserPassword(users) as User[]
} }
return users return users
} }
@ -143,7 +142,7 @@ export const searchGlobalUsersByEmail = async (
} }
let users: User[] = Array.isArray(response) ? response : [response] let users: User[] = Array.isArray(response) ? response : [response]
if (getOpts?.cleanup) { if (getOpts?.cleanup) {
users = cleanupUsers(users) as User[] users = removeUserPassword(users) as User[]
} }
return users return users
} }

View File

@ -214,36 +214,6 @@ export async function getBuildersCount() {
return builders.length return builders.length
} }
/**
* Logs a user out from budibase. Re-used across account portal and builder.
*/
export async function platformLogout(opts: PlatformLogoutOpts) {
const ctx = opts.ctx
const email = ctx.user?.email!
const userId = opts.userId
const keepActiveSession = opts.keepActiveSession
if (!ctx) throw new Error("Koa context must be supplied to logout.")
const currentSession = getCookie(ctx, Cookie.Auth)
let sessions = await getSessionsForUser(userId)
if (keepActiveSession) {
sessions = sessions.filter(
session => session.sessionId !== currentSession.sessionId
)
} else {
// clear cookies
clearCookie(ctx, Cookie.Auth)
clearCookie(ctx, Cookie.CurrentApp)
}
const sessionIds = sessions.map(({ sessionId }) => sessionId)
await invalidateSessions(userId, { sessionIds, reason: "logout" })
await events.auth.logout(email)
await userCache.invalidateUser(userId)
}
export function timeout(timeMs: number) { export function timeout(timeMs: number) {
return new Promise(resolve => setTimeout(resolve, timeMs)) return new Promise(resolve => setTimeout(resolve, timeMs))
} }

View File

@ -28,17 +28,12 @@ import * as automations from "./automations"
import { Thread } from "./threads" import { Thread } from "./threads"
import * as redis from "./utilities/redis" import * as redis from "./utilities/redis"
import { events, logging, middleware } from "@budibase/backend-core" import { events, logging, middleware } from "@budibase/backend-core"
import { sdk } from "@budibase/pro"
import { initialise as initialiseWebsockets } from "./websocket" import { initialise as initialiseWebsockets } from "./websocket"
import { startup } from "./startup" import { startup } from "./startup"
const Sentry = require("@sentry/node") const Sentry = require("@sentry/node")
const destroyable = require("server-destroy") const destroyable = require("server-destroy")
const { userAgent } = require("koa-useragent") const { userAgent } = require("koa-useragent")
// configure events to use the pro audit log write
// can't integrate directly into backend-core due to cyclic issues
events.configure(sdk.auditLogs.write)
const app = new Koa() const app = new Koa()
let mbNumber = parseInt(env.HTTP_MB_LIMIT || "10") let mbNumber = parseInt(env.HTTP_MB_LIMIT || "10")

View File

@ -5,7 +5,7 @@ import {
generateApiKey, generateApiKey,
getChecklist, getChecklist,
} from "./utilities/workerRequests" } from "./utilities/workerRequests"
import { installation, tenancy, logging } from "@budibase/backend-core" import { installation, tenancy, logging, events } from "@budibase/backend-core"
import fs from "fs" import fs from "fs"
import { watch } from "./watch" import { watch } from "./watch"
import * as automations from "./automations" import * as automations from "./automations"
@ -17,6 +17,7 @@ import * as pro from "@budibase/pro"
import * as api from "./api" import * as api from "./api"
import sdk from "./sdk" import sdk from "./sdk"
const pino = require("koa-pino-logger") const pino = require("koa-pino-logger")
import { sdk as proSdk } from "@budibase/pro"
let STARTUP_RAN = false let STARTUP_RAN = false
@ -124,6 +125,9 @@ export async function startup(app?: any, server?: any) {
// get the references to the queue promises, don't await as // get the references to the queue promises, don't await as
// they will never end, unless the processing stops // they will never end, unless the processing stops
let queuePromises = [] let queuePromises = []
// configure events to use the pro audit log write
// can't integrate directly into backend-core due to cyclic issues
queuePromises.push(events.configure(proSdk.auditLogs.write))
queuePromises.push(automations.init()) queuePromises.push(automations.init())
queuePromises.push(initPro()) queuePromises.push(initPro())
if (app) { if (app) {

View File

@ -11,7 +11,7 @@ export type AuditLogFn = (
event: Event, event: Event,
metadata: any, metadata: any,
opts: AuditWriteOpts opts: AuditWriteOpts
) => Promise<any> ) => Promise<void>
export type AuditLogQueueEvent = { export type AuditLogQueueEvent = {
event: Event event: Event

View File

@ -1,10 +1,10 @@
import { BaseEvent } from "./event" import { BaseEvent } from "./event"
import { AuditLogSearchParams } from "../../api" import { AuditLogSearchParams } from "../../api"
export interface AuditLogFilterEvent extends BaseEvent { export interface AuditLogFilteredEvent extends BaseEvent {
filters: AuditLogSearchParams filters: AuditLogSearchParams
} }
export interface AuditLogDownloadEvent extends BaseEvent { export interface AuditLogDownloadedEvent extends BaseEvent {
filters: AuditLogSearchParams filters: AuditLogSearchParams
} }

View File

@ -182,8 +182,8 @@ export enum Event {
ENVIRONMENT_VARIABLE_UPGRADE_PANEL_OPENED = "environment_variable:upgrade_panel_opened", ENVIRONMENT_VARIABLE_UPGRADE_PANEL_OPENED = "environment_variable:upgrade_panel_opened",
// AUDIT LOG // AUDIT LOG
AUDIT_LOG_FILTER = "audit_log:filter", AUDIT_LOGS_FILTERED = "audit_log:filtered",
AUDIT_LOG_DOWNLOAD = "audit_log:download", AUDIT_LOGS_DOWNLOADED = "audit_log:downloaded",
} }
// all events that are not audited have been added to this record as undefined, this means // all events that are not audited have been added to this record as undefined, this means
@ -362,8 +362,8 @@ export const AuditedEventFriendlyName: Record<Event, string | undefined> = {
[Event.INSTALLATION_FIRST_STARTUP]: undefined, [Event.INSTALLATION_FIRST_STARTUP]: undefined,
// AUDIT LOG - NOT AUDITED // AUDIT LOG - NOT AUDITED
[Event.AUDIT_LOG_FILTER]: undefined, [Event.AUDIT_LOGS_FILTERED]: undefined,
[Event.AUDIT_LOG_DOWNLOAD]: undefined, [Event.AUDIT_LOGS_DOWNLOADED]: undefined,
} }
// properties added at the final stage of the event pipeline // properties added at the final stage of the event pipeline

View File

@ -23,87 +23,89 @@ describe("/api/global/auditlogs", () => {
await config.afterAll() await config.afterAll()
}) })
it("should be able to fire some events (create audit logs)", async () => { describe("POST /api/global/auditlogs/search", () => {
await context.doInTenant(config.tenantId, async () => { it("should be able to fire some events (create audit logs)", async () => {
const userId = config.user!._id! await context.doInTenant(config.tenantId, async () => {
const identity = { const userId = config.user!._id!
...BASE_IDENTITY, const identity = {
_id: userId, ...BASE_IDENTITY,
tenantId: config.tenantId, _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 context.doInIdentityContext(identity, async () => {
await events.app.created(structures.apps.app(APP_ID)) 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)
}) })
// 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 () => { it("should be able to search by event", async () => {
const response = await config.api.auditLogs.search({ const response = await config.api.auditLogs.search({
events: [Event.USER_CREATED], events: [Event.USER_CREATED],
})
expect(response.data.length).toBeGreaterThan(0)
for (let log of response.data) {
expect(log.event).toBe(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 () => { it("should be able to search by time range (frozen)", async () => {
// this is frozen, only need to add 1 and minus 1 // this is frozen, only need to add 1 and minus 1
const now = new Date() const now = new Date()
const start = new Date() const start = new Date()
start.setSeconds(now.getSeconds() - 1) start.setSeconds(now.getSeconds() - 1)
const end = new Date() const end = new Date()
end.setSeconds(now.getSeconds() + 1) end.setSeconds(now.getSeconds() + 1)
const response = await config.api.auditLogs.search({ const response = await config.api.auditLogs.search({
startDate: start.toISOString(), startDate: start.toISOString(),
endDate: end.toISOString(), endDate: end.toISOString(),
})
expect(response.data.length).toBeGreaterThan(0)
for (let log of response.data) {
expect(log.timestamp).toBe(now.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 () => { it("should be able to search by user ID", async () => {
const userId = config.user!._id! const userId = config.user!._id!
const response = await config.api.auditLogs.search({ const response = await config.api.auditLogs.search({
userIds: [userId], userIds: [userId],
})
expect(response.data.length).toBeGreaterThan(0)
for (let log of response.data) {
expect(log.user._id).toBe(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 () => { it("should be able to search by app ID", async () => {
const response = await config.api.auditLogs.search({ const response = await config.api.auditLogs.search({
appIds: [APP_ID], appIds: [APP_ID],
})
expect(response.data.length).toBeGreaterThan(0)
for (let log of response.data) {
expect(log.app?._id).toBe(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 () => { it("should be able to search by full string", async () => {
const response = await config.api.auditLogs.search({ const response = await config.api.auditLogs.search({
fullSearch: "User", fullSearch: "User",
})
expect(response.data.length).toBeGreaterThan(0)
for (let log of response.data) {
expect(log.name.includes("User")).toBe(true)
}
}) })
expect(response.data.length).toBeGreaterThan(0)
for (let log of response.data) {
expect(log.name.includes("User")).toBe(true)
}
}) })
}) })