@@ -28,6 +28,21 @@ interface AuthorizationResult {
2828 workspaceId ?: string
2929}
3030
31+ type WorkspacePermission = 'read' | 'write' | 'admin'
32+
33+ /**
34+ * Whether a resolved workspace permission satisfies a file operation. Read and
35+ * download paths accept any membership; destructive operations (`requireWrite`)
36+ * require write or admin, matching the permission needed to create the file.
37+ */
38+ function workspacePermissionSatisfies (
39+ permission : WorkspacePermission | null ,
40+ requireWrite : boolean
41+ ) : boolean {
42+ if ( permission === null ) return false
43+ return requireWrite ? permission === 'write' || permission === 'admin' : true
44+ }
45+
3146/**
3247 * Lookup workspace file by storage key from database
3348 * @param key Storage key to lookup
@@ -117,11 +132,13 @@ export async function verifyFileAccess(
117132 userId : string ,
118133 customConfig ?: StorageConfig ,
119134 context ?: StorageContext | 'general' ,
120- isLocal ?: boolean
135+ isLocal ?: boolean ,
136+ options ?: { requireWrite ?: boolean }
121137) : Promise < boolean > {
138+ const requireWrite = options ?. requireWrite ?? false
122139 try {
123140 if ( context === 'general' ) {
124- return await verifyRegularFileAccess ( cloudKey , userId , customConfig , isLocal )
141+ return await verifyRegularFileAccess ( cloudKey , userId , customConfig , isLocal , requireWrite )
125142 }
126143
127144 // Infer context from key if not explicitly provided
@@ -139,12 +156,12 @@ export async function verifyFileAccess(
139156
140157 // 1. Workspace / mothership files: Check database first (most reliable for both local and cloud)
141158 if ( inferredContext === 'workspace' || inferredContext === 'mothership' ) {
142- return await verifyWorkspaceFileAccess ( cloudKey , userId , customConfig , isLocal )
159+ return await verifyWorkspaceFileAccess ( cloudKey , userId , customConfig , isLocal , requireWrite )
143160 }
144161
145162 // 2. Execution files: workspace_id/workflow_id/execution_id/filename
146163 if ( inferredContext === 'execution' ) {
147- return await verifyExecutionFileAccess ( cloudKey , userId , customConfig )
164+ return await verifyExecutionFileAccess ( cloudKey , userId , customConfig , requireWrite )
148165 }
149166
150167 // 3. Copilot files: Check database first, then metadata, then path pattern (legacy)
@@ -159,12 +176,12 @@ export async function verifyFileAccess(
159176
160177 // 5. Chat files: chat/filename
161178 if ( inferredContext === 'chat' ) {
162- return await verifyChatFileAccess ( cloudKey , userId , customConfig )
179+ return await verifyChatFileAccess ( cloudKey , userId , customConfig , requireWrite )
163180 }
164181
165182 // 6. Regular uploads: UUID-filename or timestamp-filename
166183 // Check metadata for userId/workspaceId, or database for workspace files
167- return await verifyRegularFileAccess ( cloudKey , userId , customConfig , isLocal )
184+ return await verifyRegularFileAccess ( cloudKey , userId , customConfig , isLocal , requireWrite )
168185 } catch ( error ) {
169186 logger . error ( 'Error verifying file access:' , { cloudKey, userId, error } )
170187 // Deny access on error to be safe
@@ -180,7 +197,8 @@ async function verifyWorkspaceFileAccess(
180197 cloudKey : string ,
181198 userId : string ,
182199 customConfig ?: StorageConfig ,
183- isLocal ?: boolean
200+ isLocal ?: boolean ,
201+ requireWrite = false
184202) : Promise < boolean > {
185203 try {
186204 const anyWorkspaceFileRecord = await getFileMetadataByKey ( cloudKey , 'workspace' , {
@@ -202,7 +220,7 @@ async function verifyWorkspaceFileAccess(
202220 'workspace' ,
203221 workspaceFileRecord . workspaceId
204222 )
205- if ( permission !== null ) {
223+ if ( workspacePermissionSatisfies ( permission , requireWrite ) ) {
206224 logger . debug ( 'Workspace file access granted (database lookup)' , {
207225 userId,
208226 workspaceId : workspaceFileRecord . workspaceId ,
@@ -225,7 +243,7 @@ async function verifyWorkspaceFileAccess(
225243
226244 if ( workspaceId ) {
227245 const permission = await getUserEntityPermissions ( userId , 'workspace' , workspaceId )
228- if ( permission !== null ) {
246+ if ( workspacePermissionSatisfies ( permission , requireWrite ) ) {
229247 logger . debug ( 'Workspace file access granted (metadata)' , {
230248 userId,
231249 workspaceId,
@@ -257,7 +275,8 @@ async function verifyWorkspaceFileAccess(
257275async function verifyExecutionFileAccess (
258276 cloudKey : string ,
259277 userId : string ,
260- customConfig ?: StorageConfig
278+ customConfig ?: StorageConfig ,
279+ requireWrite = false
261280) : Promise < boolean > {
262281 const parts = cloudKey . split ( '/' )
263282
@@ -285,7 +304,7 @@ async function verifyExecutionFileAccess(
285304 }
286305
287306 const permission = await getUserEntityPermissions ( userId , 'workspace' , workspaceId )
288- if ( permission === null ) {
307+ if ( ! workspacePermissionSatisfies ( permission , requireWrite ) ) {
289308 logger . warn ( 'User does not have workspace access for execution file' , {
290309 userId,
291310 workspaceId,
@@ -502,7 +521,8 @@ export async function verifyKBFileWriteAccess(cloudKey: string, userId: string):
502521async function verifyChatFileAccess (
503522 cloudKey : string ,
504523 userId : string ,
505- customConfig ?: StorageConfig
524+ customConfig ?: StorageConfig ,
525+ requireWrite = false
506526) : Promise < boolean > {
507527 try {
508528 const config : StorageConfig = customConfig || ( await getChatStorageConfig ( ) )
@@ -516,7 +536,7 @@ async function verifyChatFileAccess(
516536 }
517537
518538 const permission = await getUserEntityPermissions ( userId , 'workspace' , workspaceId )
519- if ( permission === null ) {
539+ if ( ! workspacePermissionSatisfies ( permission , requireWrite ) ) {
520540 logger . warn ( 'User does not have workspace access for chat file' , {
521541 userId,
522542 workspaceId,
@@ -542,7 +562,8 @@ async function verifyRegularFileAccess(
542562 cloudKey : string ,
543563 userId : string ,
544564 customConfig ?: StorageConfig ,
545- isLocal ?: boolean
565+ isLocal ?: boolean ,
566+ requireWrite = false
546567) : Promise < boolean > {
547568 try {
548569 // Priority 1: Check if this might be a workspace file (check database)
@@ -554,7 +575,7 @@ async function verifyRegularFileAccess(
554575 'workspace' ,
555576 workspaceFileRecord . workspaceId
556577 )
557- if ( permission !== null ) {
578+ if ( workspacePermissionSatisfies ( permission , requireWrite ) ) {
558579 logger . debug ( 'Regular file access granted (workspace file from database)' , {
559580 userId,
560581 workspaceId : workspaceFileRecord . workspaceId ,
@@ -589,7 +610,7 @@ async function verifyRegularFileAccess(
589610 // If file has workspaceId, verify workspace membership
590611 if ( workspaceId ) {
591612 const permission = await getUserEntityPermissions ( userId , 'workspace' , workspaceId )
592- if ( permission !== null ) {
613+ if ( workspacePermissionSatisfies ( permission , requireWrite ) ) {
593614 logger . debug ( 'Regular file access granted (workspace membership)' , {
594615 userId,
595616 workspaceId,
0 commit comments