Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,14 @@ export const PUT = withRouteHandler(
// master on/off and the per-auth-type allow-list); disabling is always
// allowed so users can still un-share after the policy is turned on.
if (isActive) {
// Validate the auth type that will ACTUALLY be persisted. upsertFileShare
// falls back to the existing share's authType when none is passed, so a bare
// re-enable must be checked against that stored mode — not 'public' — or a
// now-disallowed password/email/sso share could be silently reactivated.
const existingShare = await getShareForResource('file', fileId)
const effectiveAuthType = authType ?? existingShare?.authType ?? 'public'
try {
await validatePublicFileSharing(session.user.id, workspaceId, authType ?? 'public')
await validatePublicFileSharing(session.user.id, workspaceId, effectiveAuthType)
} catch (error) {
if (error instanceof PublicFileSharingNotAllowedError) {
logger.warn(`[${requestId}] Public file sharing disabled for workspace ${workspaceId}`)
Expand Down
57 changes: 57 additions & 0 deletions apps/sim/lib/copilot/generated/tool-catalog-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export interface ToolCatalogEntry {
| 'set_block_enabled'
| 'set_environment_variables'
| 'set_global_workflow_variables'
| 'share_file'
| 'superagent'
| 'table'
| 'update_deployment_version'
Expand Down Expand Up @@ -193,6 +194,7 @@ export interface ToolCatalogEntry {
| 'set_block_enabled'
| 'set_environment_variables'
| 'set_global_workflow_variables'
| 'share_file'
| 'superagent'
| 'table'
| 'update_deployment_version'
Expand Down Expand Up @@ -3768,6 +3770,60 @@ export const SetGlobalWorkflowVariables: ToolCatalogEntry = {
requiredPermission: 'write',
}

export const ShareFile: ToolCatalogEntry = {
id: 'share_file',
name: 'share_file',
route: 'sim',
mode: 'async',
parameters: {
type: 'object',
properties: {
action: {
type: 'string',
description: 'Whether to create/update the share link or deactivate it.',
enum: ['share', 'unshare'],
default: 'share',
},
allowedEmails: {
type: 'array',
description:
'Allowed emails or "@domain" patterns for authType "email" or "sso". Ignored for other auth types.',
items: { type: 'string' },
},
authType: {
type: 'string',
description: 'How viewers authenticate to open the link. Ignored for unshare.',
enum: ['public', 'password', 'email', 'sso'],
default: 'public',
},
password: {
type: 'string',
description:
'Password for authType "password". Leave empty to keep the file\'s existing password when re-sharing an already password-protected file. Ignored for other auth types.',
},
path: {
type: 'string',
description: 'Canonical workspace file VFS path to share, e.g. "files/Reports/Q4.md".',
},
},
required: ['path'],
},
resultSchema: {
type: 'object',
properties: {
data: {
type: 'object',
description:
'Share state. Contains url (the {baseUrl}/f/{token} link), token, authType, hasPassword, and isActive.',
},
message: { type: 'string', description: 'Human-readable outcome.' },
success: { type: 'boolean', description: 'Whether the share action succeeded.' },
},
required: ['success', 'message'],
},
requiredPermission: 'write',
}

export const Superagent: ToolCatalogEntry = {
id: 'superagent',
name: 'superagent',
Expand Down Expand Up @@ -4767,6 +4823,7 @@ export const TOOL_CATALOG: Record<string, ToolCatalogEntry> = {
[SetBlockEnabled.id]: SetBlockEnabled,
[SetEnvironmentVariables.id]: SetEnvironmentVariables,
[SetGlobalWorkflowVariables.id]: SetGlobalWorkflowVariables,
[ShareFile.id]: ShareFile,
[Superagent.id]: Superagent,
[Table.id]: Table,
[UpdateDeploymentVersion.id]: UpdateDeploymentVersion,
Expand Down
56 changes: 56 additions & 0 deletions apps/sim/lib/copilot/generated/tool-schemas-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3534,6 +3534,62 @@ export const TOOL_RUNTIME_SCHEMAS: Record<string, ToolRuntimeSchemaEntry> = {
},
resultSchema: undefined,
},
share_file: {
parameters: {
type: 'object',
properties: {
action: {
type: 'string',
description: 'Whether to create/update the share link or deactivate it.',
enum: ['share', 'unshare'],
default: 'share',
},
allowedEmails: {
type: 'array',
description:
'Allowed emails or "@domain" patterns for authType "email" or "sso". Ignored for other auth types.',
items: {
type: 'string',
},
},
authType: {
type: 'string',
description: 'How viewers authenticate to open the link. Ignored for unshare.',
enum: ['public', 'password', 'email', 'sso'],
default: 'public',
},
password: {
type: 'string',
description:
'Password for authType "password". Leave empty to keep the file\'s existing password when re-sharing an already password-protected file. Ignored for other auth types.',
},
path: {
type: 'string',
description: 'Canonical workspace file VFS path to share, e.g. "files/Reports/Q4.md".',
},
},
required: ['path'],
},
resultSchema: {
type: 'object',
properties: {
data: {
type: 'object',
description:
'Share state. Contains url (the {baseUrl}/f/{token} link), token, authType, hasPassword, and isActive.',
},
message: {
type: 'string',
description: 'Human-readable outcome.',
},
success: {
type: 'boolean',
description: 'Whether the share action succeeded.',
},
},
required: ['success', 'message'],
},
},
superagent: {
parameters: {
properties: {
Expand Down
176 changes: 176 additions & 0 deletions apps/sim/lib/copilot/tools/server/files/share-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
import { createLogger } from '@sim/logger'
import type { ShareAuthType } from '@/lib/api/contracts/public-shares'
import { ShareFile } from '@/lib/copilot/generated/tool-catalog-v1'
import { ensureWorkspaceAccess } from '@/lib/copilot/tools/handlers/access'
import {
assertServerToolNotAborted,
type BaseServerTool,
type ServerToolContext,
} from '@/lib/copilot/tools/server/base-tool'
import {
getShareForResource,
ShareValidationError,
upsertFileShare,
} from '@/lib/public-shares/share-manager'
import {
getWorkspaceFile,
resolveWorkspaceFileReference,
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import {
PublicFileSharingNotAllowedError,
validatePublicFileSharing,
} from '@/ee/access-control/utils/permission-check'

const logger = createLogger('ShareFileServerTool')

interface ShareFileArgs {
path?: string
fileId?: string
action?: 'share' | 'unshare'
authType?: ShareAuthType
password?: string
allowedEmails?: string[]
args?: Record<string, unknown>
}

interface ShareFileResult {
success: boolean
message: string
data?: {
url: string
token: string
authType: ShareAuthType
hasPassword: boolean
isActive: boolean
}
}

export const shareFileServerTool: BaseServerTool<ShareFileArgs, ShareFileResult> = {
name: ShareFile.id,
async execute(params: ShareFileArgs, context?: ServerToolContext): Promise<ShareFileResult> {
if (!context?.userId) {
throw new Error('Authentication required')
}
const workspaceId = context.workspaceId
if (!workspaceId) {
return { success: false, message: 'Workspace ID is required' }
}
await ensureWorkspaceAccess(workspaceId, context.userId, 'write')

const nested = params.args
const path = params.path || (nested?.path as string) || ''
const legacyFileId = params.fileId || (nested?.fileId as string) || ''
const action = (params.action || (nested?.action as string) || 'share') as 'share' | 'unshare'
const authType = (params.authType || (nested?.authType as ShareAuthType | undefined)) as
| ShareAuthType
| undefined
const password = params.password || (nested?.password as string) || undefined
const allowedEmails =
params.allowedEmails || (nested?.allowedEmails as string[] | undefined) || undefined

const targetRef = path || legacyFileId
if (!targetRef) return { success: false, message: 'path is required' }

const existingFile = path
? await resolveWorkspaceFileReference(workspaceId, path)
: await getWorkspaceFile(workspaceId, legacyFileId)
if (!existingFile) {
return { success: false, message: `File not found: ${targetRef}` }
}
const fileId = existingFile.id
const isActive = action !== 'unshare'

// Enabling a share is gated by the org's access-control policy (both the
// master on/off and the per-auth-type allow-list); disabling is always
// allowed so users can still un-share after the policy is turned on.
if (isActive) {
// Validate the auth type that will ACTUALLY be persisted. upsertFileShare
// falls back to the existing share's authType when none is passed, so a bare
// re-enable must be checked against that stored mode — not 'public' — or a
// now-disallowed password/email/sso share could be silently reactivated.
const existingShare = await getShareForResource('file', fileId)
const effectiveAuthType = authType ?? existingShare?.authType ?? 'public'
try {
await validatePublicFileSharing(context.userId, workspaceId, effectiveAuthType)
} catch (error) {
if (error instanceof PublicFileSharingNotAllowedError) {
return { success: false, message: error.message }
}
throw error
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

assertServerToolNotAborted(context)

let share
try {
share = await upsertFileShare({
workspaceId,
fileId,
userId: context.userId,
isActive,
authType,
password,
allowedEmails,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unshare clears stored auth type

High Severity

share_file still forwards authType into upsertFileShare on unshare, even though the catalog says it is ignored. upsertFileShare always persists authType, so an unshare that includes the schema default public overwrites a stored password/email/sso mode. A later bare re-enable then restores a public link instead of the prior protection.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9e1e71a. Configure here.

} catch (error) {
if (error instanceof ShareValidationError) {
return { success: false, message: error.message }
}
throw error
}

logger.info(`${isActive ? 'Enabled' : 'Disabled'} share for file via share_file`, {
fileId,
workspaceId,
authType: share.authType,
userId: context.userId,
})

recordAudit({
workspaceId,
actorId: context.userId,
action: isActive ? AuditAction.FILE_SHARED : AuditAction.FILE_SHARE_DISABLED,
resourceType: AuditResourceType.FILE,
resourceId: fileId,
resourceName: existingFile.name,
description: `${isActive ? 'Enabled' : 'Disabled'} public share for "${existingFile.name}"`,
})

if (!isActive) {
return {
success: true,
message: `Stopped sharing "${existingFile.name}". The previous link no longer works.`,
data: {
url: share.url,
token: share.token,
authType: share.authType,
hasPassword: share.hasPassword,
isActive: share.isActive,
},
}
}

const authNote =
share.authType === 'password'
? ' (password-protected — share the password separately)'
: share.authType === 'email'
? ' (restricted to allowed emails via one-time code)'
: share.authType === 'sso'
? ' (restricted to allowed emails via SSO)'
: ''

return {
success: true,
message: `Shared "${existingFile.name}"${authNote}: ${share.url}`,
data: {
url: share.url,
token: share.token,
authType: share.authType,
hasPassword: share.hasPassword,
isActive: share.isActive,
},
}
},
}
3 changes: 3 additions & 0 deletions apps/sim/lib/copilot/tools/server/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
renameFileFolderServerTool,
} from '@/lib/copilot/tools/server/files/file-folders'
import { renameFileServerTool } from '@/lib/copilot/tools/server/files/rename-file'
import { shareFileServerTool } from '@/lib/copilot/tools/server/files/share-file'
import { workspaceFileServerTool } from '@/lib/copilot/tools/server/files/workspace-file'
import { validateGeneratedToolPayload } from '@/lib/copilot/tools/server/generated-schema'
import { generateImageServerTool } from '@/lib/copilot/tools/server/image/generate-image'
Expand Down Expand Up @@ -132,6 +133,7 @@ const WRITE_ACTIONS: Record<string, string[]> = {
[CreateFile.id]: ['*'],
[RenameFile.id]: ['*'],
[DeleteFile.id]: ['*'],
[shareFileServerTool.name]: ['*'],
[MoveFile.id]: ['*'],
[CreateFileFolder.id]: ['*'],
[RenameFileFolder.id]: ['*'],
Expand Down Expand Up @@ -177,6 +179,7 @@ const baseServerToolRegistry: Record<string, BaseServerTool> = {
[createFileServerTool.name]: createFileServerTool,
[renameFileServerTool.name]: renameFileServerTool,
[deleteFileServerTool.name]: deleteFileServerTool,
[shareFileServerTool.name]: shareFileServerTool,
[moveFileServerTool.name]: moveFileServerTool,
[listFileFoldersServerTool.name]: listFileFoldersServerTool,
[createFileFolderServerTool.name]: createFileFolderServerTool,
Expand Down
10 changes: 10 additions & 0 deletions apps/sim/lib/copilot/vfs/serializers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { truncate } from '@sim/utils/string'
import type { ShareAuthType } from '@/lib/api/contracts/public-shares'
import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions'
import { isHosted } from '@/lib/core/config/env-flags'
import { isSubBlockHidden } from '@/lib/workflows/subblocks/visibility'
Expand Down Expand Up @@ -291,6 +292,12 @@ export function serializeFileMeta(file: {
contentType: string
size: number
uploadedAt: Date
/** Whether the file has an active public share link. */
shared?: boolean
/** Auth mode of the active share; only meaningful when `shared` is true. */
shareAuthType?: ShareAuthType
/** Public share link (`{baseUrl}/f/{token}`); only meaningful when `shared` is true. */
shareUrl?: string
}): string {
return JSON.stringify(
{
Expand All @@ -303,6 +310,9 @@ export function serializeFileMeta(file: {
size: file.size,
uploadedAt: file.uploadedAt.toISOString(),
readContentWith: file.vfsPath ? `${file.vfsPath}/content` : undefined,
shared: Boolean(file.shared),
shareAuthType: file.shared ? file.shareAuthType : undefined,
shareUrl: file.shared ? file.shareUrl : undefined,
note: 'This is file metadata only. To read the file text/bytes, read the readContentWith path (i.e. append /content).',
},
null,
Expand Down
Loading
Loading