mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix(credentials): block usage at execution layer without perms + fix invites
This commit is contained in:
@@ -200,7 +200,7 @@ export default function Invite() {
|
||||
}, [searchParams, inviteId])
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.user || !token) return
|
||||
if (!session?.user) return
|
||||
|
||||
async function fetchInvitationDetails() {
|
||||
setIsLoading(true)
|
||||
@@ -301,7 +301,7 @@ export default function Invite() {
|
||||
}
|
||||
|
||||
fetchInvitationDetails()
|
||||
}, [session?.user, inviteId, token])
|
||||
}, [session?.user, inviteId])
|
||||
|
||||
const handleAcceptInvitation = async () => {
|
||||
if (!session?.user) return
|
||||
|
||||
@@ -107,6 +107,9 @@ async function fetchOAuthConnections(signal?: AbortSignal): Promise<ServiceInfo[
|
||||
|
||||
return updatedServices
|
||||
} catch (error) {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
return defineServices()
|
||||
}
|
||||
logger.error('Error fetching OAuth connections:', error)
|
||||
return defineServices()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type {
|
||||
ExecutionContext,
|
||||
ToolCallResult,
|
||||
@@ -8,6 +9,8 @@ import type {
|
||||
} from '@/lib/copilot/orchestrator/types'
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getCredentialActorContext } from '@/lib/credentials/access'
|
||||
import { getAccessibleOAuthCredentials } from '@/lib/credentials/environment'
|
||||
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
|
||||
import { getWorkflowById } from '@/lib/workflows/utils'
|
||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
@@ -16,6 +19,8 @@ import { executeTool } from '@/tools'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
import { resolveToolId } from '@/tools/utils'
|
||||
|
||||
const logger = createLogger('CopilotIntegrationTools')
|
||||
|
||||
export async function executeIntegrationToolDirect(
|
||||
toolCall: ToolCallState,
|
||||
toolConfig: ToolConfig,
|
||||
@@ -34,40 +39,86 @@ export async function executeIntegrationToolDirect(
|
||||
const decryptedEnvVars =
|
||||
context.decryptedEnvVars || (await getEffectiveDecryptedEnv(userId, workspaceId))
|
||||
|
||||
// Deep resolution walks nested objects to replace {{ENV_VAR}} references.
|
||||
// Safe because tool arguments originate from the LLM (not direct user input)
|
||||
// and env vars belong to the user themselves.
|
||||
const executionParams = resolveEnvVarReferences(toolArgs, decryptedEnvVars, {
|
||||
deep: true,
|
||||
}) as Record<string, unknown>
|
||||
|
||||
// If the LLM passed a credential/oauthCredential ID directly, verify the user
|
||||
// has active credential_member access before proceeding. This prevents
|
||||
// unauthorized credential usage even if the agent hallucinated or received
|
||||
// a credential ID the user doesn't have access to.
|
||||
const suppliedCredentialId = (executionParams.oauthCredential || executionParams.credential) as
|
||||
| string
|
||||
| undefined
|
||||
if (suppliedCredentialId) {
|
||||
const actorCtx = await getCredentialActorContext(suppliedCredentialId, userId)
|
||||
if (!actorCtx.member) {
|
||||
logger.warn('Blocked credential use: user lacks credential_member access', {
|
||||
credentialId: suppliedCredentialId,
|
||||
userId,
|
||||
toolName,
|
||||
})
|
||||
return {
|
||||
success: false,
|
||||
error: `You do not have access to credential "${suppliedCredentialId}". Ask the credential admin to add you as a member, or connect your own account.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toolConfig.oauth?.required && toolConfig.oauth.provider) {
|
||||
const provider = toolConfig.oauth.provider
|
||||
const accounts = await db
|
||||
.select()
|
||||
.from(account)
|
||||
.where(and(eq(account.providerId, provider), eq(account.userId, userId)))
|
||||
.limit(1)
|
||||
|
||||
if (!accounts.length) {
|
||||
return {
|
||||
success: false,
|
||||
error: `No ${provider} account connected. Please connect your account first.`,
|
||||
// If the user already supplied a credential ID that passed the check above,
|
||||
// skip auto-resolution and let executeTool handle it via the token endpoint.
|
||||
if (!suppliedCredentialId) {
|
||||
if (!workspaceId) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Cannot resolve ${provider} credential without a workspace context.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const acc = accounts[0]
|
||||
const requestId = generateRequestId()
|
||||
const { accessToken } = await refreshTokenIfNeeded(requestId, acc, acc.id)
|
||||
const accessibleCreds = await getAccessibleOAuthCredentials(workspaceId, userId)
|
||||
const match = accessibleCreds.find((c) => c.providerId === provider)
|
||||
|
||||
if (!accessToken) {
|
||||
return {
|
||||
success: false,
|
||||
error: `OAuth token not available for ${provider}. Please reconnect your account.`,
|
||||
if (!match) {
|
||||
return {
|
||||
success: false,
|
||||
error: `No accessible ${provider} account found. You either don't have a ${provider} account connected in this workspace, or you don't have access to the existing one. Please connect your own account.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executionParams.accessToken = accessToken
|
||||
// Resolve the credential to its underlying account for token refresh
|
||||
const matchCtx = await getCredentialActorContext(match.id, userId)
|
||||
const accountId = matchCtx.credential?.accountId
|
||||
if (!accountId) {
|
||||
return {
|
||||
success: false,
|
||||
error: `OAuth account for ${provider} not found. Please reconnect your account.`,
|
||||
}
|
||||
}
|
||||
|
||||
const [acc] = await db.select().from(account).where(eq(account.id, accountId)).limit(1)
|
||||
|
||||
if (!acc) {
|
||||
return {
|
||||
success: false,
|
||||
error: `OAuth account for ${provider} not found. Please reconnect your account.`,
|
||||
}
|
||||
}
|
||||
|
||||
const requestId = generateRequestId()
|
||||
const { accessToken } = await refreshTokenIfNeeded(requestId, acc, acc.id)
|
||||
|
||||
if (!accessToken) {
|
||||
return {
|
||||
success: false,
|
||||
error: `OAuth token not available for ${provider}. Please reconnect your account.`,
|
||||
}
|
||||
}
|
||||
|
||||
executionParams.accessToken = accessToken
|
||||
}
|
||||
}
|
||||
|
||||
const hasHostedKeySupport = isHosted && !!toolConfig.hosting
|
||||
@@ -82,6 +133,7 @@ export async function executeIntegrationToolDirect(
|
||||
workflowId,
|
||||
workspaceId,
|
||||
userId,
|
||||
enforceCredentialAccess: true,
|
||||
}
|
||||
|
||||
if (toolName === 'function_execute') {
|
||||
|
||||
Reference in New Issue
Block a user