fix(credentials): block usage at execution layer without perms + fix invites

This commit is contained in:
Vikhyath Mondreti
2026-03-09 19:22:35 -07:00
parent 8abe717b85
commit d23afb97c5
3 changed files with 80 additions and 25 deletions

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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') {