From e4290a8e059c2edfb7457346c657874bf0bdab30 Mon Sep 17 00:00:00 2001 From: Nixxx19 Date: Tue, 3 Mar 2026 20:04:35 +0530 Subject: [PATCH 1/2] fix: add authorization check for private project assets --- server/controllers/project.controller.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/controllers/project.controller.js b/server/controllers/project.controller.js index b0ebb32aa7..57eda81381 100644 --- a/server/controllers/project.controller.js +++ b/server/controllers/project.controller.js @@ -131,6 +131,14 @@ export async function getProjectAsset(req, res) { .send({ message: 'Project with that id does not exist' }); } + // Check visibility and ownership for private projects + if ( + project.visibility === 'Private' && + (!req.user || !project.user._id.equals(req.user._id)) + ) { + return res.status(403).send({ message: 'Project is private' }); + } + const filePath = req.params[0]; const resolvedFile = resolvePathToFile(filePath, project.files); if (!resolvedFile) { From e22ad83b5cccaa722eb311d9958e1bd664886bab Mon Sep 17 00:00:00 2001 From: Nixxx19 Date: Tue, 3 Mar 2026 20:09:01 +0530 Subject: [PATCH 2/2] test: add tests for getProjectAsset authorization --- .../__test__/getProjectAsset.test.js | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 server/controllers/project.controller/__test__/getProjectAsset.test.js diff --git a/server/controllers/project.controller/__test__/getProjectAsset.test.js b/server/controllers/project.controller/__test__/getProjectAsset.test.js new file mode 100644 index 0000000000..75db184ce1 --- /dev/null +++ b/server/controllers/project.controller/__test__/getProjectAsset.test.js @@ -0,0 +1,213 @@ +/** + * @jest-environment node + */ +import { Request, Response } from 'jest-express'; +import axios from 'axios'; +import mime from 'mime'; +import Project from '../../../models/project'; +import { getProjectAsset } from '../../project.controller'; +import { resolvePathToFile } from '../../../utils/filePath'; + +jest.mock('../../../models/project'); +jest.mock('../../../utils/filePath'); +jest.mock('mime'); +jest.mock('axios'); + +describe('project.controller', () => { + describe('getProjectAsset()', () => { + let request; + let response; + + beforeEach(() => { + request = new Request(); + response = new Response(); + response.send = jest.fn(); + response.status = jest.fn().mockReturnThis(); + response.set = jest.fn().mockReturnThis(); + request.setParams({ project_id: 'project-123', 0: 'image.png' }); + Project.findOne = jest.fn().mockReturnValue({ + populate: jest.fn().mockReturnThis(), + exec: jest.fn() + }); + jest.clearAllMocks(); + }); + + afterEach(() => { + request.resetMocked(); + response.resetMocked(); + }); + + it('returns 404 if project does not exist', async () => { + Project.findOne().populate().exec.mockResolvedValue(null); + + await getProjectAsset(request, response); + + expect(response.status).toHaveBeenCalledWith(404); + expect(response.send).toHaveBeenCalledWith({ + message: 'Project with that id does not exist' + }); + }); + + it('returns 403 if private project is accessed by unauthenticated user', async () => { + const project = { + _id: 'project-123', + visibility: 'Private', + user: { _id: { equals: jest.fn() } }, + files: [] + }; + request.user = undefined; + + Project.findOne().populate().exec.mockResolvedValue(project); + + await getProjectAsset(request, response); + + expect(response.status).toHaveBeenCalledWith(403); + expect(response.send).toHaveBeenCalledWith({ + message: 'Project is private' + }); + }); + + it('returns 403 if private project is accessed by non-owner', async () => { + const ownerId = { equals: jest.fn().mockReturnValue(false) }; + const project = { + _id: 'project-123', + visibility: 'Private', + user: { _id: ownerId }, + files: [] + }; + request.user = { _id: 'other-user-id' }; + + Project.findOne().populate().exec.mockResolvedValue(project); + + await getProjectAsset(request, response); + + expect(response.status).toHaveBeenCalledWith(403); + expect(response.send).toHaveBeenCalledWith({ + message: 'Project is private' + }); + expect(ownerId.equals).toHaveBeenCalledWith('other-user-id'); + }); + + it('allows owner to access private project assets', async () => { + const ownerId = 'owner-123'; + const ownerIdObj = { equals: jest.fn().mockReturnValue(true) }; + const project = { + _id: 'project-123', + visibility: 'Private', + user: { _id: ownerIdObj }, + files: [] + }; + const resolvedFile = { + name: 'image.png', + content: Buffer.from('image content') + }; + + request.user = { _id: ownerId }; + Project.findOne().populate().exec.mockResolvedValue(project); + resolvePathToFile.mockReturnValue(resolvedFile); + mime.getType.mockReturnValue('image/png'); + + await getProjectAsset(request, response); + + expect(ownerIdObj.equals).toHaveBeenCalledWith(ownerId); + expect(response.status).not.toHaveBeenCalledWith(403); + expect(response.set).toHaveBeenCalledWith('Content-Type', 'image/png'); + expect(response.send).toHaveBeenCalledWith(resolvedFile.content); + }); + + it('allows anyone to access public project assets', async () => { + const project = { + _id: 'project-123', + visibility: 'Public', + user: { _id: { equals: jest.fn() } }, + files: [] + }; + const resolvedFile = { + name: 'image.png', + content: Buffer.from('image content') + }; + + request.user = undefined; // unauthenticated + Project.findOne().populate().exec.mockResolvedValue(project); + resolvePathToFile.mockReturnValue(resolvedFile); + mime.getType.mockReturnValue('image/png'); + + await getProjectAsset(request, response); + + expect(response.status).not.toHaveBeenCalledWith(403); + expect(response.set).toHaveBeenCalledWith('Content-Type', 'image/png'); + expect(response.send).toHaveBeenCalledWith(resolvedFile.content); + }); + + it('returns 404 if asset does not exist', async () => { + const project = { + _id: 'project-123', + visibility: 'Public', + user: { _id: { equals: jest.fn() } }, + files: [] + }; + + Project.findOne().populate().exec.mockResolvedValue(project); + resolvePathToFile.mockReturnValue(null); + + await getProjectAsset(request, response); + + expect(response.status).toHaveBeenCalledWith(404); + expect(response.send).toHaveBeenCalledWith({ + message: 'Asset does not exist' + }); + }); + + it('fetches and serves asset from URL when resolvedFile has url', async () => { + const project = { + _id: 'project-123', + visibility: 'Public', + user: { _id: { equals: jest.fn() } }, + files: [] + }; + const resolvedFile = { + name: 'image.png', + url: 'https://example.com/image.png' + }; + const imageData = Buffer.from('image data'); + + Project.findOne().populate().exec.mockResolvedValue(project); + resolvePathToFile.mockReturnValue(resolvedFile); + mime.getType.mockReturnValue('image/png'); + axios.get.mockResolvedValue({ data: imageData }); + + await getProjectAsset(request, response); + + expect(axios.get).toHaveBeenCalledWith(resolvedFile.url, { + responseType: 'arraybuffer' + }); + expect(response.set).toHaveBeenCalledWith('Content-Type', 'image/png'); + expect(response.send).toHaveBeenCalledWith(imageData); + }); + + it('returns 404 if fetching asset URL fails', async () => { + const project = { + _id: 'project-123', + visibility: 'Public', + user: { _id: { equals: jest.fn() } }, + files: [] + }; + const resolvedFile = { + name: 'image.png', + url: 'https://example.com/image.png' + }; + + Project.findOne().populate().exec.mockResolvedValue(project); + resolvePathToFile.mockReturnValue(resolvedFile); + mime.getType.mockReturnValue('image/png'); + axios.get.mockRejectedValue(new Error('Network error')); + + await getProjectAsset(request, response); + + expect(response.status).toHaveBeenCalledWith(404); + expect(response.send).toHaveBeenCalledWith({ + message: 'Asset does not exist' + }); + }); + }); +});