fix(api): call threads:create auth handler when copying a thread (#1641)

This commit is contained in:
Parker J. Rule
2025-09-10 19:22:37 -04:00
committed by GitHub
parent 43a5573c04
commit 68a1aa8123
4 changed files with 58 additions and 5 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@langchain/langgraph-api": patch
---
fix(api): call threads:create auth handler when copying a thread
+13 -4
View File
@@ -908,7 +908,7 @@ export class FileSystemThreads implements ThreadsRepo {
thread_id,
});
return this.conn.with((STORE) => {
const fromThread = await this.conn.with((STORE) => {
const thread = STORE.threads[thread_id];
if (!thread)
throw new HTTPException(409, { message: "Thread not found" });
@@ -916,14 +916,23 @@ export class FileSystemThreads implements ThreadsRepo {
if (!isAuthMatching(thread["metadata"], filters)) {
throw new HTTPException(409, { message: "Thread not found" });
}
return thread;
});
const newThreadId = uuid4();
const now = new Date();
const newThreadId = uuid4();
const now = new Date();
const newMetadata = { ...fromThread.metadata, thread_id: newThreadId };
await handleAuthEvent(auth, "threads:create", {
thread_id: newThreadId,
metadata: newMetadata,
});
return this.conn.with((STORE) => {
STORE.threads[newThreadId] = {
thread_id: newThreadId,
created_at: now,
updated_at: now,
metadata: { ...thread.metadata, thread_id: newThreadId },
metadata: newMetadata,
config: {},
status: "idle",
};
+26 -1
View File
@@ -684,7 +684,7 @@ it.skipIf(process.version.startsWith("v18."))(
);
it.skipIf(process.version.startsWith("v18."))(
"thread copy authorization",
"thread copy authorization (by owner)",
async () => {
const owner = await createJwtClient("johndoe", ["me"]);
const otherUser = await createJwtClient("alice", ["me"]);
@@ -702,6 +702,31 @@ it.skipIf(process.version.startsWith("v18."))(
}
);
it.skipIf(process.version.startsWith("v18."))(
"thread copy authorization (requires read and write)",
async () => {
const writer = await createJwtClient("johndoe", ["me"]);
const reader = await createJwtClient("johndoe", ["threads:read"]);
const thread = await writer.threads.create();
// Can read the thread from both...
const readerThread = await reader.threads.get(thread.thread_id);
const writerThread = await writer.threads.get(thread.thread_id);
expect(thread).toEqual(readerThread);
expect(thread).toEqual(writerThread);
// But can only copy the thread from the writer
const copiedThread = await writer.threads.copy(thread.thread_id);
expect(copiedThread).not.toBeNull();
expect(copiedThread.thread_id).not.toBe(thread.thread_id);
await expect(reader.threads.copy(thread.thread_id)).rejects.toThrow(
"HTTP 403: Not authorized"
);
}
);
it.skipIf(process.version.startsWith("v18."))(
"thread history authorization",
async () => {
+14
View File
@@ -103,4 +103,18 @@ export const auth = new Auth()
if (!identity || !value.namespace?.includes(identity)) {
throw new HTTPException(403, { message: "Not authorized" });
}
})
.on("threads:create", ({ value, user, permissions }) => {
if (
!["me", "threads:write", "threads:*"].some((p) =>
permissions?.includes(p)
)
) {
throw new HTTPException(403, { message: "Not authorized" });
}
const filters = { owner: user.identity };
value.metadata ??= {};
value.metadata["owner"] = user.identity;
return filters;
});