From ed0d4dae80763a6b6975729b0309d0d01e69803c Mon Sep 17 00:00:00 2001 From: maaz519 Date: Thu, 2 Jul 2026 21:51:10 +0530 Subject: [PATCH] feat(p9): fence AI + calendar + message by-id leaks (tenant scope) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- packages/iios-service/src/ai/ai.controller.ts | 6 ++--- packages/iios-service/src/ai/ai.service.ts | 11 ++++++--- .../src/calendar/calendar.controller.ts | 9 +++----- .../src/calendar/calendar.service.ts | 23 +++++++++++++++---- .../src/messaging/message.service.ts | 3 ++- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/packages/iios-service/src/ai/ai.controller.ts b/packages/iios-service/src/ai/ai.controller.ts index 6df0205..253f4d6 100644 --- a/packages/iios-service/src/ai/ai.controller.ts +++ b/packages/iios-service/src/ai/ai.controller.ts @@ -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') diff --git a/packages/iios-service/src/ai/ai.service.ts b/packages/iios-service/src/ai/ai.service.ts index 5f821a8..76d5c5e 100644 --- a/packages/iios-service/src/ai/ai.service.ts +++ b/packages/iios-service/src/ai/ai.service.ts @@ -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 }, diff --git a/packages/iios-service/src/calendar/calendar.controller.ts b/packages/iios-service/src/calendar/calendar.controller.ts index 21116d4..e9b6f82 100644 --- a/packages/iios-service/src/calendar/calendar.controller.ts +++ b/packages/iios-service/src/calendar/calendar.controller.ts @@ -44,14 +44,12 @@ export class CalendarController { @Get('meetings/:id') async get(@Param('id') id: string, @Headers('authorization') auth?: string) { - this.principal(auth); - return this.calendar.getMeeting(id); + return this.calendar.getMeeting(id, this.principal(auth)); } @Post('meetings/:id/consent') async consent(@Param('id') id: string, @Body() body: ConsentDto, @Headers('authorization') auth?: string) { - this.principal(auth); - return this.calendar.setConsent(id, body.actorRefId, body.status as IiosConsentStatus); + return this.calendar.setConsent(id, body.actorRefId, body.status as IiosConsentStatus, this.principal(auth)); } @Post('meetings/:id/transcript') @@ -66,8 +64,7 @@ export class CalendarController { @Get('meetings/:id/action-items') async actionItems(@Param('id') id: string, @Headers('authorization') auth?: string) { - this.principal(auth); - return this.calendar.listActionItems(id); + return this.calendar.listActionItems(id, this.principal(auth)); } @Post('providers') diff --git a/packages/iios-service/src/calendar/calendar.service.ts b/packages/iios-service/src/calendar/calendar.service.ts index e6e10be..74a073b 100644 --- a/packages/iios-service/src/calendar/calendar.service.ts +++ b/packages/iios-service/src/calendar/calendar.service.ts @@ -220,7 +220,7 @@ export class CalendarService { }); } - async getMeeting(id: string) { + async getMeeting(id: string, principal?: MessagePrincipal) { const meeting = await this.prisma.iiosMeeting.findUnique({ where: { id }, include: { @@ -231,11 +231,17 @@ export class CalendarService { }, }); if (!meeting) throw new NotFoundException('meeting not found'); + if (principal) await this.actors.assertOwns(principal, meeting.scopeId); // tenant fence (KG-02) return meeting; } // ── Consent + transcript + summary (the KG-14 safety spine) ────── - async setConsent(meetingId: string, actorRefId: string, status: IiosConsentStatus) { + async setConsent(meetingId: string, actorRefId: string, status: IiosConsentStatus, principal?: MessagePrincipal) { + if (principal) { + const meeting = await this.prisma.iiosMeeting.findUnique({ where: { id: meetingId } }); + if (!meeting) throw new NotFoundException('meeting not found'); + await this.actors.assertOwns(principal, meeting.scopeId); + } return this.prisma.iiosMeetingParticipant.update({ where: { meetingId_actorRefId: { meetingId, actorRefId } }, data: { recordingConsent: status }, @@ -246,9 +252,10 @@ export class CalendarService { * Create the transcript ONLY if every attendee consented + CMP allows; otherwise * a BLOCKED transcript with no segments (fail-closed, KG-14). Idempotent per meeting. */ - async generateTranscript(meetingId: string, _principal: MessagePrincipal) { + async generateTranscript(meetingId: string, principal?: MessagePrincipal) { const meeting = await this.prisma.iiosMeeting.findUnique({ where: { id: meetingId }, include: { participants: true } }); if (!meeting) throw new NotFoundException('meeting not found'); + if (principal) await this.actors.assertOwns(principal, meeting.scopeId); // tenant fence (KG-02) const existing = await this.prisma.iiosMeetingTranscript.findFirst({ where: { meetingId }, include: { segments: true } }); if (existing && existing.status === 'READY') return existing; @@ -288,12 +295,13 @@ export class CalendarService { * revoked consent blocks summarization even if a transcript already exists. * Attendees with visibility=NONE are excluded from inbox exposure (Scenario 6). */ - async summarize(meetingId: string, _principal: MessagePrincipal) { + async summarize(meetingId: string, principal?: MessagePrincipal) { const meeting = await this.prisma.iiosMeeting.findUnique({ where: { id: meetingId }, include: { participants: true, transcripts: { include: { segments: true } } }, }); if (!meeting) throw new NotFoundException('meeting not found'); + if (principal) await this.actors.assertOwns(principal, meeting.scopeId); // tenant fence (KG-02) const verdict = await checkMeetingConsent(this.ports, meetingId, meeting.participants); if (!verdict.allowed) throw new BadRequestException(`recording consent not satisfied: ${verdict.reason}`); @@ -355,7 +363,12 @@ export class CalendarService { }); } - async listActionItems(meetingId: string) { + async listActionItems(meetingId: string, principal?: MessagePrincipal) { + if (principal) { + const meeting = await this.prisma.iiosMeeting.findUnique({ where: { id: meetingId } }); + if (!meeting) throw new NotFoundException('meeting not found'); + await this.actors.assertOwns(principal, meeting.scopeId); + } return this.prisma.iiosMeetingActionItem.findMany({ where: { meetingId }, include: { actionItem: true } }); } diff --git a/packages/iios-service/src/messaging/message.service.ts b/packages/iios-service/src/messaging/message.service.ts index 7f95262..5ac7dbc 100644 --- a/packages/iios-service/src/messaging/message.service.ts +++ b/packages/iios-service/src/messaging/message.service.ts @@ -217,12 +217,13 @@ export class MessageService { return { interactionId, actorId: actor.id }; } - async getMessageById(id: string): Promise { + async getMessageById(id: string, principal?: MessagePrincipal): Promise { const i = await this.prisma.iiosInteraction.findUnique({ where: { id }, include: { parts: { orderBy: { partIndex: 'asc' } } }, }); if (!i || !i.threadId) return null; + if (principal) await this.actors.assertOwns(principal, i.scopeId); // tenant fence (KG-02) when called on behalf of a caller return this.toDto(i, i.threadId); }