Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/device-agent-release.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
name: Device Agent Release

# The push trigger is intentionally limited to protected branches (main, release).
# This pipeline handles Apple code-signing secrets (MAC_CSC_LINK, APPLE_ID, ...) and
# SSL.com eSigner secrets (ESIGNER_*) - broadening it to arbitrary branches would let
# anyone who can push a branch exfiltrate those credentials. Manual staging builds are
# still available via workflow_dispatch.
on:
workflow_dispatch:
push:
branches: ['**']
branches: ['main', 'release']
Comment thread
dennisofficial marked this conversation as resolved.
paths:
- 'packages/device-agent/**'

Expand Down Expand Up @@ -90,6 +95,7 @@ jobs:
name: Build macOS (.dmg + .zip)
needs: detect-version
runs-on: macos-latest
environment: ${{ needs.detect-version.outputs.s3_env }}
Comment thread
dennisofficial marked this conversation as resolved.
defaults:
run:
working-directory: packages/device-agent
Expand Down Expand Up @@ -148,6 +154,7 @@ jobs:
name: Build Windows (.exe)
needs: detect-version
runs-on: windows-latest
environment: ${{ needs.detect-version.outputs.s3_env }}
defaults:
run:
working-directory: packages/device-agent
Expand Down Expand Up @@ -418,6 +425,7 @@ jobs:
name: Upload to S3
needs: [detect-version, build-macos, build-windows, build-linux]
runs-on: ubuntu-latest
environment: ${{ needs.detect-version.outputs.s3_env }}
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
Expand Down
161 changes: 157 additions & 4 deletions apps/api/src/soa/soa.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jest.mock('@/vector-store/lib', () => ({
syncOrganizationEmbeddings: jest.fn(),
}));

import { syncOrganizationEmbeddings } from '@/vector-store/lib';

