|
1 | 1 | import type { Principal } from '@sim/auth/principal' |
2 | | -import { isRecordLike } from '@sim/utils/object' |
3 | | -import { authorizeWorkspaceOperation, type WorkspaceOperation } from '@/lib/core/application' |
4 | 2 | import { OrchestrationError } from '@/lib/core/orchestration/types' |
5 | 3 | import type { DbOrTx } from '@/lib/db/types' |
6 | 4 | import { |
7 | 5 | type ActiveWorkspaceFileContext, |
8 | 6 | getWorkspaceFileByVfsPath, |
9 | | - loadActiveWorkspaceContext, |
10 | 7 | loadActiveWorkspaceFileContext, |
11 | 8 | loadWorkspaceFileLifecycleContext, |
12 | 9 | type WorkspaceFileLifecycleContext, |
13 | 10 | } from '@/lib/uploads/contexts/workspace/file-manager' |
14 | | -import { decodeVfsReferenceSegments, isWorkspaceFileVfsPath, VfsPathError } from '@/lib/vfs/path' |
15 | | -import { WORKSPACE_FILES_DELEGATION_AUDIENCE } from '@/lib/workspace-files/application/authorization' |
| 11 | +import { isWorkspaceFileVfsPath, VfsPathError } from '@/lib/vfs/path' |
| 12 | +import { UnresolvedWorkspaceFileVfsPathError } from '@/lib/workspace-files/application/unresolved-vfs-path' |
16 | 13 | import { ownedFileKind } from '@/lib/workspace-files/owned-files' |
17 | 14 | import { matchesFileOwner } from '@/lib/workspace-files/ownership' |
18 | 15 |
|
@@ -81,71 +78,54 @@ function requireWorkspaceFileContext<C extends ActiveWorkspaceFileContext>( |
81 | 78 | return canonical |
82 | 79 | } |
83 | 80 |
|
84 | | -function invalidVfsPathError(vfsPath: string, error: unknown): unknown { |
85 | | - if (!(error instanceof VfsPathError)) return error |
86 | | - return new OrchestrationError( |
87 | | - 'validation', |
88 | | - `"${vfsPath}" is not a valid workspace file path: ${error.message}` |
89 | | - ) |
90 | | -} |
91 | | - |
92 | 81 | /** |
93 | | - * Admits the principal into the asserted workspace before a `files/…` path is looked up by name |
94 | | - * there, so nothing the lookup answers (a missing file, two names that display alike) reaches a |
95 | | - * caller with no access to that workspace. A malformed path is refused first: deciding that reads |
96 | | - * nothing and says nothing about the workspace. Any other input passes untouched. A delegation |
97 | | - * scoped to one file is checked once the path names a file, by the use case's own authorization. |
| 82 | + * A malformed path is decided before any read and says nothing about the workspace, so it stays a |
| 83 | + * plain validation error. The lookup's own refusal (two names that display alike) does not. |
98 | 84 | */ |
99 | | -export async function authorizeWorkspaceFileVfsPath( |
100 | | - principal: Principal, |
101 | | - operation: WorkspaceOperation, |
102 | | - input: unknown |
103 | | -): Promise<void> { |
104 | | - if (!isRecordLike(input)) return |
105 | | - const { fileId: vfsPath, assertedWorkspaceId } = input |
106 | | - if (typeof vfsPath !== 'string' || !isWorkspaceFileVfsPath(vfsPath)) return |
107 | | - if (typeof assertedWorkspaceId !== 'string') return |
108 | | - try { |
109 | | - decodeVfsReferenceSegments(vfsPath.slice('files/'.length)) |
110 | | - } catch (error) { |
111 | | - throw invalidVfsPathError(vfsPath, error) |
| 85 | +function rethrowVfsLookupError(vfsPath: string, workspaceId: string, error: unknown): never { |
| 86 | + if (error instanceof VfsPathError) { |
| 87 | + throw new OrchestrationError( |
| 88 | + 'validation', |
| 89 | + `"${vfsPath}" is not a valid workspace file path: ${error.message}` |
| 90 | + ) |
112 | 91 | } |
113 | | - const workspace = await loadActiveWorkspaceContext(assertedWorkspaceId) |
114 | | - if (!workspace) throw new OrchestrationError('not_found', 'File not found') |
115 | | - await authorizeWorkspaceOperation(principal, operation, workspace, { |
116 | | - delegation: { audience: WORKSPACE_FILES_DELEGATION_AUDIENCE, isWithinScope: () => true }, |
117 | | - }) |
| 92 | + if (error instanceof OrchestrationError && error.code === 'validation') { |
| 93 | + throw new UnresolvedWorkspaceFileVfsPathError(workspaceId, 'validation', error.message) |
| 94 | + } |
| 95 | + throw error |
118 | 96 | } |
119 | 97 |
|
120 | | -/** |
121 | | - * A path becomes an id first, so the canonical load and every authorization check run unchanged. |
122 | | - * Use cases reach this only after {@link authorizeWorkspaceFileVfsPath} admitted the caller. |
123 | | - */ |
| 98 | +/** A path becomes an id first, so the canonical load and every authorization check run unchanged. */ |
124 | 99 | async function resolveFileId(input: WorkspaceFileContextInput): Promise<string> { |
125 | 100 | if (!isWorkspaceFileVfsPath(input.fileId)) return input.fileId |
126 | | - if (input.assertedWorkspaceId === undefined) { |
| 101 | + const workspaceId = input.assertedWorkspaceId |
| 102 | + if (workspaceId === undefined) { |
127 | 103 | throw new OrchestrationError('not_found', 'File not found') |
128 | 104 | } |
129 | | - const file = await getWorkspaceFileByVfsPath(input.assertedWorkspaceId, input.fileId).catch( |
130 | | - (error: unknown) => { |
131 | | - throw invalidVfsPathError(input.fileId, error) |
132 | | - } |
| 105 | + const file = await getWorkspaceFileByVfsPath(workspaceId, input.fileId).catch((error: unknown) => |
| 106 | + rethrowVfsLookupError(input.fileId, workspaceId, error) |
133 | 107 | ) |
134 | | - if (!file) throw new OrchestrationError('not_found', 'File not found') |
| 108 | + if (!file) |
| 109 | + throw new UnresolvedWorkspaceFileVfsPathError(workspaceId, 'not_found', 'File not found') |
135 | 110 | return file.id |
136 | 111 | } |
137 | 112 |
|
138 | 113 | export async function resolveActiveWorkspaceFileContext( |
139 | 114 | input: WorkspaceFileContextInput |
140 | 115 | ): Promise<ActiveWorkspaceFileContext> { |
141 | | - const canonical = requireWorkspaceFileContext( |
142 | | - await loadActiveWorkspaceFileContext(await resolveFileId(input), { |
143 | | - includeDeleted: input.includeDeleted, |
144 | | - includeChatUploads: input.includeChatUploads, |
145 | | - includeOwnedFiles: Boolean(input.ownedFilePrincipal), |
146 | | - }), |
147 | | - input.assertedWorkspaceId |
148 | | - ) |
| 116 | + const loaded = await loadActiveWorkspaceFileContext(await resolveFileId(input), { |
| 117 | + includeDeleted: input.includeDeleted, |
| 118 | + includeChatUploads: input.includeChatUploads, |
| 119 | + includeOwnedFiles: Boolean(input.ownedFilePrincipal), |
| 120 | + }) |
| 121 | + if (!loaded && isWorkspaceFileVfsPath(input.fileId) && input.assertedWorkspaceId) { |
| 122 | + throw new UnresolvedWorkspaceFileVfsPathError( |
| 123 | + input.assertedWorkspaceId, |
| 124 | + 'not_found', |
| 125 | + 'File not found' |
| 126 | + ) |
| 127 | + } |
| 128 | + const canonical = requireWorkspaceFileContext(loaded, input.assertedWorkspaceId) |
149 | 129 | if (input.ownedFilePrincipal) await assertOwnedFileAccess(input.ownedFilePrincipal, canonical) |
150 | 130 | return canonical |
151 | 131 | } |
|
0 commit comments