Skip to content

Commit 5eaf4a5

Browse files
committed
fix(credentials): decrypt Microsoft app secrets only on a token miss and scope the ADX operation
- Token cache key is named before decrypting, so a cached token skips the decrypt and parse; Azure Data Explorer authorization no longer decrypts, and its cluster key includes the resource's clouds. - The ADX operation re-checks the credential against the run's workspace or organization, as the AWS operation path does. - A credential in the wrong Microsoft cloud is refused as cloud_mismatch with its own message instead of "reconnect the credential"; ADX tool errors come from the refusal code, not log detail. - The operation-minted service comes from the shared ADX credential config. - Reconnect reopens a national-cloud credential on its stored cloud. - China signs in at login.partner.microsoftonline.cn (Entra national-cloud reference) for both ADX auth paths; Kusto authorities derive from MICROSOFT_CLOUDS. - Drop the one-line ADX operations passthrough; refresh TSDoc and the Power BI description.
1 parent ce9dc53 commit 5eaf4a5

20 files changed

Lines changed: 413 additions & 156 deletions

File tree

‎apps/docs/content/docs/integrations/microsoft-app-credentials.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Every option resolves on Sim's servers at run time, and tokens never appear in b
4747

4848
## National clouds [#national-clouds]
4949

50-
Choose the **Cloud** the app is registered in: **Global** (which includes Microsoft 365 GCC), **US Government (GCC High)**, **US Government (DoD)**, or **China (operated by 21Vianet)**. Each is a separate Microsoft Entra deployment, and tokens from one cloud don't work in another. Sim signs in at that cloud's endpoint (`login.microsoftonline.com`, `login.microsoftonline.us`, or `login.chinacloudapi.cn`) and checks the app against that cloud's Microsoft Graph when you save.
50+
Choose the **Cloud** the app is registered in: **Global** (which includes Microsoft 365 GCC), **US Government (GCC High)**, **US Government (DoD)**, or **China (operated by 21Vianet)**. Each is a separate Microsoft Entra deployment, and tokens from one cloud don't work in another. Sim signs in at that cloud's endpoint (`login.microsoftonline.com`, `login.microsoftonline.us`, or `login.partner.microsoftonline.cn`) and checks the app against that cloud's Microsoft Graph when you save.
5151

5252
Only **Azure Data Explorer** runs outside the global cloud: a cluster in `kusto.usgovcloudapi.net` takes a US Government credential, and one in `kusto.chinacloudapi.cn` a China credential. The Azure AD, SharePoint, Intune, Power BI, Dataverse, and Dynamics 365 blocks call global-cloud endpoints, so they refuse a national-cloud credential, and a Dataverse environment URL can be saved only on a global credential.
5353

‎apps/docs/content/docs/integrations/powerbi.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ Sim requests these scopes when someone connects a Power BI account. For a self-h
2929

3030
## Use the block [#usage-instructions]
3131

32-
Connect a home-tenant organizational account through the Power BI Microsoft connection, or run as an app registration with a Microsoft app credential, to list named workspaces, inspect reports and semantic models, execute DAX queries, request standard refreshes, and read refresh history. Microsoft Graph connections cannot substitute for this delegated OAuth connection. An app credential needs the Power BI tenant setting that allows service principals to use Power BI APIs and its service principal added to each workspace as a Member or Admin; DAX queries do not run as a service principal against semantic models with row-level security or SSO. Self-hosted installations use the shared Microsoft app registration and register /api/auth/oauth2/callback/microsoft-powerbi as a Web redirect URI. Workspace listing supports a result limit, offset, and OData filter; reports and semantic models return flat lists. Read-only models may contain only their ID and name, and paginated reports may omit their semantic-model ID. Missing optional fields are null and do not establish permission or feature availability. Keep results small with summaries or TOPN: provider JSON reads are limited to 20 MiB, and workflow tool responses have a stricter 10 MiB inline budget. My Workspace, cross-tenant guest configuration, Credential Groups, impersonation, enhanced refresh, report exports, dashboards, and triggers are outside this version.
32+
Connect a home-tenant organizational account through the Power BI Microsoft connection, or run as an app registration with a Microsoft app credential, to list named workspaces, inspect reports and semantic models, execute DAX queries, request standard refreshes, and read refresh history. A Microsoft Graph connection cannot substitute for the Power BI connection or app credential. An app credential needs the Power BI tenant setting that allows service principals to use Power BI APIs and its service principal added to each workspace as a Member or Admin; DAX queries do not run as a service principal against semantic models with row-level security or SSO. Self-hosted installations use the shared Microsoft app registration and register /api/auth/oauth2/callback/microsoft-powerbi as a Web redirect URI. Workspace listing supports a result limit, offset, and OData filter; reports and semantic models return flat lists. Read-only models may contain only their ID and name, and paginated reports may omit their semantic-model ID. Missing optional fields are null and do not establish permission or feature availability. Keep results small with summaries or TOPN: provider JSON reads are limited to 20 MiB, and workflow tool responses have a stricter 10 MiB inline budget. My Workspace, cross-tenant guest configuration, Credential Groups, impersonation, enhanced refresh, report exports, dashboards, and triggers are outside this version.
3333

