Merge branch 'feature/audit-logs' of github.com:Budibase/budibase into feature/audit-logs
This commit is contained in:
commit
3c639a8e85
|
@ -10,14 +10,38 @@ import { BulkDocsResponse, User } from "@budibase/types"
|
||||||
import { getGlobalDB } from "./context"
|
import { getGlobalDB } from "./context"
|
||||||
import * as context 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 => {
|
||||||
|
if (user) {
|
||||||
|
delete user.password
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (users) {
|
||||||
|
delete users.password
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bulkGetGlobalUsersById = async (
|
||||||
|
userIds: string[],
|
||||||
|
opts?: GetOpts
|
||||||
|
) => {
|
||||||
const db = getGlobalDB()
|
const db = getGlobalDB()
|
||||||
return (
|
let users = (
|
||||||
await db.allDocs({
|
await db.allDocs({
|
||||||
keys: userIds,
|
keys: userIds,
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
).rows.map(row => row.doc) as User[]
|
).rows.map(row => row.doc) as User[]
|
||||||
|
if (opts?.cleanup) {
|
||||||
|
users = cleanupUsers(users) as User[]
|
||||||
|
}
|
||||||
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bulkUpdateGlobalUsers = async (users: User[]) => {
|
export const bulkUpdateGlobalUsers = async (users: User[]) => {
|
||||||
|
@ -25,9 +49,13 @@ export const bulkUpdateGlobalUsers = async (users: User[]) => {
|
||||||
return (await db.bulkDocs(users)) as BulkDocsResponse
|
return (await db.bulkDocs(users)) as BulkDocsResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getById(id: string): Promise<User> {
|
export async function getById(id: string, opts?: GetOpts): Promise<User> {
|
||||||
const db = context.getGlobalDB()
|
const db = context.getGlobalDB()
|
||||||
return db.get(id)
|
let user = await db.get(id)
|
||||||
|
if (opts?.cleanup) {
|
||||||
|
user = cleanupUsers(user)
|
||||||
|
}
|
||||||
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,7 +64,8 @@ export async function getById(id: string): Promise<User> {
|
||||||
* @param {string} email the email to lookup the user by.
|
* @param {string} email the email to lookup the user by.
|
||||||
*/
|
*/
|
||||||
export const getGlobalUserByEmail = async (
|
export const getGlobalUserByEmail = async (
|
||||||
email: String
|
email: String,
|
||||||
|
opts?: GetOpts
|
||||||
): Promise<User | undefined> => {
|
): Promise<User | undefined> => {
|
||||||
if (email == null) {
|
if (email == null) {
|
||||||
throw "Must supply an email address to view"
|
throw "Must supply an email address to view"
|
||||||
|
@ -52,10 +81,19 @@ export const getGlobalUserByEmail = async (
|
||||||
throw new Error(`Multiple users found with email address: ${email}`)
|
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") {
|
if (typeof appId !== "string") {
|
||||||
throw new Error("Must provide a string based app ID")
|
throw new Error("Must provide a string based app ID")
|
||||||
}
|
}
|
||||||
|
@ -67,7 +105,11 @@ export const searchGlobalUsersByApp = async (appId: any, opts: any) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
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) => {
|
export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
||||||
|
@ -80,7 +122,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => {
|
||||||
/**
|
/**
|
||||||
* Performs a starts with search on the global email view.
|
* 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") {
|
if (typeof email !== "string") {
|
||||||
throw new Error("Must provide a string to search by")
|
throw new Error("Must provide a string to search by")
|
||||||
}
|
}
|
||||||
|
@ -95,5 +141,9 @@ export const searchGlobalUsersByEmail = async (email: string, opts: any) => {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,10 @@ export const useEnvironmentVariables = () => {
|
||||||
return useFeature(Feature.ENVIRONMENT_VARIABLES)
|
return useFeature(Feature.ENVIRONMENT_VARIABLES)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useAuditLogs = () => {
|
||||||
|
return useFeature(Feature.AUDIT_LOGS)
|
||||||
|
}
|
||||||
|
|
||||||
// QUOTAS
|
// QUOTAS
|
||||||
|
|
||||||
export const setAutomationLogsQuota = (value: number) => {
|
export const setAutomationLogsQuota = (value: number) => {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import { GroupsAPI } from "./groups"
|
||||||
import { RolesAPI } from "./roles"
|
import { RolesAPI } from "./roles"
|
||||||
import { TemplatesAPI } from "./templates"
|
import { TemplatesAPI } from "./templates"
|
||||||
import { LicenseAPI } from "./license"
|
import { LicenseAPI } from "./license"
|
||||||
|
import { AuditLogAPI } from "./auditLogs"
|
||||||
export default class API {
|
export default class API {
|
||||||
accounts: AccountAPI
|
accounts: AccountAPI
|
||||||
auth: AuthAPI
|
auth: AuthAPI
|
||||||
|
@ -30,6 +31,7 @@ export default class API {
|
||||||
roles: RolesAPI
|
roles: RolesAPI
|
||||||
templates: TemplatesAPI
|
templates: TemplatesAPI
|
||||||
license: LicenseAPI
|
license: LicenseAPI
|
||||||
|
auditLogs: AuditLogAPI
|
||||||
|
|
||||||
constructor(config: TestConfiguration) {
|
constructor(config: TestConfiguration) {
|
||||||
this.accounts = new AccountAPI(config)
|
this.accounts = new AccountAPI(config)
|
||||||
|
@ -47,5 +49,6 @@ export default class API {
|
||||||
this.roles = new RolesAPI(config)
|
this.roles = new RolesAPI(config)
|
||||||
this.templates = new TemplatesAPI(config)
|
this.templates = new TemplatesAPI(config)
|
||||||
this.license = new LicenseAPI(config)
|
this.license = new LicenseAPI(config)
|
||||||
|
this.auditLogs = new AuditLogAPI(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue