fix: scope SAM sessions to owning user (#354)

This commit is contained in:
Ben Senescu 2026-07-05 18:36:58 -04:00 committed by GitHub
parent b22dc13b51
commit 36bd1cd7de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 31 additions and 10 deletions

View File

@ -72,7 +72,10 @@ async function authorizeSamChat(
} catch { } catch {
return new Response("Unauthorized", { status: 401 }); return new Response("Unauthorized", { status: 401 });
} }
const session = await SamSessionRepository.getActiveSession(sessionId); const session = await SamSessionRepository.getActiveSession(
sessionId,
context.userId,
);
const project = session const project = session
? await ProjectRepository.getProjectForOrganization( ? await ProjectRepository.getProjectForOrganization(
session.projectId, session.projectId,

View File

@ -21,7 +21,8 @@ async function createSession(input: CreateSamSessionInput) {
} }
// Callers must have already authorized the project (requireProjectContext). // Callers must have already authorized the project (requireProjectContext).
async function listSessionsForProject(projectId: string) { // Scoped to userId so a project member only sees their own sessions.
async function listSessionsForProject(projectId: string, userId: string) {
return db return db
.select({ .select({
id: samSessions.id, id: samSessions.id,
@ -31,7 +32,11 @@ async function listSessionsForProject(projectId: string) {
}) })
.from(samSessions) .from(samSessions)
.where( .where(
and(eq(samSessions.projectId, projectId), isNull(samSessions.archivedAt)), and(
eq(samSessions.projectId, projectId),
eq(samSessions.userId, userId),
isNull(samSessions.archivedAt),
),
) )
.orderBy(desc(samSessions.updatedAt), desc(samSessions.id)); .orderBy(desc(samSessions.updatedAt), desc(samSessions.id));
} }
@ -49,16 +54,23 @@ async function getSessionById(id: string) {
return row ?? null; return row ?? null;
} }
// Excludes archived sessions so callers treat them like deleted ones // Look up a caller's own active session by id, scoped to userId so one org
// (connection refused / not archivable) even though the row and DO transcript // member can't act on another's session. Excludes archived sessions so callers
// are kept. Does NOT authorize: callers must check the caller's access to // treat them like deleted ones (connection refused / not archivable) even
// though the row and DO transcript are kept. Callers must still check access to
// row.projectId via the canonical project-access path // row.projectId via the canonical project-access path
// (ProjectRepository.getProjectForOrganization) before acting on the session. // (ProjectRepository.getProjectForOrganization) before acting on the session.
async function getActiveSession(id: string) { async function getActiveSession(id: string, userId: string) {
const [row] = await db const [row] = await db
.select() .select()
.from(samSessions) .from(samSessions)
.where(and(eq(samSessions.id, id), isNull(samSessions.archivedAt))) .where(
and(
eq(samSessions.id, id),
eq(samSessions.userId, userId),
isNull(samSessions.archivedAt),
),
)
.limit(1); .limit(1);
return row ?? null; return row ?? null;
} }

View File

@ -17,7 +17,10 @@ export const listSamSessions = createServerFn({ method: "GET" })
.middleware(requireProjectContext) .middleware(requireProjectContext)
.inputValidator((data: unknown) => projectScopedSchema.parse(data)) .inputValidator((data: unknown) => projectScopedSchema.parse(data))
.handler(async ({ context }) => { .handler(async ({ context }) => {
return SamSessionRepository.listSessionsForProject(context.projectId); return SamSessionRepository.listSessionsForProject(
context.projectId,
context.userId,
);
}); });
// Creates a new SAM chat session and returns its id; the client then opens a DO // Creates a new SAM chat session and returns its id; the client then opens a DO
@ -47,7 +50,10 @@ export const archiveSamSession = createServerFn({ method: "POST" })
.handler(async ({ data, context }) => { .handler(async ({ data, context }) => {
// Authorize against the session's project (the canonical project-access // Authorize against the session's project (the canonical project-access
// path), not the caller's org directly. // path), not the caller's org directly.
const session = await SamSessionRepository.getActiveSession(data.sessionId); const session = await SamSessionRepository.getActiveSession(
data.sessionId,
context.userId,
);
const project = session const project = session
? await ProjectRepository.getProjectForOrganization( ? await ProjectRepository.getProjectForOrganization(
session.projectId, session.projectId,