From 25b7debe43ac42741cc828c1622c1dde4db7675d Mon Sep 17 00:00:00 2001 From: "@daniel-lxs" <57051444+daniel-lxs@users.noreply.github.com> Date: Sat, 12 Sep 2026 06:58:02 +0000 Subject: [PATCH 1/2] fix: authorize session artifact helper reads --- .../lib/server/__tests__/artifacts.test.ts | 23 +++++++++++++++++++ apps/web/src/lib/server/artifacts.ts | 17 ++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/apps/web/src/lib/server/__tests__/artifacts.test.ts b/apps/web/src/lib/server/__tests__/artifacts.test.ts index 35b4c9425c..29c98f9d12 100644 --- a/apps/web/src/lib/server/__tests__/artifacts.test.ts +++ b/apps/web/src/lib/server/__tests__/artifacts.test.ts @@ -9,6 +9,7 @@ import { import { getArtifactByPath, getArtifactBySessionPath, + getArtifactVersionsBySessionPath, validateArtifactPath, validateArtifactSize, } from '../artifacts'; @@ -95,6 +96,28 @@ describe.each(['task', 'session'] as const)( }, ); +describe('Session artifact helper authorization', () => { + it('rejects path and version reads without a human user', async () => { + const session = await sessionFactory.create(); + const path = 'reports/private.pdf'; + await db.insert(taskArtifacts).values({ + sessionId: session.id, + path, + uploaded: true, + contentType: 'application/pdf', + size: 100, + }); + const auth = { userId: null, isAdmin: false }; + + await expect( + getArtifactBySessionPath({ sessionId: session.id, path, auth }), + ).resolves.toBeNull(); + await expect( + getArtifactVersionsBySessionPath({ sessionId: session.id, path, auth }), + ).resolves.toEqual([]); + }); +}); + describe('validateArtifactPath', () => { it('should accept valid paths', () => { const validPaths = [ diff --git a/apps/web/src/lib/server/artifacts.ts b/apps/web/src/lib/server/artifacts.ts index 86065eda18..4075ddee43 100644 --- a/apps/web/src/lib/server/artifacts.ts +++ b/apps/web/src/lib/server/artifacts.ts @@ -13,6 +13,7 @@ import { validateTaskArtifactPath, } from '@roomote/types'; import { canReadTask } from './custom-automation-task-access'; +import { findReadableSession } from './sessions'; function withTypedArtifactType( artifact: T, @@ -34,6 +35,16 @@ type ArtifactAuth = { isAdmin: boolean; }; +async function canReadSessionArtifacts(auth: ArtifactAuth, sessionId: string) { + if (!auth.userId) return false; + return Boolean( + await findReadableSession( + { userId: auth.userId, isAdmin: auth.isAdmin }, + sessionId, + ), + ); +} + /** * Get an artifact by its ID. */ @@ -89,13 +100,14 @@ export async function getArtifactBySessionPath({ sessionId, path, version, - auth: _auth, + auth, }: { sessionId: string; path: string; version?: number; auth: ArtifactAuth; }) { + if (!(await canReadSessionArtifacts(auth, sessionId))) return null; const artifact = await getSessionArtifactByPath({ sessionId, path, version }); return artifact ? withTypedArtifactType(artifact) : null; } @@ -103,12 +115,13 @@ export async function getArtifactBySessionPath({ export async function getArtifactVersionsBySessionPath({ sessionId, path, - auth: _auth, + auth, }: { sessionId: string; path: string; auth: ArtifactAuth; }) { + if (!(await canReadSessionArtifacts(auth, sessionId))) return []; return db .select({ id: taskArtifacts.id, From 51ca2933aa812d08e78eeebee84e945518c4d587 Mon Sep 17 00:00:00 2001 From: "@daniel-lxs" <57051444+daniel-lxs@users.noreply.github.com> Date: Sat, 12 Sep 2026 17:31:12 +0000 Subject: [PATCH 2/2] test: assert authorized session artifact version reads --- .../lib/server/__tests__/artifacts.test.ts | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/apps/web/src/lib/server/__tests__/artifacts.test.ts b/apps/web/src/lib/server/__tests__/artifacts.test.ts index 29c98f9d12..9112733dac 100644 --- a/apps/web/src/lib/server/__tests__/artifacts.test.ts +++ b/apps/web/src/lib/server/__tests__/artifacts.test.ts @@ -97,25 +97,44 @@ describe.each(['task', 'session'] as const)( ); describe('Session artifact helper authorization', () => { + const path = 'reports/private.pdf'; + let sessionId: string; + + beforeEach(async () => { + sessionId = (await sessionFactory.create()).id; + await db.insert(taskArtifacts).values( + [1, 2, 3].map((version) => ({ + sessionId, + path, + version, + uploaded: version < 3, + contentType: 'application/pdf', + size: 100, + })), + ); + }); + it('rejects path and version reads without a human user', async () => { - const session = await sessionFactory.create(); - const path = 'reports/private.pdf'; - await db.insert(taskArtifacts).values({ - sessionId: session.id, - path, - uploaded: true, - contentType: 'application/pdf', - size: 100, - }); const auth = { userId: null, isAdmin: false }; await expect( - getArtifactBySessionPath({ sessionId: session.id, path, auth }), + getArtifactBySessionPath({ sessionId, path, auth }), ).resolves.toBeNull(); await expect( - getArtifactVersionsBySessionPath({ sessionId: session.id, path, auth }), + getArtifactVersionsBySessionPath({ sessionId, path, auth }), ).resolves.toEqual([]); }); + + it('returns uploaded versions, latest first, for an authorized member', async () => { + const auth = { userId: (await userFactory.create()).id, isAdmin: false }; + + await expect( + getArtifactVersionsBySessionPath({ sessionId, path, auth }), + ).resolves.toMatchObject([{ version: 2 }, { version: 1 }]); + await expect( + getArtifactBySessionPath({ sessionId, path, auth }), + ).resolves.toMatchObject({ path, version: 2, uploaded: true }); + }); }); describe('validateArtifactPath', () => {