Skip to content

Commit 4ca0e54

Browse files
committed
[server] audit log service
1 parent 14bbec0 commit 4ca0e54

File tree

17 files changed

+808
-250
lines changed

17 files changed

+808
-250
lines changed

.devcontainer/Dockerfile

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -294,19 +294,19 @@ RUN mkdir -p ~/.terraform \
294294
## Java
295295
ENV JAVA_VERSION=11.0.23.fx-zulu
296296
RUN curl -fsSL "https://get.sdkman.io" | bash \
297-
&& bash -c ". /root/.sdkman/bin/sdkman-init.sh \
298-
&& sed -i 's/sdkman_selfupdate_enable=true/sdkman_selfupdate_enable=false/g' /root/.sdkman/etc/config \
299-
&& sed -i 's/sdkman_selfupdate_feature=true/sdkman_selfupdate_feature=false/g' /root/.sdkman/etc/config \
300-
&& sdk install java ${JAVA_VERSION} \
301-
&& sdk default java ${JAVA_VERSION} \
302-
&& sdk install gradle \
303-
&& sdk install maven \
304-
&& sdk flush archives \
305-
&& sdk flush temp \
306-
&& mkdir /root/.m2 \
307-
&& printf '<settings>\n <localRepository>/workspace/m2-repository/</localRepository>\n</settings>\n' > /root/.m2/settings.xml \
308-
&& echo 'export SDKMAN_DIR=\"/root/.sdkman\"' >> /root/.bashrc.d/99-java \
309-
&& echo '[[ -s \"/root/.sdkman/bin/sdkman-init.sh\" ]] && source \"/root/.sdkman/bin/sdkman-init.sh\"' >> /root/.bashrc.d/99-java"
297+
&& bash -c ". /root/.sdkman/bin/sdkman-init.sh \
298+
&& sed -i 's/sdkman_selfupdate_enable=true/sdkman_selfupdate_enable=false/g' /root/.sdkman/etc/config \
299+
&& sed -i 's/sdkman_selfupdate_feature=true/sdkman_selfupdate_feature=false/g' /root/.sdkman/etc/config \
300+
&& sdk install java ${JAVA_VERSION} \
301+
&& sdk default java ${JAVA_VERSION} \
302+
&& sdk install gradle \
303+
&& sdk install maven \
304+
&& sdk flush archives \
305+
&& sdk flush temp \
306+
&& mkdir /root/.m2 \
307+
&& printf '<settings>\n <localRepository>/workspace/m2-repository/</localRepository>\n</settings>\n' > /root/.m2/settings.xml \
308+
&& echo 'export SDKMAN_DIR=\"/root/.sdkman\"' >> /root/.bashrc.d/99-java \
309+
&& echo '[[ -s \"/root/.sdkman/bin/sdkman-init.sh\" ]] && source \"/root/.sdkman/bin/sdkman-init.sh\"' >> /root/.bashrc.d/99-java"
310310
# above, we are adding the sdkman init to .bashrc (executing sdkman-init.sh does that), because one is executed on interactive shells, the other for non-interactive shells (e.g. plugin-host)
311311
ENV GRADLE_USER_HOME=/workspace/.gradle/
312312

@@ -317,12 +317,28 @@ ENV PATH=/root/.nvm/versions/node/v${NODE_VERSION}/bin:/root/.yarn/bin:${PNPM_HO
317317
ENV HOME=/root
318318
RUN curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash \
319319
&& bash -c ". $HOME/.nvm/nvm.sh \
320-
&& nvm install v${NODE_VERSION} \
321-
&& nvm alias default v${NODE_VERSION} \
322-
&& npm install -g typescript yarn pnpm node-gyp"
320+
&& nvm install v${NODE_VERSION} \
321+
&& nvm alias default v${NODE_VERSION} \
322+
&& npm install -g typescript yarn pnpm node-gyp"
323323

324324
ENV PATH=$PATH:/root/.aws-iam:/root/.terraform:/workspace/bin
325325

326+
# install spicedb and dependencies
327+
# Install dependencies
328+
RUN apt-get update && apt-get install -y \
329+
curl \
330+
ca-certificates \
331+
netcat \
332+
gpg
333+
334+
# Add and trust the SpiceDB apt source
335+
RUN curl https://pkg.authzed.com/apt/gpg.key | apt-key add - && \
336+
sh -c 'echo "deb https://pkg.authzed.com/apt/ * *" > /etc/apt/sources.list.d/authzed.list' && \
337+
apt-get update
338+
339+
# Install SpiceDB
340+
RUN apt-get install -y spicedb zed
341+
326342
# Install pre-commit hooks under /workspace during prebuilds
327343
ENV PRE_COMMIT_HOME=/workspace/.pre-commit
328344

.devcontainer/devcontainer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"installDockerComposeSwitch": false
2020
}
2121
},
22+
"onCreateCommand": "yarn && yarn build",
2223
"customizations": {
2324
"vscode": {
2425
"extensions": [

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ components/versions/versions.json
3131
.helm-chart-release
3232
*.tgz
3333

34+
components/public-api/java/bin
35+
3436
# Symbolic links created by scripts for building docker images
3537
/.dockerignore
3638

Lines changed: 46 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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();
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Copyright (c) 2020 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 | undefined;
37+
to?: string | undefined;
38+
actorId?: string | undefined;
39+
action?: string | undefined;
40+
pagination?: { offset?: number | undefined; limit?: number | undefined };
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+
skip: params?.pagination?.offset,
60+
take: params?.pagination?.limit,
61+
});
62+
}
63+
64+
async purgeAuditLogs(before: string, organizationId?: string): Promise<number> {
65+
const repo = await this.getRepo();
66+
const findConditions: FindConditions<DBAuditLog> = {
67+
timestamp: LessThan(before),
68+
};
69+
if (organizationId) {
70+
findConditions.organizationId = organizationId;
71+
}
72+
const result = await repo.delete(findConditions);
73+
return result.affected ?? 0;
74+
}
75+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright (c) 2021 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+
}
Lines changed: 31 additions & 0 deletions
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+
}
Lines changed: 14 additions & 0 deletions
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/server/src/api/server.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { InstallationService } from "@gitpod/public-api/lib/gitpod/v1/installati
6262
import { RateLimitter } from "../rate-limitter";
6363
import { TokenServiceAPI } from "./token-service-api";
6464
import { TokenService } from "@gitpod/public-api/lib/gitpod/v1/token_connect";
65+
import { AuditLogService } from "../audit/AuditLogService";
6566

6667
decorate(injectable(), PublicAPIConverter);
6768

@@ -92,6 +93,7 @@ export class API {
9293
@inject(VerificationServiceAPI) private readonly verificationServiceApi: VerificationServiceAPI;
9394
@inject(InstallationServiceAPI) private readonly installationServiceApi: InstallationServiceAPI;
9495
@inject(RateLimitter) private readonly rateLimitter: RateLimitter;
96+
@inject(AuditLogService) private readonly auditLogService: AuditLogService;
9597

9698
listenPrivate(): http.Server {
9799
const app = express();
@@ -299,6 +301,17 @@ export class API {
299301
try {
300302
const promise = await apply<Promise<any>>();
301303
const result = await promise;
304+
if (subjectId) {
305+
try {
306+
self.auditLogService.recordAuditLog(
307+
subjectId!.userId()!,
308+
requestContext.requestMethod,
309+
args,
310+
);
311+
} catch (error) {
312+
log.error("Failed to record audit log", error);
313+
}
314+
}
302315
done();
303316
return result;
304317
} catch (e) {

0 commit comments

Comments
 (0)