Skip to content

Commit

Permalink
[server] audit log service (#19917)
Browse files Browse the repository at this point in the history
  • Loading branch information
svenefftinge authored Jun 25, 2024
1 parent f24a610 commit 9fa6f88
Show file tree
Hide file tree
Showing 29 changed files with 6,503 additions and 266 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ components/versions/versions.json
.helm-chart-release
*.tgz


# Symbolic links created by scripts for building docker images
/.dockerignore

Expand Down
46 changes: 46 additions & 0 deletions components/gitpod-db/src/audit-log-db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* 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.
*/

import { AuditLog } from "@gitpod/gitpod-protocol/lib/audit-log";

export const AuditLogDB = Symbol("AuditLogDB");

export interface AuditLogDB {
/**
* Records an audit log entry.
*
* @param logEntry
*/
recordAuditLog(logEntry: AuditLog): Promise<void>;

/**
* Lists audit logs.
*
* @param organizationId
* @param params
*/
listAuditLogs(
organizationId: string,
params?: {
from?: string;
to?: string;
actorId?: string;
action?: string;
pagination?: {
offset?: number;
// must not be larger than 250, default is 100
limit?: number;
};
},
): Promise<AuditLog[]>;

/**
* Purges audit logs older than the given date.
*
* @param before ISO 8601 date string
*/
purgeAuditLogs(before: string, organizationId?: string): Promise<number>;
}
5 changes: 5 additions & 0 deletions components/gitpod-db/src/container-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import { LinkedInProfileDB } from "./linked-in-profile-db";
import { DataCache, DataCacheNoop } from "./data-cache";
import { TracingManager } from "@gitpod/gitpod-protocol/lib/util/tracing";
import { EncryptionService, GlobalEncryptionService } from "@gitpod/gitpod-protocol/lib/encryption/encryption-service";
import { AuditLogDB } from "./audit-log-db";
import { AuditLogDBImpl } from "./typeorm/audit-log-db-impl";

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

bind(AuditLogDBImpl).toSelf().inSingletonScope();
bind(AuditLogDB).toService(AuditLogDBImpl);

// com concerns
bind(EmailDomainFilterDB).to(EmailDomainFilterDBImpl).inSingletonScope();
bind(LinkedInProfileDBImpl).toSelf().inSingletonScope();
Expand Down
78 changes: 78 additions & 0 deletions components/gitpod-db/src/typeorm/audit-log-db-impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* 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.
*/

import { inject, injectable } from "inversify";
import { TypeORM } from "./typeorm";

import { AuditLog } from "@gitpod/gitpod-protocol/lib/audit-log";
import { Between, FindConditions, LessThan, Repository } from "typeorm";
import { AuditLogDB } from "../audit-log-db";
import { DBAuditLog } from "./entity/db-audit-log";

@injectable()
export class AuditLogDBImpl implements AuditLogDB {
@inject(TypeORM) typeORM: TypeORM;

private async getEntityManager() {
return (await this.typeORM.getConnection()).manager;
}

private async getRepo(): Promise<Repository<DBAuditLog>> {
return (await this.getEntityManager()).getRepository(DBAuditLog);
}

async recordAuditLog(logEntry: AuditLog): Promise<void> {
const repo = await this.getRepo();
await repo.insert(logEntry);
}

async listAuditLogs(
organizationId: string,
params?:
| {
from?: string;
to?: string;
actorId?: string;
action?: string;
pagination?: { offset?: number; limit?: number };
}
| undefined,
): Promise<AuditLog[]> {
const repo = await this.getRepo();
const where: FindConditions<DBAuditLog> = {
organizationId,
};
if (params?.from && params?.to) {
where.timestamp = Between(params.from, params.to);
}
if (params?.actorId) {
where.actorId = params.actorId;
}
if (params?.action) {
where.action = params.action;
}
return repo.find({
where,
order: {
timestamp: "DESC",
},
skip: params?.pagination?.offset,
take: params?.pagination?.limit,
});
}

async purgeAuditLogs(before: string, organizationId?: string): Promise<number> {
const repo = await this.getRepo();
const findConditions: FindConditions<DBAuditLog> = {
timestamp: LessThan(before),
};
if (organizationId) {
findConditions.organizationId = organizationId;
}
const result = await repo.delete(findConditions);
return result.affected ?? 0;
}
}
30 changes: 30 additions & 0 deletions components/gitpod-db/src/typeorm/entity/db-audit-log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* 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.
*/

import { Entity, Column, PrimaryColumn } from "typeorm";
import { TypeORM } from "../typeorm";
import { AuditLog } from "@gitpod/gitpod-protocol/lib/audit-log";

@Entity()
export class DBAuditLog implements AuditLog {
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
id: string;

@Column("varchar")
timestamp: string;

@Column("varchar")
organizationId: string;

@Column("varchar")
actorId: string;

@Column("varchar")
action: string;

@Column("simple-json")
args: object[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* 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.
*/

import { MigrationInterface, QueryRunner } from "typeorm";
import { tableExists } from "./helper/helper";

export class AddAuditLogs1718628014741 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
if (!(await tableExists(queryRunner, "d_b_audit_log"))) {
await queryRunner.query(
`CREATE TABLE d_b_audit_log (
id VARCHAR(36) PRIMARY KEY NOT NULL,
timestamp VARCHAR(30) NOT NULL,
organizationId VARCHAR(36) NOT NULL,
actorId VARCHAR(36) NOT NULL,
action VARCHAR(128) NOT NULL,
args JSON NOT NULL
)`,
);
await queryRunner.query("CREATE INDEX `ind_organizationId` ON `d_b_audit_log` (organizationId)");
await queryRunner.query("CREATE INDEX `ind_timestamp` ON `d_b_audit_log` (timestamp)");
await queryRunner.query("CREATE INDEX `ind_actorId` ON `d_b_audit_log` (actorId)");
await queryRunner.query("CREATE INDEX `ind_action` ON `d_b_audit_log` (action)");
}
}

public async down(queryRunner: QueryRunner): Promise<void> {}
}
14 changes: 14 additions & 0 deletions components/gitpod-protocol/src/audit-log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* 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 interface AuditLog {
id: string;
timestamp: string;
action: string;
organizationId: string;
actorId: string;
args: object[];
}
58 changes: 29 additions & 29 deletions components/public-api/buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
version: v1
plugins:
- name: go
out: go
opt:
- module=github.com/gitpod-io/gitpod/components/public-api/go
- name: go-grpc
out: go
opt:
- module=github.com/gitpod-io/gitpod/components/public-api/go
- name: connect-go
out: go
opt:
- module=github.com/gitpod-io/gitpod/components/public-api/go
- name: protoc-proxy-gen
out: go
path: /workspace/go/bin/protoc-proxy-gen
opt:
- module=github.com/gitpod-io/gitpod/components/public-api/go
- name: go
out: go
opt:
- module=github.com/gitpod-io/gitpod/components/public-api/go
- name: go-grpc
out: go
opt:
- module=github.com/gitpod-io/gitpod/components/public-api/go
- name: connect-go
out: go
opt:
- module=github.com/gitpod-io/gitpod/components/public-api/go
- name: protoc-proxy-gen
out: go
path: /root/go-packages/bin/protoc-proxy-gen
opt:
- module=github.com/gitpod-io/gitpod/components/public-api/go

- name: es
out: typescript/src
opt: target=ts
path: typescript/node_modules/.bin/protoc-gen-es
- name: connect-es
out: typescript/src
opt: target=ts
path: typescript/node_modules/.bin/protoc-gen-connect-es
- name: es
out: typescript/src
opt: target=ts
path: typescript/node_modules/.bin/protoc-gen-es
- name: connect-es
out: typescript/src
opt: target=ts
path: typescript/node_modules/.bin/protoc-gen-connect-es

- plugin: buf.build/connectrpc/kotlin
out: java/src/main/java
- plugin: buf.build/protocolbuffers/java
out: java/src/main/java
- plugin: buf.build/connectrpc/kotlin
out: java/src/main/java
- plugin: buf.build/protocolbuffers/java
out: java/src/main/java
70 changes: 70 additions & 0 deletions components/public-api/gitpod/v1/auditlogs.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
syntax = "proto3";

package gitpod.v1;

import "gitpod/v1/pagination.proto";
import "google/protobuf/timestamp.proto";

option go_package = "github.com/gitpod-io/gitpod/components/public-api/go/v1";
option java_package = "io.gitpod.publicapi.v1";

service AuditLogService {
// ListAuditLogs returns a list of audit logs
rpc ListAuditLogs(ListAuditLogsRequest) returns (ListAuditLogsResponse) {}
}

message ListAuditLogsRequest {
// pagination contains the pagination options for listing workspaces
PaginationRequest pagination = 1;

// organization_id is the ID of the organization that contains the workspaces
//
// +required
string organization_id = 2;

// from specifies the starting time range for this request.
// All sessions which existed starting at from will be returned.
google.protobuf.Timestamp from = 3;

// to specifies the end time range for this request.
// All sessions which existed ending at to will be returned.
google.protobuf.Timestamp to = 4;

// actor_id is the ID of the user that performed the action
string actor_id = 5;

// action is the action that was performed
string action = 6;
}

message ListAuditLogsResponse {
// pagination contains the pagination options for listing workspaces
PaginationResponse pagination = 1;

// audit_logs that matched the query
repeated AuditLog audit_logs = 2;
}

/**
* AuditLog represents an audit log entry
* typescript shape:
*/
message AuditLog {
// id is the unique identifier of the audit log
string id = 1;

// timestamp is the time when the audit log was created
google.protobuf.Timestamp timestamp = 2;

// action is the action that was performed
string action = 3;

// organization_id is the ID of the organization that contains the workspaces
string organization_id = 4;

// actor_id is the ID of the user that performed the action
string actor_id = 5;

// args contains a serialized JSON array off the arguments that were passed to the action
string args = 6;
}
Loading

0 comments on commit 9fa6f88

Please sign in to comment.