Skip to content

Commit 9fa6f88

Browse files
authored
[server] audit log service (#19917)
1 parent f24a610 commit 9fa6f88

29 files changed

+6503
-266
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ components/versions/versions.json
3131
.helm-chart-release
3232
*.tgz
3333

34+
3435
# Symbolic links created by scripts for building docker images
3536
/.dockerignore
3637

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
import { AuditLog } from "@gitpod/gitpod-protocol/lib/audit-log";
8+
9+
export const AuditLogDB = Symbol("AuditLogDB");
10+
11+
export interface AuditLogDB {
12+
/**
13+
* Records an audit log entry.
14+
*
15+
* @param logEntry
16+
*/
17+
recordAuditLog(logEntry: AuditLog): Promise<void>;
18+
19+
/**
20+
* Lists audit logs.
21+
*
22+
* @param organizationId
23+
* @param params
24+
*/
25+
listAuditLogs(
26+
organizationId: string,
27+
params?: {
28+
from?: string;
29+
to?: string;
30+
actorId?: string;
31+
action?: string;
32+
pagination?: {
33+
offset?: number;
34+
// must not be larger than 250, default is 100
35+
limit?: number;
36+
};
37+
},
38+
): Promise<AuditLog[]>;
39+
40+
/**
41+
* Purges audit logs older than the given date.
42+
*
43+
* @param before ISO 8601 date string
44+
*/
45+
purgeAuditLogs(before: string, organizationId?: string): Promise<number>;
46+
}

components/gitpod-db/src/container-module.ts

+5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import { LinkedInProfileDB } from "./linked-in-profile-db";
4545
import { DataCache, DataCacheNoop } from "./data-cache";
4646
import { TracingManager } from "@gitpod/gitpod-protocol/lib/util/tracing";
4747
import { EncryptionService, GlobalEncryptionService } from "@gitpod/gitpod-protocol/lib/encryption/encryption-service";
48+
import { AuditLogDB } from "./audit-log-db";
49+
import { AuditLogDBImpl } from "./typeorm/audit-log-db-impl";
4850

4951
// THE DB container module that contains all DB implementations
5052
export const dbContainerModule = (cacheClass = DataCacheNoop) =>
@@ -112,6 +114,9 @@ export const dbContainerModule = (cacheClass = DataCacheNoop) =>
112114
bind(PersonalAccessTokenDBImpl).toSelf().inSingletonScope();
113115
bind(PersonalAccessTokenDB).toService(PersonalAccessTokenDBImpl);
114116

117+
bind(AuditLogDBImpl).toSelf().inSingletonScope();
118+
bind(AuditLogDB).toService(AuditLogDBImpl);
119+
115120
// com concerns
116121
bind(EmailDomainFilterDB).to(EmailDomainFilterDBImpl).inSingletonScope();
117122
bind(LinkedInProfileDBImpl).toSelf().inSingletonScope();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
import { inject, injectable } from "inversify";
8+
import { TypeORM } from "./typeorm";
9+
10+
import { AuditLog } from "@gitpod/gitpod-protocol/lib/audit-log";
11+
import { Between, FindConditions, LessThan, Repository } from "typeorm";
12+
import { AuditLogDB } from "../audit-log-db";
13+
import { DBAuditLog } from "./entity/db-audit-log";
14+
15+
@injectable()
16+
export class AuditLogDBImpl implements AuditLogDB {
17+
@inject(TypeORM) typeORM: TypeORM;
18+
19+
private async getEntityManager() {
20+
return (await this.typeORM.getConnection()).manager;
21+
}
22+
23+
private async getRepo(): Promise<Repository<DBAuditLog>> {
24+
return (await this.getEntityManager()).getRepository(DBAuditLog);
25+
}
26+
27+
async recordAuditLog(logEntry: AuditLog): Promise<void> {
28+
const repo = await this.getRepo();
29+
await repo.insert(logEntry);
30+
}
31+
32+
async listAuditLogs(
33+
organizationId: string,
34+
params?:
35+
| {
36+
from?: string;
37+
to?: string;
38+
actorId?: string;
39+
action?: string;
40+
pagination?: { offset?: number; limit?: number };
41+
}
42+
| undefined,
43+
): Promise<AuditLog[]> {
44+
const repo = await this.getRepo();
45+
const where: FindConditions<DBAuditLog> = {
46+
organizationId,
47+
};
48+
if (params?.from && params?.to) {
49+
where.timestamp = Between(params.from, params.to);
50+
}
51+
if (params?.actorId) {
52+
where.actorId = params.actorId;
53+
}
54+
if (params?.action) {
55+
where.action = params.action;
56+
}
57+
return repo.find({
58+
where,
59+
order: {
60+
timestamp: "DESC",
61+
},
62+
skip: params?.pagination?.offset,
63+
take: params?.pagination?.limit,
64+
});
65+
}
66+
67+
async purgeAuditLogs(before: string, organizationId?: string): Promise<number> {
68+
const repo = await this.getRepo();
69+
const findConditions: FindConditions<DBAuditLog> = {
70+
timestamp: LessThan(before),
71+
};
72+
if (organizationId) {
73+
findConditions.organizationId = organizationId;
74+
}
75+
const result = await repo.delete(findConditions);
76+
return result.affected ?? 0;
77+
}
78+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
import { Entity, Column, PrimaryColumn } from "typeorm";
8+
import { TypeORM } from "../typeorm";
9+
import { AuditLog } from "@gitpod/gitpod-protocol/lib/audit-log";
10+
11+
@Entity()
12+
export class DBAuditLog implements AuditLog {
13+
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
14+
id: string;
15+
16+
@Column("varchar")
17+
timestamp: string;
18+
19+
@Column("varchar")
20+
organizationId: string;
21+
22+
@Column("varchar")
23+
actorId: string;
24+
25+
@Column("varchar")
26+
action: string;
27+
28+
@Column("simple-json")
29+
args: object[];
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
import { MigrationInterface, QueryRunner } from "typeorm";
8+
import { tableExists } from "./helper/helper";
9+
10+
export class AddAuditLogs1718628014741 implements MigrationInterface {
11+
public async up(queryRunner: QueryRunner): Promise<void> {
12+
if (!(await tableExists(queryRunner, "d_b_audit_log"))) {
13+
await queryRunner.query(
14+
`CREATE TABLE d_b_audit_log (
15+
id VARCHAR(36) PRIMARY KEY NOT NULL,
16+
timestamp VARCHAR(30) NOT NULL,
17+
organizationId VARCHAR(36) NOT NULL,
18+
actorId VARCHAR(36) NOT NULL,
19+
action VARCHAR(128) NOT NULL,
20+
args JSON NOT NULL
21+
)`,
22+
);
23+
await queryRunner.query("CREATE INDEX `ind_organizationId` ON `d_b_audit_log` (organizationId)");
24+
await queryRunner.query("CREATE INDEX `ind_timestamp` ON `d_b_audit_log` (timestamp)");
25+
await queryRunner.query("CREATE INDEX `ind_actorId` ON `d_b_audit_log` (actorId)");
26+
await queryRunner.query("CREATE INDEX `ind_action` ON `d_b_audit_log` (action)");
27+
}
28+
}
29+
30+
public async down(queryRunner: QueryRunner): Promise<void> {}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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 interface AuditLog {
8+
id: string;
9+
timestamp: string;
10+
action: string;
11+
organizationId: string;
12+
actorId: string;
13+
args: object[];
14+
}

components/public-api/buf.gen.yaml

+29-29
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
version: v1
22
plugins:
3-
- name: go
4-
out: go
5-
opt:
6-
- module=github.com/gitpod-io/gitpod/components/public-api/go
7-
- name: go-grpc
8-
out: go
9-
opt:
10-
- module=github.com/gitpod-io/gitpod/components/public-api/go
11-
- name: connect-go
12-
out: go
13-
opt:
14-
- module=github.com/gitpod-io/gitpod/components/public-api/go
15-
- name: protoc-proxy-gen
16-
out: go
17-
path: /workspace/go/bin/protoc-proxy-gen
18-
opt:
19-
- module=github.com/gitpod-io/gitpod/components/public-api/go
3+
- name: go
4+
out: go
5+
opt:
6+
- module=github.com/gitpod-io/gitpod/components/public-api/go
7+
- name: go-grpc
8+
out: go
9+
opt:
10+
- module=github.com/gitpod-io/gitpod/components/public-api/go
11+
- name: connect-go
12+
out: go
13+
opt:
14+
- module=github.com/gitpod-io/gitpod/components/public-api/go
15+
- name: protoc-proxy-gen
16+
out: go
17+
path: /root/go-packages/bin/protoc-proxy-gen
18+
opt:
19+
- module=github.com/gitpod-io/gitpod/components/public-api/go
2020

21-
- name: es
22-
out: typescript/src
23-
opt: target=ts
24-
path: typescript/node_modules/.bin/protoc-gen-es
25-
- name: connect-es
26-
out: typescript/src
27-
opt: target=ts
28-
path: typescript/node_modules/.bin/protoc-gen-connect-es
21+
- name: es
22+
out: typescript/src
23+
opt: target=ts
24+
path: typescript/node_modules/.bin/protoc-gen-es
25+
- name: connect-es
26+
out: typescript/src
27+
opt: target=ts
28+
path: typescript/node_modules/.bin/protoc-gen-connect-es
2929

30-
- plugin: buf.build/connectrpc/kotlin
31-
out: java/src/main/java
32-
- plugin: buf.build/protocolbuffers/java
33-
out: java/src/main/java
30+
- plugin: buf.build/connectrpc/kotlin
31+
out: java/src/main/java
32+
- plugin: buf.build/protocolbuffers/java
33+
out: java/src/main/java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
syntax = "proto3";
2+
3+
package gitpod.v1;
4+
5+
import "gitpod/v1/pagination.proto";
6+
import "google/protobuf/timestamp.proto";
7+
8+
option go_package = "github.com/gitpod-io/gitpod/components/public-api/go/v1";
9+
option java_package = "io.gitpod.publicapi.v1";
10+
11+
service AuditLogService {
12+
// ListAuditLogs returns a list of audit logs
13+
rpc ListAuditLogs(ListAuditLogsRequest) returns (ListAuditLogsResponse) {}
14+
}
15+
16+
message ListAuditLogsRequest {
17+
// pagination contains the pagination options for listing workspaces
18+
PaginationRequest pagination = 1;
19+
20+
// organization_id is the ID of the organization that contains the workspaces
21+
//
22+
// +required
23+
string organization_id = 2;
24+
25+
// from specifies the starting time range for this request.
26+
// All sessions which existed starting at from will be returned.
27+
google.protobuf.Timestamp from = 3;
28+
29+
// to specifies the end time range for this request.
30+
// All sessions which existed ending at to will be returned.
31+
google.protobuf.Timestamp to = 4;
32+
33+
// actor_id is the ID of the user that performed the action
34+
string actor_id = 5;
35+
36+
// action is the action that was performed
37+
string action = 6;
38+
}
39+
40+
message ListAuditLogsResponse {
41+
// pagination contains the pagination options for listing workspaces
42+
PaginationResponse pagination = 1;
43+
44+
// audit_logs that matched the query
45+
repeated AuditLog audit_logs = 2;
46+
}
47+
48+
/**
49+
* AuditLog represents an audit log entry
50+
* typescript shape:
51+
*/
52+
message AuditLog {
53+
// id is the unique identifier of the audit log
54+
string id = 1;
55+
56+
// timestamp is the time when the audit log was created
57+
google.protobuf.Timestamp timestamp = 2;
58+
59+
// action is the action that was performed
60+
string action = 3;
61+
62+
// organization_id is the ID of the organization that contains the workspaces
63+
string organization_id = 4;
64+
65+
// actor_id is the ID of the user that performed the action
66+
string actor_id = 5;
67+
68+
// args contains a serialized JSON array off the arguments that were passed to the action
69+
string args = 6;
70+
}

0 commit comments

Comments
 (0)