describe('SOAController', () => {
let controller: SOAController;
let soaService: jest.Mocked<SOAService>;
Expand Down Expand Up @@ -98,7 +100,7 @@ describe('SOAController', () => {
mockSOAService.saveAnswer.mockResolvedValue({ success: true });

const result = await controller.saveAnswer(
dto as never,
dto,
'org_123',
mockAuthContext,
);
Expand All @@ -112,6 +114,64 @@ describe('SOAController', () => {
controller.saveAnswer(dto as never, 'org_123', noUserAuthContext),
).rejects.toThrow(BadRequestException);
});

it('should overwrite a body-supplied organizationId with the session organizationId', async () => {
const spoofedDto = { ...dto, organizationId: 'org_victim' };
mockSOAService.saveAnswer.mockResolvedValue({ success: true });

await controller.saveAnswer(spoofedDto, 'org_own', mockAuthContext);

expect(soaService.saveAnswer).toHaveBeenCalledWith(
expect.objectContaining({ organizationId: 'org_own' }),
'usr_123',
);
});
});

describe('autoFill', () => {
const dto = {
organizationId: 'org_123',
documentId: 'doc_1',
};

it('should overwrite a body-supplied organizationId with the session organizationId', async () => {
const spoofedDto = { ...dto, organizationId: 'org_victim' };
mockSOAService.getDocument.mockResolvedValue({
configuration: { questions: [] },
});
mockSOAService.checkIfFullyRemote.mockResolvedValue(false);
mockSOAService.batchSearchSOAQuestions.mockResolvedValue(new Map());
mockSOAService.saveAnswersToDatabase.mockResolvedValue(undefined);
mockSOAService.countAnsweredAnswers.mockResolvedValue(0);
mockSOAService.updateDocumentAfterAutoFill.mockResolvedValue(undefined);
const res = {
setHeader: jest.fn(),
write: jest.fn(),
end: jest.fn(),
} as unknown as Response;

await controller.autoFill(spoofedDto, 'org_own', mockAuthContext, res);

expect(syncOrganizationEmbeddings).toHaveBeenCalledWith('org_own');
expect(soaService.getDocument).toHaveBeenCalledWith('doc_1', 'org_own');
expect(soaService.checkIfFullyRemote).toHaveBeenCalledWith('org_own');
expect(soaService.batchSearchSOAQuestions).toHaveBeenCalledWith(
[],
'org_own',
);
});

it('should throw BadRequestException when userId is missing', async () => {
const res = {
setHeader: jest.fn(),
write: jest.fn(),
end: jest.fn(),
} as unknown as Response;

await expect(
controller.autoFill(dto as never, 'org_123', noUserAuthContext, res),
).rejects.toThrow(BadRequestException);
});
});

describe('createDocument', () => {
Expand All @@ -129,6 +189,17 @@ describe('SOAController', () => {
expect(soaService.createDocument).toHaveBeenCalledWith(dto);
expect(result).toEqual(created);
});

it('should overwrite a body-supplied organizationId with the session organizationId', async () => {
const spoofedDto = { ...dto, organizationId: 'org_victim' };
mockSOAService.createDocument.mockResolvedValue({ id: 'doc_1' });

await controller.createDocument(spoofedDto as never, 'org_own');

expect(soaService.createDocument).toHaveBeenCalledWith(
expect.objectContaining({ organizationId: 'org_own' }),
);
});
});

describe('ensureSetup', () => {
Expand All @@ -146,6 +217,19 @@ describe('SOAController', () => {
expect(soaService.ensureSetup).toHaveBeenCalledWith(dto);
expect(result).toEqual(setupResult);
});

it('should overwrite a body-supplied organizationId with the session organizationId', async () => {
const spoofedDto = { ...dto, organizationId: 'org_victim' };
mockSOAService.ensureSetup.mockResolvedValue({
document: { id: 'doc_1' },
});

await controller.ensureSetup(spoofedDto as never, 'org_own');

expect(soaService.ensureSetup).toHaveBeenCalledWith(
expect.objectContaining({ organizationId: 'org_own' }),
);
});
});

describe('getSetup', () => {
Expand All @@ -162,11 +246,26 @@ describe('SOAController', () => {
};
mockSOAService.getSetup.mockResolvedValue(setupResult);

const result = await controller.getSetup(dto as never, 'org_123');
const result = await controller.getSetup(dto, 'org_123');

expect(soaService.getSetup).toHaveBeenCalledWith(dto);
expect(result).toEqual(setupResult);
});

it('should overwrite a body-supplied organizationId with the session organizationId', async () => {
const spoofedDto = { ...dto, organizationId: 'org_victim' };
mockSOAService.getSetup.mockResolvedValue({
success: true,
configuration: null,
document: null,
});

await controller.getSetup(spoofedDto, 'org_own');

expect(soaService.getSetup).toHaveBeenCalledWith(
expect.objectContaining({ organizationId: 'org_own' }),
);
});
});

