fix(ai): don't require admin ability to resolve org default model in Slack#25265
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
🧪 Test Selection✅ Tests that will run
⏭️ Tests skipped (no relevant file changes detected)
|
| async getDefaultModelConfig( | ||
| organizationUuid: string, | ||
| ): Promise<AiAgentModelConfig | null> { | ||
| const settings = | ||
| await this.aiOrganizationSettingsModel.findByOrganizationUuid( | ||
| organizationUuid, | ||
| ); | ||
| return settings?.defaultAiAgentModelConfig ?? null; | ||
| } |
There was a problem hiding this comment.
The backend rules require that all service methods have permission checks. getDefaultModelConfig reads org-level settings (which may include sensitive model configuration) but performs no authorization check whatsoever — any caller with an organizationUuid can retrieve this data. Even if the returned value is non-secret, the method should at minimum verify the caller belongs to the organization (e.g. check organizationUuid matches the caller's org) to prevent cross-org data leakage (IDOR). Consider adding a lightweight membership/ownership check, or at minimum document and enforce that callers are already scoped to the correct org.
| async getDefaultModelConfig( | |
| organizationUuid: string, | |
| ): Promise<AiAgentModelConfig | null> { | |
| const settings = | |
| await this.aiOrganizationSettingsModel.findByOrganizationUuid( | |
| organizationUuid, | |
| ); | |
| return settings?.defaultAiAgentModelConfig ?? null; | |
| } | |
| async getDefaultModelConfig( | |
| user: SessionUser, | |
| organizationUuid: string, | |
| ): Promise<AiAgentModelConfig | null> { | |
| if (user.organizationUuid !== organizationUuid) { | |
| throw new ForbiddenError( | |
| 'You do not have permission to access this organization settings', | |
| ); | |
| } | |
| const settings = | |
| await this.aiOrganizationSettingsModel.findByOrganizationUuid( | |
| organizationUuid, | |
| ); | |
| return settings?.defaultAiAgentModelConfig ?? null; | |
| } | |
Spotted by Graphite (based on custom rule: packages/backend rules)
Is this helpful? React 👍 or 👎 to let us know.
…Slack createSlackPrompt resolved the org default model config by calling AiOrganizationSettingsService.getSettings with a user built from getUserDetailsByUuid (a LightdashUser with no CASL ability) cast as SessionUser. A permission check added to getSettings (#25194) then hit createAuditedAbility on that ability-less user, throwing 'Cannot read properties of undefined (reading can)' and crashing every Slack @mention that fell back to the org default model. Split the non-secret default model config read into an ungated getDefaultModelConfig(organizationUuid) and use it from the Slack path. getSettings keeps its admin-gated provider-key-hint masking. Using the org model (no secret) is separated from seeing/changing the key. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xxRosT4vB4fizzmJAQmE2
539b149 to
9212fd4
Compare
0026e30 to
bbfc73d
Compare
|
Your preview environment pr-25265 has been deployed. |
Preview Environment🌐 URL: https://lightdash-preview-pr-25265.lightdash.okteto.dev 📋 Logs: View in GCP Console 🔧 SSH: |
|
🎉 This PR is included in version 0.3328.1 🎉 The release is available on:
Your semantic-release bot 📦🚀 |

Problem
Every Slack
@mentionof an AI agent that falls back to the org default model crashes with:createSlackPromptresolves the default model by callingAiOrganizationSettingsService.getSettings, passing a user built fromuserModel.getUserDetailsByUuid— aLightdashUser, which has no CASLability— cast asSessionUser. Theas SessionUsercast hid the mismatch.getSettingsgained a permission check in #25194 (canManageAiAgent→createAuditedAbility(user)), which dereferencesuser.ability. On the ability-less Slack user that isundefined, sowrappedAbility.can(...)throws.Fix
The Slack path only needs the non-secret model selection (
{ modelName, modelProvider, reasoning }) — it does not need the admin-facing settings payload or its provider-key-hint masking. So this separates the two concerns:getDefaultModelConfig(organizationUuid), ungated (no ability, nomanagepermission). The Slack flow uses this.getSettingskeeps its admin-gatedmaskProviderKeyExposure(…, canManage);upsertSettingskeepscheckManageAiAgentAccess.This also removes the
as SessionUserlie — the internal flow no longer masquerades as an interactive user.The model config carries no secret (the API key lives in
providerApiKeys, resolved server-side), so exposing it ungated is safe: anyone can use the admin's key via the agent; only admins can see/change it.Regression analysis
@mentionwhere the agent uses the org default model — i.e. neither the request (data.modelConfig) nor the agent (agent.modelConfig) pins a model. Introduced by feat(backend): org-aware AI model options and provider key settings #25194 (merged the same day).modelConfig(they skip the settings read entirely); the web-app prompt path and MCP path, which pass a realSessionUserwith a populatedability.Test plan
pnpm -F backend typecheck:fastAiOrganizationSettingsService.test.tspasses (7/7)pnpm -F backend lintclean on changed files@mentionan agent (no per-agent/per-request model) in Slack and confirm it responds instead of crashingNotes
Relates: #25194