|
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' |
2 | 4 | import { OrchestrationError } from '@/lib/core/orchestration/types' |
3 | 5 | import type { DbOrTx } from '@/lib/db/types' |
4 | 6 | import { |
5 | 7 | type ActiveWorkspaceFileContext, |
6 | 8 | getWorkspaceFileByVfsPath, |
| 9 | + loadActiveWorkspaceContext, |
7 | 10 | loadActiveWorkspaceFileContext, |
8 | 11 | loadWorkspaceFileLifecycleContext, |
9 | 12 | type WorkspaceFileLifecycleContext, |
10 | 13 | } from '@/lib/uploads/contexts/workspace/file-manager' |
11 | | -import { isWorkspaceFileVfsPath, VfsPathError } from '@/lib/vfs/path' |
| 14 | +import { decodeVfsReferenceSegments, isWorkspaceFileVfsPath, VfsPathError } from '@/lib/vfs/path' |
| 15 | +import { WORKSPACE_FILES_DELEGATION_AUDIENCE } from '@/lib/workspace-files/application/authorization' |
12 | 16 | import { ownedFileKind } from '@/lib/workspace-files/owned-files' |
13 | 17 | import { matchesFileOwner } from '@/lib/workspace-files/ownership' |
14 | 18 |
|
@@ -77,19 +81,54 @@ function requireWorkspaceFileContext<C extends ActiveWorkspaceFileContext>( |
77 | 81 | return canonical |
78 | 82 | } |
79 | 83 |
|
80 | | -/** A path becomes an id first, so the canonical load and every authorization check run unchanged. */ |
| 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 | +/** |
| 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. |
| 98 | + */ |
| 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) |
| 112 | + } |
| 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 | + }) |
| 118 | +} |
| 119 | + |
| 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 | + */ |
81 | 124 | async function resolveFileId(input: WorkspaceFileContextInput): Promise<string> { |
82 | 125 | if (!isWorkspaceFileVfsPath(input.fileId)) return input.fileId |
83 | 126 | if (input.assertedWorkspaceId === undefined) { |
84 | 127 | throw new OrchestrationError('not_found', 'File not found') |
85 | 128 | } |
86 | 129 | const file = await getWorkspaceFileByVfsPath(input.assertedWorkspaceId, input.fileId).catch( |
87 | 130 | (error: unknown) => { |
88 | | - if (!(error instanceof VfsPathError)) throw error |
89 | | - throw new OrchestrationError( |
90 | | - 'validation', |
91 | | - `"${input.fileId}" is not a valid workspace file path: ${error.message}` |
92 | | - ) |
| 131 | + throw invalidVfsPathError(input.fileId, error) |
93 | 132 | } |
94 | 133 | ) |
95 | 134 | if (!file) throw new OrchestrationError('not_found', 'File not found') |
|
0 commit comments