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 {
return new Response("Unauthorized", { status: 401 });
}
const session = await SamSessionRepository.getActiveSession(sessionId);
const session = await SamSessionRepository.getActiveSession(
sessionId,
context.userId,
);
const project = session
? await ProjectRepository.getProjectForOrganization(
session.projectId,

View File

@ -21,7 +21,8 @@ async function createSession(input: CreateSamSessionInput) {
}
// 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
.select({
id: samSessions.id,
@ -31,7 +32,11 @@ async function listSessionsForProject(projectId: string) {
})
.from(samSessions)
.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));
}
@ -49,16 +54,23 @@ async function getSessionById(id: string) {
return row ?? null;
}
// Excludes archived sessions so callers treat them like deleted ones
// (connection refused / not archivable) even though the row and DO transcript
// are kept. Does NOT authorize: callers must check the caller's access to
// Look up a caller's own active session by id, scoped to userId so one org
// member can't act on another's session. Excludes archived sessions so callers
// 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
// (ProjectRepository.getProjectForOrganization) before acting on the session.
async function getActiveSession(id: string) {
async function getActiveSession(id: string, userId: string) {
const [row] = await db
.select()
.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);
return row ?? null;
}

View File

@ -17,7 +17,10 @@ export const listSamSessions = createServerFn({ method: "GET" })
.middleware(requireProjectContext)
.inputValidator((data: unknown) => projectScopedSchema.parse(data))
.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
@ -47,7 +50,10 @@ export const archiveSamSession = createServerFn({ method: "POST" })
.handler(async ({ data, context }) => {
// Authorize against the session's project (the canonical project-access
// 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
? await ProjectRepository.getProjectForOrganization(
session.projectId,