Skip to content

Commit

Permalink
[audit log] Fix audit log persistence (#20054)
Browse files Browse the repository at this point in the history
* [audit] Catch + log errors properly

* [audit] Properly serialize BigInts

* [public-api] Fix PublicApiConverter toAuditLog by re-using the BigIntToJson.replacer
  • Loading branch information
geropl authored Jul 22, 2024
1 parent 9089924 commit a03a28e
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 12 deletions.
7 changes: 6 additions & 1 deletion components/gitpod-db/src/typeorm/entity/db-audit-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import { Entity, Column, PrimaryColumn } from "typeorm";
import { TypeORM } from "../typeorm";
import { AuditLog } from "@gitpod/gitpod-protocol/lib/audit-log";
import { BigIntToJson } from "@gitpod/gitpod-protocol/lib/util/stringify";
import { Transformer } from "../transformer";

@Entity()
export class DBAuditLog implements AuditLog {
Expand All @@ -25,6 +27,9 @@ export class DBAuditLog implements AuditLog {
@Column("varchar")
action: string;

@Column("simple-json")
@Column({
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
transformer: Transformer.SIMPLE_JSON_CUSTOM([], BigIntToJson.replacer, BigIntToJson.reviver),
})
args: object[];
}
16 changes: 16 additions & 0 deletions components/gitpod-db/src/typeorm/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ export namespace Transformer {
};
};

export const SIMPLE_JSON_CUSTOM = (
defaultValue: any,
replacer?: (this: any, key: string, value: any) => any,
reviver?: (this: any, key: string, value: any) => any,
) => {
return <ValueTransformer>{
to(value: any): any {
return JSON.stringify(value || defaultValue, replacer);
},
from(value: any): any {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return JSON.parse(value, reviver);
},
};
};

export const encrypted = (encryptionServiceProvider: () => EncryptionService): ValueTransformer => {
return {
to(value: any): any {
Expand Down
22 changes: 22 additions & 0 deletions components/gitpod-protocol/src/util/stringify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2024 Gitpod GmbH. All rights reserved.
* Licensed under the GNU Affero General Public License (AGPL).
* See License.AGPL.txt in the project root for license information.
*/

export namespace BigIntToJson {
export const BIGINT_KEY = "bigint:";
export function replacer(key: string, value: any) {
if (typeof value === "bigint") {
return `${BIGINT_KEY}${value.toString()}`;
}
return value;
}
export function reviver(key: string, value: any) {
if (typeof value === "string" && value.startsWith(BIGINT_KEY)) {
const v: string = value.substring(7);
return BigInt(v);
}
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ import {
WorkspaceStatus_PrebuildResult,
WorkspaceStatus_WorkspaceConditions,
} from "@gitpod/public-api/lib/gitpod/v1/workspace_pb";
import { BigIntToJson } from "@gitpod/gitpod-protocol/lib/util/stringify";
import { getPrebuildLogPath } from "./prebuild-utils";
import { InvalidGitpodYMLError, RepositoryNotFoundError, UnauthorizedRepositoryAccessError } from "./public-api-errors";
const URL = require("url").URL || window.URL;
Expand Down Expand Up @@ -1604,7 +1605,7 @@ export class PublicAPIConverter {
actorId: input.actorId,
action: input.action,
timestamp: this.toTimestamp(input.timestamp),
args: JSON.stringify(input.args),
args: JSON.stringify(input.args, BigIntToJson.replacer),
});
}
}
14 changes: 5 additions & 9 deletions components/server/src/api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,15 +306,11 @@ export class API {
const promise = await apply<Promise<any>>();
const result = await promise;
if (subjectId) {
try {
self.auditLogService.recordAuditLog(
subjectId!.userId()!,
requestContext.requestMethod,
[args[0]],
);
} catch (error) {
log.error("Failed to record audit log", error);
}
self.auditLogService.asyncRecordAuditLog(
subjectId!.userId()!,
requestContext.requestMethod,
[args[0]],
);
}
done();
return result;
Expand Down
17 changes: 17 additions & 0 deletions components/server/src/audit/AuditLogService.spec.db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { createTestContainer } from "../test/service-testing-container-module";
import { UserService } from "../user/user-service";
import { AuditLogService } from "./AuditLogService";
import { Experiments } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
import { Timestamp } from "@bufbuild/protobuf";

describe("AuditLogService", () => {
let container: Container;
Expand Down Expand Up @@ -94,6 +95,22 @@ describe("AuditLogService", () => {
expect(logs.length).to.eq(1);
});

it("should record audit log containing timestamp args (like listWorkspaceSessions)", async () => {
const t1 = new Timestamp({ seconds: 100n, nanos: 0 });
await auditLogService.recordAuditLog(owner.id, "action1", [
{
organizationId: org.id,
},
t1,
]);

const logs = await auditLogService.listAuditLogs(owner.id, org.id);

expect(logs.length).to.eq(1);
expect(logs[0].args.length).to.eq(2);
expect(logs[0].args[1]).to.deep.equal(t1);
});

it("should list audit logs", async () => {
await auditLogDB.recordAuditLog({
id: v4(),
Expand Down
4 changes: 4 additions & 0 deletions components/server/src/audit/AuditLogService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export class AuditLogService {
@inject(UserDB) private readonly dbUser: UserDB,
) {}

asyncRecordAuditLog(actorId: string, method: string, args: any[]): void {
this.recordAuditLog(actorId, method, args).catch((err) => log.error("failed to record audit log", err));
}

async recordAuditLog(actorId: string, method: string, args: any[]): Promise<void> {
if (
!(await getExperimentsClientForBackend().getValueAsync("audit_logs", false, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ class GitpodJsonRpcProxyFactory<T extends object> extends JsonRpcProxyFactory<T>
const result = await this.internalOnRequest(span, method, ...args);
if (userId) {
// omit the last argument, which is the cancellation token
this.auditLogService.recordAuditLog(userId, method, args.slice(0, -1));
this.auditLogService.asyncRecordAuditLog(userId, method, args.slice(0, -1));
}
return result;
} finally {
Expand Down

0 comments on commit a03a28e

Please sign in to comment.