3434
## Actions
3535

‎apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/keyless-connection.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ export function useKeylessSetup<P extends WorkloadIdentityProvider>({
7373
available: workspaceScoped ? setup.data?.available : false,
7474
/** The stored mode of the credential being reconnected; a reconnect reopens on it. */
7575
reconnectAuthType,
76+
/** A reconnected Microsoft app credential's national cloud; a reconnect reopens on it. */
77+
reconnectCloud: setup.data?.reconnectCloud,
7678
isReconnect: Boolean(credentialId),
7779
subjectChoice,
7880
setSubjectChoice,

‎apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/microsoft-app-account-modal.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ function MicrosoftAppAccountForm({
9090
onClose: () => onOpenChange(false),
9191
})
9292
const [selectedAuthType, setSelectedAuthType] = useState<MicrosoftAuthType | null>(null)
93-
const [cloud, setCloud] = useState<MicrosoftCloudId>('global')
93+
const [selectedCloud, setSelectedCloud] = useState<MicrosoftCloudId | null>(null)
9494
const [tenantId, setTenantId] = useState('')
9595
const [clientId, setClientId] = useState('')
9696
const [environmentUrl, setEnvironmentUrl] = useState('')
@@ -108,6 +108,8 @@ function MicrosoftAppAccountForm({
108108
? 'federated'
109109
: 'client_secret'
110110
const authType: MicrosoftAuthType = selectedAuthType ?? defaultAuthType
111+
// A reconnect also opens on the credential's stored cloud.
112+
const cloud: MicrosoftCloudId = selectedCloud ?? keyless.reconnectCloud ?? 'global'
111113
const disabled =
112114
isPending ||
113115
!tenantId.trim() ||
@@ -172,7 +174,7 @@ function MicrosoftAppAccountForm({
172174
title='Cloud'
173175
value={cloud}
174176
onChange={(value) => {
175-
setCloud(value as MicrosoftCloudId)
177+
setSelectedCloud(value as MicrosoftCloudId)
176178
clearError()
177179
}}
178180
options={CLOUD_OPTIONS}

‎apps/sim/blocks/blocks/powerbi.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const PowerBIBlock: BlockConfig = {
2525
type: 'powerbi',
2626
name: 'Power BI',
2727
description: 'Query semantic models and manage reports and refreshes',
28-
longDescription: `Connect a home-tenant organizational account through the Power BI Microsoft connection, or run as an app registration with a Microsoft app credential, to list named workspaces, inspect reports and semantic models, execute DAX queries, request standard refreshes, and read refresh history. Microsoft Graph connections cannot substitute for this delegated OAuth connection. An app credential needs the Power BI tenant setting that allows service principals to use Power BI APIs and its service principal added to each workspace as a Member or Admin; DAX queries do not run as a service principal against semantic models with row-level security or SSO. Self-hosted installations use the shared Microsoft app registration and register /api/auth/oauth2/callback/microsoft-powerbi as a Web redirect URI. Workspace listing supports a result limit, offset, and OData filter; reports and semantic models return flat lists. Read-only models may contain only their ID and name, and paginated reports may omit their semantic-model ID. Missing optional fields are null and do not establish permission or feature availability. Keep results small with summaries or TOPN: provider JSON reads are limited to 20 MiB, and workflow tool responses have a stricter 10 MiB inline budget. My Workspace, cross-tenant guest configuration, Credential Groups, impersonation, enhanced refresh, report exports, dashboards, and triggers are outside this version.`,
28+
longDescription: `Connect a home-tenant organizational account through the Power BI Microsoft connection, or run as an app registration with a Microsoft app credential, to list named workspaces, inspect reports and semantic models, execute DAX queries, request standard refreshes, and read refresh history. A Microsoft Graph connection cannot substitute for the Power BI connection or app credential. An app credential needs the Power BI tenant setting that allows service principals to use Power BI APIs and its service principal added to each workspace as a Member or Admin; DAX queries do not run as a service principal against semantic models with row-level security or SSO. Self-hosted installations use the shared Microsoft app registration and register /api/auth/oauth2/callback/microsoft-powerbi as a Web redirect URI. Workspace listing supports a result limit, offset, and OData filter; reports and semantic models return flat lists. Read-only models may contain only their ID and name, and paginated reports may omit their semantic-model ID. Missing optional fields are null and do not establish permission or feature availability. Keep results small with summaries or TOPN: provider JSON reads are limited to 20 MiB, and workflow tool responses have a stricter 10 MiB inline budget. My Workspace, cross-tenant guest configuration, Credential Groups, impersonation, enhanced refresh, report exports, dashboards, and triggers are outside this version.`,
2929
docsLink: 'https://docs.sim.ai/integrations/powerbi',
3030
category: 'tools',
3131
integrationType: IntegrationType.Analytics,

‎apps/sim/lib/api/contracts/credentials.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from 'zod'
22
import { organizationIdSchema, workspaceIdSchema } from '@/lib/api/contracts/primitives'
33
import { defineRouteContract } from '@/lib/api/contracts/types'
4+
import { MICROSOFT_CLOUD_IDS } from '@/lib/credentials/microsoft-clouds'
45
import {
56
ATLASSIAN_PRODUCTS,
67
getMissingServiceAccountFields,
@@ -574,6 +575,8 @@ export const createWorkspaceCredentialContract = defineRouteContract({
574575
*/
575576
/** The reconnected credential's stored auth mode, so the dialog reopens on it. */
576577
const reconnectAuthTypeSchema = z.enum(WORKLOAD_IDENTITY_AUTH_TYPE_LIST).optional()
578+
/** A reconnected Microsoft app credential's national cloud, absent for the global cloud. */
579+
const reconnectCloudSchema = z.enum(MICROSOFT_CLOUD_IDS).optional()
577580

578581
export const workloadIdentitySubjectGrantShape = {
579582
issuer: z
@@ -613,11 +616,16 @@ export const prepareWorkloadIdentityContract = defineRouteContract({
613616
mode: 'json',
614617
schema: z.discriminatedUnion('available', [
615618
/** This deployment has no workload-identity signing key; offer key-based modes only. */
616-
z.object({ available: z.literal(false), reconnectAuthType: reconnectAuthTypeSchema }),
619+
z.object({
620+
available: z.literal(false),
621+
reconnectAuthType: reconnectAuthTypeSchema,
622+
reconnectCloud: reconnectCloudSchema,
623+
}),
617624
z.object({
618625
available: z.literal(true),
619626
...workloadIdentitySubjectGrantShape,
620627
reconnectAuthType: reconnectAuthTypeSchema,
628+
reconnectCloud: reconnectCloudSchema,
621629
}),
622630
]),
623631
},

‎apps/sim/lib/credentials/application/prepare-workload-identity.test.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,27 @@ describe('preparing a reconnect', () => {
235235
}
236236
})
237237

238+
it('reports a national-cloud Microsoft credential’s cloud, so the dialog reopens on it', async () => {
239+
reconnecting(
240+
{
241+
type: 'microsoft_app_credential_v1',
242+
config: { ...appConfig, authType: 'client_secret', clientSecret: 'value', cloud: 'usgov' },
243+
},
244+
{ providerId: 'microsoft-app-service-account' }
245+
)
246+
await expect(prepare('microsoft')).resolves.toMatchObject({
247+
reconnectAuthType: 'client_secret',
248+
reconnectCloud: 'usgov',
249+
})
250+
})
251+
252+
it('reports no cloud for a global Microsoft credential or another provider', async () => {
253+
reconnecting(STORED_MODES[9].secret, { providerId: 'microsoft-app-service-account' })
254+
expect(await prepare('microsoft')).not.toHaveProperty('reconnectCloud')
255+
reconnecting(keylessSecret)
256+
expect(await prepare()).not.toHaveProperty('reconnectCloud')
257+
})
258+
238259
it('gives Google no fixed audience', async () => {
239260
reconnecting(STORED_MODES[11].secret, { providerId: 'google-service-account' })
240261
expect(await prepare('google')).not.toHaveProperty('audience')

‎apps/sim/lib/credentials/application/prepare-workload-identity.ts‎

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import { credentialOperations } from '@/lib/credentials/application/operations'
77
import { storedAwsAuthType } from '@/lib/credentials/aws-service-account.server'
88
import { storedDatabricksAuthType } from '@/lib/credentials/databricks-service-account.server'
99
import { isGoogleWorkloadIdentitySecret } from '@/lib/credentials/google-workload-identity'
10-
import { storedMicrosoftAppAuthType } from '@/lib/credentials/microsoft-app-service-account.server'
10+
import {
11+
storedMicrosoftAppAuthType,
12+
storedMicrosoftAppCloud,
13+
} from '@/lib/credentials/microsoft-app-service-account.server'
14+
import type { MicrosoftCloudId } from '@/lib/credentials/microsoft-clouds'
1115
import { storedSnowflakeAuthType } from '@/lib/credentials/snowflake-service-account.server'
1216
import {
1317
fixedWorkloadIdentityAudience,
@@ -30,9 +34,13 @@ export interface PrepareWorkloadIdentityInput {
3034
credentialId?: string
3135
}
3236

33-
/** The reconnected credential's stored mode; absent without one, or for anyone but its admin. */
37+
/**
38+
* The reconnected credential's stored mode, and a Microsoft app credential's national cloud;
39+
* absent without one, or for anyone but its admin.
40+
*/
3441
interface ReconnectAuthTypeField {
3542
reconnectAuthType?: WorkloadIdentityProviderAuthType
43+
reconnectCloud?: MicrosoftCloudId
3644
}
3745

3846
export type PrepareWorkloadIdentityResult =
@@ -66,16 +74,17 @@ const STORED_AUTH_TYPE: {
6674
}
6775

6876
/**
69-
* The stored auth mode of a credential of this provider in this workspace, which
70-
* this person administers. Only the mode leaves the stored secret, and only for
71-
* someone allowed to reconnect the credential.
77+
* The stored auth mode (and a Microsoft app credential's national cloud) of a
78+
* credential of this provider in this workspace, which this person administers.
79+
* Only these leave the stored secret, and only for someone allowed to reconnect
80+
* the credential.
7281
*/
73-
async function storedReconnectAuthType(
82+
async function storedReconnectFields(
7483
provider: WorkloadIdentityProvider,
7584
credentialId: string,
7685
workspaceId: string,
7786
userId: string
78-
): Promise<WorkloadIdentityProviderAuthType | undefined> {
87+
): Promise<ReconnectAuthTypeField> {
7988
const access = await getCredentialActorContext(credentialId, userId, { workspaceId })
8089
const row = access.credential
8190
if (
@@ -87,10 +96,15 @@ async function storedReconnectAuthType(
8796
workloadIdentityProviderFor(row.providerId) !== provider ||
8897
!row.encryptedServiceAccountKey
8998
) {
90-
return undefined
99+
return {}
91100
}
92101
const { decrypted } = await decryptSecret(row.encryptedServiceAccountKey)
93-
return STORED_AUTH_TYPE[provider](decrypted)
102+
const reconnectAuthType = STORED_AUTH_TYPE[provider](decrypted)
103+
const reconnectCloud = provider === 'microsoft' ? storedMicrosoftAppCloud(decrypted) : undefined
104+
return {
105+
...(reconnectAuthType ? { reconnectAuthType } : {}),
106+
...(reconnectCloud ? { reconnectCloud } : {}),
107+
}
94108
}
95109

96110
/**
@@ -110,15 +124,14 @@ export const prepareWorkloadIdentity = defineAuthorizedWorkspaceUseCase({
110124
},
111125
authorizationOptions: {},
112126
execute: async ({ principal, input }): Promise<PrepareWorkloadIdentityResult> => {
113-
const reconnectAuthType = input.credentialId
114-
? await storedReconnectAuthType(
127+
const reconnect = input.credentialId
128+
? await storedReconnectFields(
115129
input.provider,
116130
input.credentialId,
117131
input.workspaceId,
118132
requirePrincipalSubjectUserId(principal)
119133
)
120-
: undefined
121-
const reconnect = reconnectAuthType ? { reconnectAuthType } : {}
134+
: {}
122135
// A deployment without a signing key offers only key- and secret-based credentials.
123136
if (!(await isWorkloadIdentityConfigured())) return { available: false, ...reconnect }
124137
const userId = requirePrincipalSubjectUserId(principal)

0 commit comments

Comments
 (0)