Skip to content

Commit a03a28e

Browse files
authored
[audit log] Fix audit log persistence (#20054)
* [audit] Catch + log errors properly * [audit] Properly serialize BigInts * [public-api] Fix PublicApiConverter toAuditLog by re-using the BigIntToJson.replacer
1 parent 9089924 commit a03a28e

File tree

8 files changed

+73
-12
lines changed

8 files changed

+73
-12
lines changed

components/gitpod-db/src/typeorm/entity/db-audit-log.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import { Entity, Column, PrimaryColumn } from "typeorm";
88
import { TypeORM } from "../typeorm";
99
import { AuditLog } from "@gitpod/gitpod-protocol/lib/audit-log";
10+
import { BigIntToJson } from "@gitpod/gitpod-protocol/lib/util/stringify";
11+
import { Transformer } from "../transformer";
1012

1113
@Entity()
1214
export class DBAuditLog implements AuditLog {
@@ -25,6 +27,9 @@ export class DBAuditLog implements AuditLog {
2527
@Column("varchar")
2628
action: string;
2729

28-
@Column("simple-json")
30+
@Column({
31+
type: "text", // it's JSON on DB, but we aim to disable the Typeorm-internal JSON-transformer we can't control and can't disable otherwise
32+
transformer: Transformer.SIMPLE_JSON_CUSTOM([], BigIntToJson.replacer, BigIntToJson.reviver),
33+
})
2934
args: object[];
3035
}

components/gitpod-db/src/typeorm/transformer.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ export namespace Transformer {
8181
};
8282
};
8383

84+
export const SIMPLE_JSON_CUSTOM = (
85+
defaultValue: any,
86+
replacer?: (this: any, key: string, value: any) => any,
87+
reviver?: (this: any, key: string, value: any) => any,
88+
) => {
89+
return <ValueTransformer>{
90+
to(value: any): any {
91+
return JSON.stringify(value || defaultValue, replacer);
92+
},
93+
from(value: any): any {
94+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
95+
return JSON.parse(value, reviver);
96+
},
97+
};
98+
};
99+
84100
export const encrypted = (encryptionServiceProvider: () => EncryptionService): ValueTransformer => {
85101
return {
86102
to(value: any): any {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Copyright (c) 2024 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
export namespace BigIntToJson {
8+
export const BIGINT_KEY = "bigint:";
9+
export function replacer(key: string, value: any) {
10+
if (typeof value === "bigint") {
11+
return `${BIGINT_KEY}${value.toString()}`;
12+
}
13+
return value;
14+
}
15+
export function reviver(key: string, value: any) {
16+
if (typeof value === "string" && value.startsWith(BIGINT_KEY)) {
17+
const v: string = value.substring(7);
18+
return BigInt(v);
19+
}
20+
return value;
21+
}
22+
}

components/public-api/typescript-common/src/public-api-converter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ import {
159159
WorkspaceStatus_PrebuildResult,
160160
WorkspaceStatus_WorkspaceConditions,
161161
} from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
162+
import { BigIntToJson } from "@gitpod/gitpod-protocol/lib/util/stringify";
162163
import { getPrebuildLogPath } from "./prebuild-utils";
163164
import { InvalidGitpodYMLError, RepositoryNotFoundError, UnauthorizedRepositoryAccessError } from "./public-api-errors";
164165
const URL = require("url").URL || window.URL;
@@ -1604,7 +1605,7 @@ export class PublicAPIConverter {
16041605
actorId: input.actorId,
16051606
action: input.action,
16061607
timestamp: this.toTimestamp(input.timestamp),
1607-
args: JSON.stringify(input.args),
1608+
args: JSON.stringify(input.args, BigIntToJson.replacer),
16081609
});
16091610
}
16101611
}

components/server/src/api/server.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -306,15 +306,11 @@ export class API {
306306
const promise = await apply<Promise<any>>();
307307
const result = await promise;
308308
if (subjectId) {
309-
try {
310-
self.auditLogService.recordAuditLog(
311-
subjectId!.userId()!,
312-
requestContext.requestMethod,
313-
[args[0]],
314-
);
315-
} catch (error) {
316-
log.error("Failed to record audit log", error);
317-
}
309+
self.auditLogService.asyncRecordAuditLog(
310+
subjectId!.userId()!,
311+
requestContext.requestMethod,
312+
[args[0]],
313+
);
318314
}
319315
done();
320316
return result;

components/server/src/audit/AuditLogService.spec.db.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { createTestContainer } from "../test/service-testing-container-module";
1515
import { UserService } from "../user/user-service";
1616
import { AuditLogService } from "./AuditLogService";
1717
import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
18+
import { Timestamp } from "@bufbuild/protobuf";
1819

1920
describe("AuditLogService", () => {
2021
let container: Container;
@@ -94,6 +95,22 @@ describe("AuditLogService", () => {
9495
expect(logs.length).to.eq(1);
9596
});
9697

98+
it("should record audit log containing timestamp args (like listWorkspaceSessions)", async () => {
99+
const t1 = new Timestamp({ seconds: 100n, nanos: 0 });
100+
await auditLogService.recordAuditLog(owner.id, "action1", [
101+
{
102+
organizationId: org.id,
103+
},
104+
t1,
105+
]);
106+
107+
const logs = await auditLogService.listAuditLogs(owner.id, org.id);
108+
109+
expect(logs.length).to.eq(1);
110+
expect(logs[0].args.length).to.eq(2);
111+
expect(logs[0].args[1]).to.deep.equal(t1);
112+
});
113+
97114
it("should list audit logs", async () => {
98115
await auditLogDB.recordAuditLog({
99116
id: v4(),

components/server/src/audit/AuditLogService.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export class AuditLogService {
2323
@inject(UserDB) private readonly dbUser: UserDB,
2424
) {}
2525

26+
asyncRecordAuditLog(actorId: string, method: string, args: any[]): void {
27+
this.recordAuditLog(actorId, method, args).catch((err) => log.error("failed to record audit log", err));
28+
}
29+
2630
async recordAuditLog(actorId: string, method: string, args: any[]): Promise<void> {
2731
if (
2832
!(await getExperimentsClientForBackend().getValueAsync("audit_logs", false, {

components/server/src/websocket/websocket-connection-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ class GitpodJsonRpcProxyFactory<T extends object> extends JsonRpcProxyFactory<T>
404404
const result = await this.internalOnRequest(span, method, ...args);
405405
if (userId) {
406406
// omit the last argument, which is the cancellation token
407-
this.auditLogService.recordAuditLog(userId, method, args.slice(0, -1));
407+
this.auditLogService.asyncRecordAuditLog(userId, method, args.slice(0, -1));
408408
}
409409
return result;
410410
} finally {

0 commit comments

Comments
 (0)