feat(p9): fence AI + calendar + message by-id leaks (tenant scope)
Task T3.2: AiJobService.getArtifact/accept/reject assertOwns on artifact.job.scopeId; listArtifacts scoped to caller. CalendarService.getMeeting/setConsent/generateTranscript/ summarize/listActionItems assertOwns on meeting.scopeId (optional principal → asserts when supplied by a controller, skipped for internal calls). MessageService.getMessageById gains the same optional fence. Controllers thread the principal through. All 46 ai/calendar/routing/messaging tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,14 +27,12 @@ export class AiController {
|
||||
@Query('status') status?: string,
|
||||
@Headers('authorization') auth?: string,
|
||||
) {
|
||||
this.principal(auth);
|
||||
return this.ai.listArtifacts({ interactionId, status });
|
||||
return this.ai.listArtifacts(this.principal(auth), { interactionId, status });
|
||||
}
|
||||
|
||||
@Get('artifacts/:id')
|
||||
async getArtifact(@Param('id') id: string, @Headers('authorization') auth?: string) {
|
||||
this.principal(auth);
|
||||
return this.ai.getArtifact(id);
|
||||
return this.ai.getArtifact(id, this.principal(auth));
|
||||
}
|
||||
|
||||
@Post('artifacts/:id/accept')
|
||||
|
||||
@@ -166,11 +166,13 @@ export class AiJobService {
|
||||
return { job, artifact };
|
||||
}
|
||||
|
||||
async listArtifacts(opts: { interactionId?: string; status?: string } = {}) {
|
||||
async listArtifacts(principal: MessagePrincipal, opts: { interactionId?: string; status?: string } = {}) {
|
||||
const scope = await this.actors.findScope(principal);
|
||||
if (!scope) return [];
|
||||
return this.prisma.iiosAiArtifact.findMany({
|
||||
where: {
|
||||
job: { scopeId: scope.id, ...(opts.interactionId ? { interactionId: opts.interactionId } : {}) },
|
||||
...(opts.status ? { status: opts.status as Prisma.EnumIiosAiArtifactStatusFilter } : {}),
|
||||
...(opts.interactionId ? { job: { interactionId: opts.interactionId } } : {}),
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
take: 100,
|
||||
@@ -178,12 +180,13 @@ export class AiJobService {
|
||||
});
|
||||
}
|
||||
|
||||
async getArtifact(id: string) {
|
||||
async getArtifact(id: string, principal: MessagePrincipal) {
|
||||
const artifact = await this.prisma.iiosAiArtifact.findUnique({
|
||||
where: { id },
|
||||
include: { claims: true, evidence: true, modelRun: true, job: true },
|
||||
});
|
||||
if (!artifact) throw new NotFoundException('artifact not found');
|
||||
await this.actors.assertOwns(principal, artifact.job.scopeId); // tenant fence (KG-02)
|
||||
return artifact;
|
||||
}
|
||||
|
||||
@@ -191,6 +194,7 @@ export class AiJobService {
|
||||
async accept(artifactId: string, principal: MessagePrincipal) {
|
||||
const artifact = await this.prisma.iiosAiArtifact.findUnique({ where: { id: artifactId }, include: { job: true } });
|
||||
if (!artifact) throw new NotFoundException('artifact not found');
|
||||
await this.actors.assertOwns(principal, artifact.job.scopeId); // tenant fence (KG-02)
|
||||
const actor = await this.actors.resolveActor(artifact.job.scopeId, principal);
|
||||
const updated = await this.prisma.iiosAiArtifact.update({
|
||||
where: { id: artifactId },
|
||||
@@ -207,6 +211,7 @@ export class AiJobService {
|
||||
async reject(artifactId: string, principal: MessagePrincipal) {
|
||||
const artifact = await this.prisma.iiosAiArtifact.findUnique({ where: { id: artifactId }, include: { job: true } });
|
||||
if (!artifact) throw new NotFoundException('artifact not found');
|
||||
await this.actors.assertOwns(principal, artifact.job.scopeId); // tenant fence (KG-02)
|
||||
const actor = await this.actors.resolveActor(artifact.job.scopeId, principal);
|
||||
const updated = await this.prisma.iiosAiArtifact.update({
|
||||
where: { id: artifactId },
|
||||
|
||||
Reference in New Issue
Block a user