describe('approveDocument', () => {
Expand All @@ -180,7 +279,7 @@ describe('SOAController', () => {
mockSOAService.approveDocument.mockResolvedValue(approved);

const result = await controller.approveDocument(
dto as never,
dto,
'org_123',
mockAuthContext,
);
Expand All @@ -194,6 +293,18 @@ describe('SOAController', () => {
controller.approveDocument(dto as never, 'org_123', noUserAuthContext),
).rejects.toThrow(BadRequestException);
});

it('should overwrite a body-supplied organizationId with the session organizationId', async () => {
const spoofedDto = { ...dto, organizationId: 'org_victim' };
mockSOAService.approveDocument.mockResolvedValue({ success: true });

await controller.approveDocument(spoofedDto, 'org_own', mockAuthContext);

expect(soaService.approveDocument).toHaveBeenCalledWith(
expect.objectContaining({ organizationId: 'org_own' }),
'usr_123',
);
});
});

describe('declineDocument', () => {
Expand All @@ -208,7 +319,7 @@ describe('SOAController', () => {
mockSOAService.declineDocument.mockResolvedValue(declined);

const result = await controller.declineDocument(
dto as never,
dto,
'org_123',
mockAuthContext,
);
Expand All @@ -222,6 +333,18 @@ describe('SOAController', () => {
controller.declineDocument(dto as never, 'org_123', noUserAuthContext),
).rejects.toThrow(BadRequestException);
});

it('should overwrite a body-supplied organizationId with the session organizationId', async () => {
const spoofedDto = { ...dto, organizationId: 'org_victim' };
mockSOAService.declineDocument.mockResolvedValue({ success: true });

await controller.declineDocument(spoofedDto, 'org_own', mockAuthContext);

expect(soaService.declineDocument).toHaveBeenCalledWith(
expect.objectContaining({ organizationId: 'org_own' }),
'usr_123',
);
});
});

describe('submitForApproval', () => {
Expand All @@ -242,6 +365,17 @@ describe('SOAController', () => {
expect(soaService.submitForApproval).toHaveBeenCalledWith(dto);
expect(result).toEqual(submitted);
});

it('should overwrite a body-supplied organizationId with the session organizationId', async () => {
const spoofedDto = { ...dto, organizationId: 'org_victim' };
mockSOAService.submitForApproval.mockResolvedValue({ success: true });

await controller.submitForApproval(spoofedDto as never, 'org_own');

expect(soaService.submitForApproval).toHaveBeenCalledWith(
expect.objectContaining({ organizationId: 'org_own' }),
);
});
});

describe('exportDocument', () => {
Expand Down Expand Up @@ -278,5 +412,24 @@ describe('SOAController', () => {
);
expect(res.send).toHaveBeenCalledWith(fileBuffer);
});

it('should overwrite a body-supplied organizationId with the session organizationId', async () => {
const spoofedDto = { ...dto, organizationId: 'org_victim' };
mockSOAService.exportDocument.mockResolvedValue({
fileBuffer: Buffer.from('pdf-data'),
mimeType: 'application/pdf',
filename: 'soa-export.pdf',
});
const res = {
setHeader: jest.fn(),
send: jest.fn(),
} as unknown as Response;

await controller.exportDocument(spoofedDto as never, res, 'org_own');

expect(soaService.exportDocument).toHaveBeenCalledWith(
expect.objectContaining({ organizationId: 'org_own' }),
);
});
});
});
7 changes: 7 additions & 0 deletions apps/api/src/soa/soa.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export class SOAController {
throw new BadRequestException('User not authenticated');
}

dto.organizationId = organizationId;
Comment thread
dennisofficial marked this conversation as resolved.
return this.soaService.saveAnswer(dto, authContext.userId);
}

Expand All @@ -97,6 +98,7 @@ export class SOAController {
throw new BadRequestException('User not authenticated');
}

dto.organizationId = organizationId;
const userId = authContext.userId;
setupSSEHeaders(res);
const send = createSafeSSESender(res);
Expand Down Expand Up @@ -319,6 +321,7 @@ export class SOAController {
@Body() dto: CreateSOADocumentDto,
@OrganizationId() organizationId: string,
) {
dto.organizationId = organizationId;
return this.soaService.createDocument(dto);
}

Expand All @@ -334,6 +337,7 @@ export class SOAController {
@Body() dto: EnsureSOASetupDto,
@OrganizationId() organizationId: string,
) {
dto.organizationId = organizationId;
return this.soaService.ensureSetup(dto);
}

Expand Down Expand Up @@ -372,6 +376,7 @@ export class SOAController {
throw new BadRequestException('User not authenticated');
}

dto.organizationId = organizationId;
return this.soaService.approveDocument(dto, authContext.userId);
}

Expand All @@ -392,6 +397,7 @@ export class SOAController {
throw new BadRequestException('User not authenticated');
}

dto.organizationId = organizationId;
return this.soaService.declineDocument(dto, authContext.userId);
}

Expand All @@ -407,6 +413,7 @@ export class SOAController {
@Body() dto: SubmitSOAForApprovalDto,
@OrganizationId() organizationId: string,
) {
dto.organizationId = organizationId;
return this.soaService.submitForApproval(dto);
}

Expand Down
Loading