Skip to content

Commit d1b9c9a

Browse files
committed
add audit records where documents accessed
1 parent 9fa233e commit d1b9c9a

9 files changed

Lines changed: 215 additions & 37 deletions

apps/backend-services/src/benchmark/dataset.controller.spec.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ jest.mock("@/auth/identity.helpers", () => ({
66
import { BadRequestException, NotFoundException } from "@nestjs/common";
77
import { Test, TestingModule } from "@nestjs/testing";
88
import { Request } from "express";
9+
import { AuditService } from "@/audit/audit.service";
10+
import { AppLoggerService } from "@/logging/app-logger.service";
911
import { DatasetController } from "./dataset.controller";
1012
import { DatasetService } from "./dataset.service";
1113
import {
@@ -40,6 +42,16 @@ const mockDatasetService = {
4042
deleteDataset: jest.fn(),
4143
};
4244

45+
const mockAuditService = {
46+
recordEvent: jest.fn().mockResolvedValue(undefined),
47+
};
48+
const mockLogger = {
49+
debug: jest.fn(),
50+
warn: jest.fn(),
51+
error: jest.fn(),
52+
log: jest.fn(),
53+
};
54+
4355
describe("DatasetController", () => {
4456
let controller: DatasetController;
4557

@@ -58,7 +70,11 @@ describe("DatasetController", () => {
5870

5971
const module: TestingModule = await Test.createTestingModule({
6072
controllers: [DatasetController],
61-
providers: [{ provide: DatasetService, useValue: mockDatasetService }],
73+
providers: [
74+
{ provide: DatasetService, useValue: mockDatasetService },
75+
{ provide: AppLoggerService, useValue: mockLogger },
76+
{ provide: AuditService, useValue: mockAuditService },
77+
],
6278
}).compile();
6379

6480
controller = module.get<DatasetController>(DatasetController);
@@ -701,6 +717,14 @@ describe("DatasetController", () => {
701717
"Content-Length": fileBuffer.length.toString(),
702718
});
703719
expect(mockRes.send).toHaveBeenCalledWith(fileBuffer);
720+
expect(mockAuditService.recordEvent).toHaveBeenCalledWith({
721+
event_type: "document_accessed",
722+
resource_type: "dataset_document",
723+
resource_id: "dataset-123",
724+
actor_id: "user-123",
725+
group_id: "test-group",
726+
payload: { action: "download" },
727+
});
704728
});
705729

706730
it("throws when path query param is missing", async () => {

apps/backend-services/src/benchmark/dataset.controller.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ import {
4040
} from "@nestjs/swagger";
4141
import type { Response } from "express";
4242
import { Request } from "express";
43+
import { AuditService } from "@/audit/audit.service";
4344
import { Identity } from "@/auth/identity.decorator";
4445
import {
4546
getIdentityGroupIds,
4647
identityCanAccessGroup,
4748
} from "@/auth/identity.helpers";
49+
import { AppLoggerService } from "@/logging/app-logger.service";
4850
import { DatasetService } from "./dataset.service";
4951
import {
5052
CreateDatasetDto,
@@ -69,14 +71,18 @@ import {
6971
@ApiTags("Benchmark - Datasets")
7072
@Controller("api/benchmark/datasets")
7173
export class DatasetController {
72-
constructor(private readonly datasetService: DatasetService) {}
74+
constructor(
75+
private readonly datasetService: DatasetService,
76+
private readonly auditService: AuditService,
77+
) {}
7378

7479
private async assertDatasetGroupAccess(
7580
datasetId: string,
7681
req: Request,
77-
): Promise<void> {
82+
): Promise<string> {
7883
const dataset = await this.datasetService.getDatasetById(datasetId);
7984
identityCanAccessGroup(req.resolvedIdentity, dataset.groupId);
85+
return dataset.groupId;
8086
}
8187

8288
@Post()
@@ -508,7 +514,15 @@ export class DatasetController {
508514
@Param("sampleId") sampleId: string,
509515
@Req() req: Request,
510516
): Promise<GroundTruthResponseDto> {
511-
await this.assertDatasetGroupAccess(id, req);
517+
const groupId = await this.assertDatasetGroupAccess(id, req);
518+
await this.auditService.recordEvent({
519+
event_type: "dataset_accessed",
520+
resource_type: "ground_truth",
521+
resource_id: id,
522+
actor_id: req.resolvedIdentity.actorId,
523+
group_id: groupId,
524+
payload: { action: "download" },
525+
});
512526
return this.datasetService.getGroundTruth(id, versionId, sampleId);
513527
}
514528

@@ -530,13 +544,21 @@ export class DatasetController {
530544
@Req() req: Request,
531545
@Res() res: Response,
532546
): Promise<void> {
533-
await this.assertDatasetGroupAccess(id, req);
547+
const groupId = await this.assertDatasetGroupAccess(id, req);
534548

535549
if (!filePath) {
536550
throw new BadRequestException("Query parameter 'path' is required");
537551
}
538552
const { buffer, filename, mimeType } =
539553
await this.datasetService.getSampleFile(id, versionId, filePath);
554+
await this.auditService.recordEvent({
555+
event_type: "document_accessed",
556+
resource_type: "dataset_document",
557+
resource_id: id,
558+
actor_id: req.resolvedIdentity.actorId,
559+
group_id: groupId,
560+
payload: { action: "download" },
561+
});
540562
res.set({
541563
"Content-Type": mimeType,
542564
"Content-Disposition": `attachment; filename="${filename}"`,

apps/backend-services/src/document/document.controller.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,11 @@ describe("DocumentController", () => {
224224
const result = await controller.getOcrResult("1", mockReq as any);
225225
expect(mockAuditService.recordEvent).toHaveBeenCalledWith({
226226
event_type: "document_accessed",
227-
resource_type: "document",
228-
resource_id: "1",
227+
resource_type: "ocr_result",
228+
resource_id: "ocr-1",
229229
actor_id: "actor-1",
230230
document_id: "1",
231231
group_id: mockGroupId,
232-
request_id: undefined,
233232
payload: { action: "ocr" },
234233
});
235234
expect(result).toEqual({

apps/backend-services/src/document/document.controller.ts

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ export class DocumentController {
137137
throw new NotFoundException(`Document not found: ${documentId}`);
138138
}
139139

140+
await this.auditService.recordEvent({
141+
event_type: "document_accessed",
142+
resource_type: "document",
143+
resource_id: documentId,
144+
actor_id: req.resolvedIdentity.actorId,
145+
document_id: documentId,
146+
group_id: document.group_id ?? undefined,
147+
payload: { action: "metadata" },
148+
});
149+
140150
this.logger.debug("=== DocumentController.updateDocument completed ===");
141151
return updated;
142152
}
@@ -277,6 +287,15 @@ export class DocumentController {
277287
);
278288
}
279289
}
290+
await this.auditService.recordEvent({
291+
event_type: "document_accessed",
292+
resource_type: "document",
293+
resource_id: doc.id,
294+
actor_id: req.resolvedIdentity.actorId,
295+
document_id: doc.id,
296+
group_id: doc.group_id,
297+
payload: { action: "metadata" },
298+
});
280299
return doc;
281300
}),
282301
);
@@ -323,16 +342,6 @@ export class DocumentController {
323342

324343
identityCanAccessGroup(req.resolvedIdentity, document.group_id);
325344

326-
await this.auditService.recordEvent({
327-
event_type: "document_accessed",
328-
resource_type: "document",
329-
resource_id: documentId,
330-
actor_id: req.resolvedIdentity.actorId,
331-
document_id: documentId,
332-
group_id: document.group_id ?? undefined,
333-
payload: { action: "ocr" },
334-
});
335-
336345
this.logger.debug(`Document status: ${document.status}`);
337346
this.logger.debug(`Document created: ${document.created_at}`);
338347
if (document.apim_request_id) {
@@ -366,9 +375,20 @@ export class DocumentController {
366375
`OCR result retrieved successfully for document: ${documentId}`,
367376
);
368377
this.logger.debug(`OCR processed at: ${ocrResult.processed_at}`);
378+
379+
await this.auditService.recordEvent({
380+
event_type: "document_accessed",
381+
resource_type: "ocr_result",
382+
resource_id: ocrResult.id,
383+
actor_id: req.resolvedIdentity.actorId,
384+
document_id: documentId,
385+
group_id: document.group_id ?? undefined,
386+
payload: { action: "ocr" },
387+
});
369388
}
370389

371390
this.logger.debug("=== DocumentController.getOcrResult completed ===");
391+
372392
return response;
373393
} catch (error) {
374394
this.logger.error(`Error retrieving OCR result: ${error.message}`);
@@ -415,16 +435,6 @@ export class DocumentController {
415435

416436
identityCanAccessGroup(req.resolvedIdentity, document.group_id);
417437

418-
await this.auditService.recordEvent({
419-
event_type: "document_accessed",
420-
resource_type: "document",
421-
resource_id: documentId,
422-
actor_id: req.resolvedIdentity.actorId,
423-
document_id: documentId,
424-
group_id: document.group_id ?? undefined,
425-
payload: { action: "download" },
426-
});
427-
428438
// Read file from blob storage using the blob key
429439
const fileBuffer = await this.blobStorage.read(document.file_path);
430440

@@ -448,6 +458,16 @@ export class DocumentController {
448458
"=== DocumentController.downloadDocument completed ===",
449459
);
450460

461+
await this.auditService.recordEvent({
462+
event_type: "document_accessed",
463+
resource_type: "document",
464+
resource_id: documentId,
465+
actor_id: req.resolvedIdentity.actorId,
466+
document_id: documentId,
467+
group_id: document.group_id,
468+
payload: { action: "download" },
469+
});
470+
451471
res.send(fileBuffer);
452472
} catch (error) {
453473
this.logger.error(`Error downloading document: ${error.message}`);

apps/backend-services/src/hitl/hitl.controller.spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { GroupRole } from "@generated/client";
22
import { ForbiddenException, NotFoundException } from "@nestjs/common";
33
import { Test, TestingModule } from "@nestjs/testing";
44
import { Request } from "express";
5+
import { AuditService } from "@/audit/audit.service";
56
import { DocumentService } from "../document/document.service";
67
import { EscalateDto, SubmitCorrectionsDto } from "./dto/correction.dto";
78
import { ReviewSessionDto } from "./dto/review-session.dto";
@@ -63,6 +64,10 @@ describe("HitlController", () => {
6364
provide: DocumentService,
6465
useValue: documentService,
6566
},
67+
{
68+
provide: AuditService,
69+
useValue: { recordEvent: jest.fn().mockResolvedValue(undefined) },
70+
},
6671
],
6772
}).compile();
6873

@@ -76,6 +81,7 @@ describe("HitlController", () => {
7681
userId: "user-1",
7782
isSystemAdmin: false,
7883
groupRoles: { "group-1": GroupRole.MEMBER },
84+
actorId: "actor-1",
7985
},
8086
} as unknown as Request;
8187
const mockResult = { documents: [], total: 0 };
@@ -103,6 +109,7 @@ describe("HitlController", () => {
103109
userId: "user-1",
104110
isSystemAdmin: false,
105111
groupRoles: { "group-1": GroupRole.MEMBER },
112+
actorId: "actor-1",
106113
},
107114
} as unknown as Request;
108115
hitlService.getQueue.mockResolvedValue({
@@ -315,6 +322,7 @@ describe("HitlController", () => {
315322
userId: "user-1",
316323
isSystemAdmin: false,
317324
groupRoles: { "group-1": GroupRole.MEMBER },
325+
actorId: "actor-1",
318326
},
319327
} as unknown as Request;
320328
const mockResult = { id: "session-1" };

apps/backend-services/src/hitl/hitl.controller.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
ApiTags,
2121
} from "@nestjs/swagger";
2222
import { Request } from "express";
23+
import { AuditService } from "@/audit/audit.service";
2324
import { Identity } from "@/auth/identity.decorator";
2425
import {
2526
getIdentityGroupIds,
@@ -47,6 +48,7 @@ export class HitlController {
4748
constructor(
4849
private readonly hitlService: HitlService,
4950
private readonly documentService: DocumentService,
51+
private readonly auditService: AuditService,
5052
) {}
5153

5254
@Get("queue")
@@ -64,7 +66,20 @@ export class HitlController {
6466
} else {
6567
groupIds = getIdentityGroupIds(req.resolvedIdentity);
6668
}
67-
return this.hitlService.getQueue(filters, groupIds);
69+
const result = await this.hitlService.getQueue(filters, groupIds);
70+
await Promise.all(
71+
result.documents.map((doc) =>
72+
this.auditService.recordEvent({
73+
event_type: "document_accessed",
74+
resource_type: "document",
75+
resource_id: doc.id,
76+
actor_id: req.resolvedIdentity?.actorId,
77+
document_id: doc.id,
78+
payload: { action: "queue_review" },
79+
}),
80+
),
81+
);
82+
return result;
6883
}
6984

7085
@Get("queue/stats")
@@ -138,7 +153,17 @@ export class HitlController {
138153
throw new NotFoundException(`Review session ${id} not found`);
139154
}
140155
identityCanAccessGroup(req.resolvedIdentity, session.document.group_id);
141-
return this.hitlService.getSession(id);
156+
const result = await this.hitlService.getSession(id);
157+
await this.auditService.recordEvent({
158+
event_type: "document_accessed",
159+
resource_type: "ocr_result",
160+
resource_id: session.document_id,
161+
actor_id: req.resolvedIdentity?.actorId,
162+
document_id: session.document_id,
163+
group_id: session.document.group_id ?? undefined,
164+
payload: { action: "review_session" },
165+
});
166+
return result;
142167
}
143168

144169
@Post("sessions/:id/corrections")

0 commit comments

Comments
 (0)