diff --git a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppWorkspace.test.ts b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppWorkspace.test.ts index 51888da9983..747bfe732cf 100644 --- a/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppWorkspace.test.ts +++ b/apps/vs-code-designer/src/app/commands/createNewCodeProject/CodeProjectBase/__test__/CreateLogicAppWorkspace.test.ts @@ -65,6 +65,7 @@ vi.mock('path', () => ({ vi.mock('../../../../utils/vsCodeConfig/settings', () => ({ getGlobalSetting: vi.fn(), + isManagedIdentityAuthEnabled: vi.fn(() => true), })); // Import actual path for test setup (not affected by mock) @@ -1416,10 +1417,11 @@ describe('createLocalConfigurationFiles', () => { FUNCTIONS_WORKER_RUNTIME: 'dotnet', APP_KIND: 'workflowapp', ProjectDirectoryPath: path.join('test', 'workspace', 'TestLogicApp'), + WORKFLOWS_AUTHENTICATION_METHOD: 'managedServiceIdentity', }); // Verify no other properties exist - expect(Object.keys(localSettingsData.Values)).toHaveLength(5); + expect(Object.keys(localSettingsData.Values)).toHaveLength(6); }); it('should include global.json in .funcignore for custom code projects', async () => { @@ -1461,11 +1463,12 @@ describe('createLocalConfigurationFiles', () => { FUNCTIONS_WORKER_RUNTIME: 'dotnet', APP_KIND: 'workflowapp', ProjectDirectoryPath: path.join('test', 'workspace', 'TestLogicApp'), + WORKFLOWS_AUTHENTICATION_METHOD: 'managedServiceIdentity', AzureWebJobsFeatureFlags: 'EnableMultiLanguageWorker', }); - // Verify exactly 6 properties exist (5 standard + 1 feature flag) - expect(Object.keys(localSettingsData.Values)).toHaveLength(6); + // Verify exactly 7 properties exist (5 standard + auth method + 1 feature flag) + expect(Object.keys(localSettingsData.Values)).toHaveLength(7); }); it('should include global.json in .funcignore for rules engine projects', async () => { @@ -1507,11 +1510,12 @@ describe('createLocalConfigurationFiles', () => { FUNCTIONS_WORKER_RUNTIME: 'dotnet', APP_KIND: 'workflowapp', ProjectDirectoryPath: path.join('test', 'workspace', 'TestLogicApp'), + WORKFLOWS_AUTHENTICATION_METHOD: 'managedServiceIdentity', AzureWebJobsFeatureFlags: 'EnableMultiLanguageWorker', }); - // Verify exactly 6 properties exist (5 standard + 1 feature flag) - expect(Object.keys(localSettingsData.Values)).toHaveLength(6); + // Verify exactly 7 properties exist (5 standard + auth method + 1 feature flag) + expect(Object.keys(localSettingsData.Values)).toHaveLength(7); }); it('should create local.settings.json with exact required values for codeful projects', async () => { @@ -1531,12 +1535,13 @@ describe('createLocalConfigurationFiles', () => { FUNCTIONS_WORKER_RUNTIME: 'dotnet', APP_KIND: 'workflowapp', ProjectDirectoryPath: path.join('test', 'workspace', 'TestLogicApp'), + WORKFLOWS_AUTHENTICATION_METHOD: 'managedServiceIdentity', AzureWebJobsFeatureFlags: 'EnableMultiLanguageWorker', WORKFLOW_CODEFUL_ENABLED: 'true', }); - // Verify exactly 7 properties exist (5 standard + feature flag + codeful-enabled). - expect(Object.keys(localSettingsData.Values)).toHaveLength(7); + // Verify exactly 8 properties exist (5 standard + auth method + feature flag + codeful-enabled). + expect(Object.keys(localSettingsData.Values)).toHaveLength(8); }); it('should include extension bundle configuration in host.json', async () => { diff --git a/apps/vs-code-designer/src/app/commands/workflows/__test__/authenticationMethodStep.test.ts b/apps/vs-code-designer/src/app/commands/workflows/__test__/authenticationMethodStep.test.ts deleted file mode 100644 index 13b3420f3ca..00000000000 --- a/apps/vs-code-designer/src/app/commands/workflows/__test__/authenticationMethodStep.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -vi.mock('../../../../localize', () => ({ - localize: (_key: string, defaultMessage: string) => defaultMessage, -})); - -vi.mock('@microsoft/vscode-azext-utils', () => ({ - AzureWizardPromptStep: class AzureWizardPromptStep {}, - parseError: (error: any) => ({ - isUserCancelledError: error?.isUserCancelledError === true, - message: error?.message ?? String(error), - }), -})); - -import { AuthenticationMethodSelectionStep } from '../authenticationMethodStep'; - -describe('AuthenticationMethodSelectionStep', () => { - let context: any; - - beforeEach(() => { - context = { - enabled: true, - telemetry: { properties: {}, measurements: {} }, - ui: { - showQuickPick: vi.fn(), - }, - }; - }); - - it('defaults to connection keys when the authentication prompt is cancelled', async () => { - context.ui.showQuickPick.mockRejectedValue({ isUserCancelledError: true }); - - await new AuthenticationMethodSelectionStep().prompt(context); - - expect(context.authenticationMethod).toBe('rawKeys'); - expect(context.telemetry.properties.authenticationMethodDefaulted).toBe('rawKeys'); - }); - - it('keeps explicit Managed Service Identity selections', async () => { - context.ui.showQuickPick.mockResolvedValue({ data: 'managedServiceIdentity' }); - - await new AuthenticationMethodSelectionStep().prompt(context); - - expect(context.authenticationMethod).toBe('managedServiceIdentity'); - }); - - it('keeps explicit connection-key selections', async () => { - context.ui.showQuickPick.mockResolvedValue({ data: 'rawKeys' }); - - await new AuthenticationMethodSelectionStep().prompt(context); - - expect(context.authenticationMethod).toBe('rawKeys'); - }); - - it('defaults to rawKeys without prompting when MSI is disabled', () => { - context.enabled = true; - - expect(new AuthenticationMethodSelectionStep().shouldPrompt(context)).toBe(false); - expect(context.authenticationMethod).toBe('rawKeys'); - }); - - it('does not override an already-set authentication method', () => { - context.enabled = true; - context.authenticationMethod = 'managedServiceIdentity'; - - expect(new AuthenticationMethodSelectionStep().shouldPrompt(context)).toBe(false); - expect(context.authenticationMethod).toBe('managedServiceIdentity'); - }); - - it('rethrows non-cancellation errors', async () => { - context.ui.showQuickPick.mockRejectedValue(new Error('quick pick failed')); - - await expect(new AuthenticationMethodSelectionStep().prompt(context)).rejects.toThrow('quick pick failed'); - }); -}); diff --git a/apps/vs-code-designer/src/app/commands/workflows/__test__/azureConnectorWizard.test.ts b/apps/vs-code-designer/src/app/commands/workflows/__test__/azureConnectorWizard.test.ts index 13caa97434c..8df44aa4ca4 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/__test__/azureConnectorWizard.test.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/__test__/azureConnectorWizard.test.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { defaultMsiAudience, - workflowAuthenticationMethodKey, workflowLocationKey, workflowManagementBaseURIKey, workflowResourceGroupNameKey, @@ -65,15 +64,13 @@ describe('createAzureWizard', () => { } as any; }); - it('defaults cancelled connector selection to disabled raw keys', async () => { + it('defaults cancelled connector selection to disabled', async () => { vi.mocked(context.ui.showQuickPick).mockRejectedValue({ isUserCancelledError: true }); const wizard = createAzureWizard(context, projectPath) as any; await wizard.options.promptSteps[0].prompt(context); expect(context.enabled).toBe(false); - expect(context.authenticationMethod).toBe('rawKeys'); - expect(context.telemetry.properties.azureConnectorsDefaulted).toBe('rawKeys'); }); it('surfaces non-cancel connector selection failures', async () => { @@ -105,23 +102,20 @@ describe('createAzureWizard', () => { expect(azureConnectorStep.shouldPrompt({ ...context, enabled: false })).toBe(false); }); - it('persists disabled raw-key settings when Azure connectors are skipped', async () => { + it('persists empty subscription when Azure connectors are skipped', async () => { context.enabled = false; - context.authenticationMethod = 'rawKeys'; const wizard = createAzureWizard(context, projectPath) as any; await wizard.options.executeSteps[0].execute(context); expect(addOrUpdateLocalAppSettings).toHaveBeenCalledWith(context, projectPath, { [workflowSubscriptionIdKey]: '', - [workflowAuthenticationMethodKey]: 'rawKeys', }); }); it('persists Azure connector settings and notifies the language client when enabled', async () => { Object.assign(context, { enabled: true, - authenticationMethod: 'managedServiceIdentity', tenantId: 'tenant-id', subscriptionId: 'subscription-id', resourceGroup: { name: 'rg', location: 'westus' }, @@ -137,7 +131,6 @@ describe('createAzureWizard', () => { [workflowResourceGroupNameKey]: 'rg', [workflowLocationKey]: 'westus', [workflowManagementBaseURIKey]: 'https://management.azure.com/', - [workflowAuthenticationMethodKey]: 'managedServiceIdentity', [workflowsDynamicConnectionDefaultAuthAudienceKey]: defaultMsiAudience, }); expect(ext.languageClient.sendNotification).toHaveBeenCalledWith('custom/updateApiConfig', { diff --git a/apps/vs-code-designer/src/app/commands/workflows/__test__/enableAzureConnectors.test.ts b/apps/vs-code-designer/src/app/commands/workflows/__test__/enableAzureConnectors.test.ts index e2fa9ac9d32..eb749ce2896 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/__test__/enableAzureConnectors.test.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/__test__/enableAzureConnectors.test.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; -import { localSettingsFileName, workflowAuthenticationMethodKey, workflowSubscriptionIdKey } from '../../../../constants'; +import { localSettingsFileName, workflowSubscriptionIdKey } from '../../../../constants'; import { getLocalSettingsJson } from '../../../utils/appSettings/localSettings'; import { getAzureConnectorDetailsForLocalProject, invalidateAzureDetailsCache } from '../../../utils/codeless/common'; import { getLogicAppProjectRoot } from '../../../utils/codeless/connection'; @@ -68,11 +68,10 @@ describe('enableAzureConnectors', () => { expect(ext.outputChannel.appendLog).toHaveBeenCalledWith('Azure connectors are enabled for the workflow.'); }); - it('shows already-enabled information when subscription and auth settings exist', async () => { + it('shows already-enabled information when subscription setting exists', async () => { (getLocalSettingsJson as Mock).mockResolvedValue({ Values: { [workflowSubscriptionIdKey]: 'subscription-id', - [workflowAuthenticationMethodKey]: 'ActiveDirectoryOAuth', }, }); diff --git a/apps/vs-code-designer/src/app/commands/workflows/authenticationMethodStep.ts b/apps/vs-code-designer/src/app/commands/workflows/authenticationMethodStep.ts deleted file mode 100644 index b34a76742f7..00000000000 --- a/apps/vs-code-designer/src/app/commands/workflows/authenticationMethodStep.ts +++ /dev/null @@ -1,82 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzureWizardPromptStep, parseError } from '@microsoft/vscode-azext-utils'; -import type { IActionContext, IAzureQuickPickItem } from '@microsoft/vscode-azext-utils'; -import { localize } from '../../../localize'; - -/** - * String literal union for supported authentication methods. - * This replaces the enum for simpler runtime footprint. - */ -export type AuthenticationMethodType = 'managedServiceIdentity' | 'rawKeys'; - -/** - * Authentication method selection step - * This step prompts the user to select between MSI and raw keys - * and sets the `authenticationMethod` in the context. - */ -export class AuthenticationMethodSelectionStep< - T extends IActionContext & { authenticationMethod?: AuthenticationMethodType; enabled?: boolean }, -> extends AzureWizardPromptStep { - /** - * Prompts the user to select an authentication method. - * The result is stored in `context.authenticationMethod`. - */ - public async prompt(context: T): Promise { - const placeHolder: string = localize('selectAuthMethod', 'Select authentication method for Azure connectors'); - - const picks: IAzureQuickPickItem[] = [ - { - label: localize('authMethodMSI', '$(shield) Managed Service Identity'), - description: localize('authMethodMSIDesc', 'Use Azure Managed Identity'), - detail: localize( - 'authMethodMSIDetail', - 'Authenticate using Azure Managed Service Identity. More secure, no keys stored locally. Requires proper Azure RBAC configuration.' - ), - data: 'managedServiceIdentity', - }, - { - label: localize('authMethodRawKeys', '$(key) Connection Keys'), - description: localize('authMethodRawKeysDesc', 'Use connection strings and access keys (traditional method)'), - detail: localize( - 'authMethodRawKeysDetail', - 'Authenticate using connection strings, access keys, or API keys configured in your app settings.' - ), - data: 'rawKeys', - }, - ]; - - const selectedMethod = await context.ui - .showQuickPick(picks, { - placeHolder, - suppressPersistence: true, - ignoreFocusOut: true, - }) - .catch((error) => { - if (parseError(error).isUserCancelledError) { - context.telemetry.properties.authenticationMethodDefaulted = 'rawKeys'; - return { data: 'rawKeys' as AuthenticationMethodType }; - } - - throw error; - }); - - // Save the selected authentication method - context.authenticationMethod = selectedMethod.data; - } - - /** - * Determines if this step should prompt again (only if no method is selected yet). - * - * TODO: MSI option is temporarily disabled until functions extension fix deployed — always default to rawKeys. - */ - public shouldPrompt(context: T): boolean { - if (context.enabled === true && context.authenticationMethod === undefined) { - context.authenticationMethod = 'rawKeys'; - } - return false; - } -} diff --git a/apps/vs-code-designer/src/app/commands/workflows/azureConnectorWizard.ts b/apps/vs-code-designer/src/app/commands/workflows/azureConnectorWizard.ts index 95fccbf40dc..3e7d5c993e9 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/azureConnectorWizard.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/azureConnectorWizard.ts @@ -9,7 +9,6 @@ import type { IProjectWizardContext } from '@microsoft/vscode-extension-logic-ap import * as path from 'path'; import { defaultMsiAudience, - workflowAuthenticationMethodKey, workflowLocationKey, workflowManagementBaseURIKey, workflowResourceGroupNameKey, @@ -20,7 +19,6 @@ import { import { ext } from '../../../extensionVariables'; import { localize } from '../../../localize'; import { addOrUpdateLocalAppSettings } from '../../utils/appSettings/localSettings'; -import { AuthenticationMethodSelectionStep, type AuthenticationMethodType } from './authenticationMethodStep'; export interface IAzureConnectorsContext extends IActionContext, IProjectWizardContext { credentials: any; @@ -29,14 +27,13 @@ export interface IAzureConnectorsContext extends IActionContext, IProjectWizardC enabled: boolean; tenantId: any; environment: any; - authenticationMethod?: AuthenticationMethodType; MSIenabled?: boolean; } //TODO: Update to be in webview after ignite redesign is done export function createAzureWizard(wizardContext: IAzureConnectorsContext, projectPath: string): AzureWizard { return new AzureWizard(wizardContext, { - promptSteps: [new GetSubscriptionDetailsStep(projectPath), new AuthenticationMethodSelectionStep()], + promptSteps: [new GetSubscriptionDetailsStep(projectPath)], executeSteps: [new SaveAzureContext(projectPath)], }); } @@ -59,7 +56,6 @@ class GetSubscriptionDetailsStep extends AzureWizardPromptStep { if (parseError(error).isUserCancelledError) { - context.telemetry.properties.azureConnectorsDefaulted = 'rawKeys'; return { data: 'no' }; } @@ -67,9 +63,6 @@ class GetSubscriptionDetailsStep extends AzureWizardPromptStep { const valuesToUpdateInSettings: Record = {}; if (context.enabled === false) { valuesToUpdateInSettings[workflowSubscriptionIdKey] = ''; - valuesToUpdateInSettings[workflowAuthenticationMethodKey] = 'rawKeys'; } else { const { resourceGroup, subscriptionId, tenantId, environment } = context; valuesToUpdateInSettings[workflowTenantIdKey] = tenantId; @@ -114,11 +106,7 @@ class SaveAzureContext extends AzureWizardExecuteStep { valuesToUpdateInSettings[workflowResourceGroupNameKey] = resourceGroup?.name || ''; valuesToUpdateInSettings[workflowLocationKey] = resourceGroup?.location || ''; valuesToUpdateInSettings[workflowManagementBaseURIKey] = environment.resourceManagerEndpointUrl; - // Save the authentication method to local settings - if (context.authenticationMethod) { - valuesToUpdateInSettings[workflowAuthenticationMethodKey] = context.authenticationMethod; - valuesToUpdateInSettings[workflowsDynamicConnectionDefaultAuthAudienceKey] = defaultMsiAudience; - } + valuesToUpdateInSettings[workflowsDynamicConnectionDefaultAuthAudienceKey] = defaultMsiAudience; // Then send notifications for runtime updates ext?.languageClient?.sendNotification('custom/updateApiConfig', { diff --git a/apps/vs-code-designer/src/app/commands/workflows/enableAzureConnectors.ts b/apps/vs-code-designer/src/app/commands/workflows/enableAzureConnectors.ts index 7f790344eee..c9f08503b8d 100644 --- a/apps/vs-code-designer/src/app/commands/workflows/enableAzureConnectors.ts +++ b/apps/vs-code-designer/src/app/commands/workflows/enableAzureConnectors.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localSettingsFileName, workflowAuthenticationMethodKey, workflowSubscriptionIdKey } from '../../../../src/constants'; +import { localSettingsFileName, workflowSubscriptionIdKey } from '../../../../src/constants'; import { localize } from '../../../localize'; import type { IAzureConnectorsContext } from '../../commands/workflows/azureConnectorWizard'; import { createAzureWizard } from '../../commands/workflows/azureConnectorWizard'; @@ -29,8 +29,7 @@ export async function enableAzureConnectors(context: IActionContext, node: vscod const localSettings: ILocalSettingsJson = await getLocalSettingsJson(context, localSettingsFilePath); const subscriptionId: string | undefined = localSettings.Values?.[workflowSubscriptionIdKey]; - const authenticationMethod: string | undefined = localSettings.Values?.[workflowAuthenticationMethodKey]; - if (!!subscriptionId && !!authenticationMethod) { + if (subscriptionId) { ext.outputChannel.appendLog(localize('logicapp.azureConnectorsEnabledForWorkflow', 'Azure connectors are enabled for the workflow.')); return; } @@ -43,7 +42,7 @@ export async function enableAzureConnectors(context: IActionContext, node: vscod if (connectorsContext.enabled) { invalidateAzureDetailsCache(projectPath); getAzureConnectorDetailsForLocalProject(context, projectPath).catch(() => {}); - + ext.outputChannel.appendLog(localize('logicapp.azureConnectorsEnabledForWorkflow', 'Azure connectors are enabled for the workflow.')); } } diff --git a/apps/vs-code-designer/src/app/utils/appSettings/__test__/localSettings.test.ts b/apps/vs-code-designer/src/app/utils/appSettings/__test__/localSettings.test.ts index 1cd8b562f70..e2b67f91ec2 100644 --- a/apps/vs-code-designer/src/app/utils/appSettings/__test__/localSettings.test.ts +++ b/apps/vs-code-designer/src/app/utils/appSettings/__test__/localSettings.test.ts @@ -13,6 +13,7 @@ import { localEmulatorConnectionString, logicAppKind, multiLanguageWorkerSetting, + workflowAuthenticationMethodKey, workerRuntimeKey, workflowCodefulEnabledKey, } from '../../../../constants'; @@ -74,6 +75,7 @@ describe('utils/appSettings', () => { [workerRuntimeKey]: WorkerRuntime.Dotnet, [appKindSetting]: logicAppKind, [ProjectDirectoryPathKey]: projectPath, + [workflowAuthenticationMethodKey]: 'managedServiceIdentity', }; it('logicApp: 5 base keys, no feature or codeful flags', () => { @@ -120,10 +122,11 @@ describe('utils/appSettings', () => { expect(getLocalSettingsSchema(false)).toEqual({ IsEncrypted: false, Values: { - [appKindSetting]: logicAppKind, - [workerRuntimeKey]: WorkerRuntime.Dotnet, [azureWebJobsStorageKey]: localEmulatorConnectionString, [functionsInprocNet8Enabled]: functionsInprocNet8EnabledTrue, + [workerRuntimeKey]: WorkerRuntime.Dotnet, + [appKindSetting]: logicAppKind, + [workflowAuthenticationMethodKey]: 'managedServiceIdentity', }, }); }); @@ -183,6 +186,7 @@ describe('utils/appSettings', () => { workerRuntimeKey, appKindSetting, ProjectDirectoryPathKey, + workflowAuthenticationMethodKey, ]); }); @@ -194,6 +198,7 @@ describe('utils/appSettings', () => { workerRuntimeKey, appKindSetting, ProjectDirectoryPathKey, + workflowAuthenticationMethodKey, azureWebJobsFeatureFlagsKey, workflowCodefulEnabledKey, ]); diff --git a/apps/vs-code-designer/src/app/utils/appSettings/localSettings.ts b/apps/vs-code-designer/src/app/utils/appSettings/localSettings.ts index f7df74dcdd4..1514a45a945 100644 --- a/apps/vs-code-designer/src/app/utils/appSettings/localSettings.ts +++ b/apps/vs-code-designer/src/app/utils/appSettings/localSettings.ts @@ -16,7 +16,9 @@ import { azureStorageTypeSetting, functionsInprocNet8Enabled, functionsInprocNet8EnabledTrue, + workflowAuthenticationMethodKey, workflowCodefulEnabledKey, + workflowAuthenticationMethodMIValue, } from '../../../constants'; import { localize } from '../../../localize'; import { decryptLocalSettings } from '../../commands/appSettings/decryptLocalSettings'; @@ -24,6 +26,7 @@ import { encryptLocalSettings } from '../../commands/appSettings/encryptLocalSet import { executeOnFunctions } from '../../functionsExtension/executeOnFunctionsExt'; import { writeFormattedJson } from '../fs'; import { parseJson } from '../parseJson'; +import { isManagedIdentityAuthEnabled } from '../vsCodeConfig/settings'; import { DialogResponses, parseError } from '@microsoft/vscode-azext-utils'; import type { IActionContext } from '@microsoft/vscode-azext-utils'; import { MismatchBehavior, ProjectType, WorkerRuntime } from '@microsoft/vscode-extension-logic-apps'; @@ -227,6 +230,9 @@ export const getLocalSettingsSchema = ( if (projectPath) { values[ProjectDirectoryPathKey] = projectPath; } + if (isManagedIdentityAuthEnabled()) { + values[workflowAuthenticationMethodKey] = workflowAuthenticationMethodMIValue; + } if (logicAppType !== undefined && logicAppType !== ProjectType.logicApp) { values[azureWebJobsFeatureFlagsKey] = multiLanguageWorkerSetting; } diff --git a/apps/vs-code-designer/src/app/utils/codeless/__test__/common.test.ts b/apps/vs-code-designer/src/app/utils/codeless/__test__/common.test.ts index 68aee7e7f26..98bf6746f4d 100644 --- a/apps/vs-code-designer/src/app/utils/codeless/__test__/common.test.ts +++ b/apps/vs-code-designer/src/app/utils/codeless/__test__/common.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { workflowAuthenticationMethodKey, workflowSubscriptionIdKey } from '../../../../constants'; +import { workflowSubscriptionIdKey } from '../../../../constants'; vi.mock('../../../../localize', () => ({ localize: (_key: string, defaultMessage: string, ...args: unknown[]) => @@ -58,7 +58,7 @@ describe('getAzureConnectorDetailsForLocalProject', () => { }; }); - it('defaults cancelled Azure connector discovery to disabled raw-key settings', async () => { + it('defaults cancelled Azure connector discovery to disabled settings', async () => { vi.mocked(getLocalSettingsJson).mockResolvedValue({ Values: {} } as any); vi.mocked(createAzureWizard).mockReturnValue({ prompt: vi.fn().mockRejectedValue({ isUserCancelledError: true }), @@ -70,10 +70,8 @@ describe('getAzureConnectorDetailsForLocalProject', () => { expect(details).toEqual({ enabled: false }); expect(addOrUpdateLocalAppSettings).toHaveBeenCalledWith(context, projectPath, { [workflowSubscriptionIdKey]: '', - [workflowAuthenticationMethodKey]: 'rawKeys', }); expect(getAuthData).not.toHaveBeenCalled(); - expect(context.telemetry.properties.azureConnectorsDefaulted).toBe('rawKeys'); }); it('handles undefined projectPath', async () => { @@ -106,17 +104,15 @@ describe('getAzureConnectorDetailsForLocalProject', () => { expect(addOrUpdateLocalAppSettings).not.toHaveBeenCalled(); }); - it('persists raw keys when the Azure wizard explicitly skips connectors', async () => { + it('persists disabled state when the Azure wizard explicitly skips connectors', async () => { vi.mocked(getLocalSettingsJson).mockResolvedValue({ Values: {} } as any); vi.mocked(createAzureWizard).mockImplementation((wizardContext: any) => ({ prompt: vi.fn(async () => { wizardContext.enabled = false; - wizardContext.authenticationMethod = 'rawKeys'; }), execute: vi.fn(async () => { await addOrUpdateLocalAppSettings(wizardContext, projectPath, { [workflowSubscriptionIdKey]: '', - [workflowAuthenticationMethodKey]: 'rawKeys', }); }), })) as any; @@ -126,7 +122,6 @@ describe('getAzureConnectorDetailsForLocalProject', () => { expect(details.enabled).toBe(false); expect(addOrUpdateLocalAppSettings).toHaveBeenCalledWith(context, projectPath, { [workflowSubscriptionIdKey]: '', - [workflowAuthenticationMethodKey]: 'rawKeys', }); }); diff --git a/apps/vs-code-designer/src/app/utils/codeless/__test__/validateProjectArtifacts.test.ts b/apps/vs-code-designer/src/app/utils/codeless/__test__/validateProjectArtifacts.test.ts index 6ae47557a11..1b783374b7d 100644 --- a/apps/vs-code-designer/src/app/utils/codeless/__test__/validateProjectArtifacts.test.ts +++ b/apps/vs-code-designer/src/app/utils/codeless/__test__/validateProjectArtifacts.test.ts @@ -21,6 +21,7 @@ import { localEmulatorConnectionString, logicAppKind, multiLanguageWorkerSetting, + workflowAuthenticationMethodKey, workerRuntimeKey, workflowCodefulEnabledKey, workflowOperationDiscoveryHostModeKey, @@ -73,6 +74,7 @@ vi.mock('../../vsCodeConfig/settings', async (importActual) => { return { ...actual, useNodeDesignTimeWorker: vi.fn(() => false), + isManagedIdentityAuthEnabled: vi.fn(() => true), }; }); @@ -192,6 +194,7 @@ describe('validateProjectArtifacts', () => { [workerRuntimeKey]: WorkerRuntime.Dotnet, [azureWebJobsStorageKey]: localEmulatorConnectionString, [functionsInprocNet8Enabled]: functionsInprocNet8EnabledTrue, + [workflowAuthenticationMethodKey]: 'managedServiceIdentity', [azureWebJobsFeatureFlagsKey]: multiLanguageWorkerSetting, [workflowCodefulEnabledKey]: 'true', }); @@ -211,6 +214,7 @@ describe('validateProjectArtifacts', () => { ProjectDirectoryPath: projectPath, AzureWebJobsStorage: 'UseDevelopmentStorage=true', FUNCTIONS_INPROC_NET8_ENABLED: '1', + WORKFLOWS_AUTHENTICATION_METHOD: 'managedServiceIdentity', EXISTING_SECRET: 'super-secret', }, }); @@ -234,6 +238,7 @@ describe('validateProjectArtifacts', () => { ProjectDirectoryPath: projectPath, AzureWebJobsStorage: 'UseDevelopmentStorage=true', FUNCTIONS_INPROC_NET8_ENABLED: '1', + WORKFLOWS_AUTHENTICATION_METHOD: 'managedServiceIdentity', }, }); @@ -256,6 +261,7 @@ describe('validateProjectArtifacts', () => { [workerRuntimeKey]: WorkerRuntime.Dotnet, [azureWebJobsStorageKey]: localEmulatorConnectionString, [functionsInprocNet8Enabled]: functionsInprocNet8EnabledTrue, + [workflowAuthenticationMethodKey]: 'managedServiceIdentity', }; beforeEach(() => { @@ -875,6 +881,7 @@ describe('validateProjectArtifacts', () => { ProjectDirectoryPath: projectPath, AzureWebJobsStorage: 'UseDevelopmentStorage=true', FUNCTIONS_INPROC_NET8_ENABLED: '1', + WORKFLOWS_AUTHENTICATION_METHOD: 'managedServiceIdentity', }, }); @@ -901,6 +908,7 @@ describe('validateProjectArtifacts', () => { ProjectDirectoryPath: projectPath, AzureWebJobsStorage: 'UseDevelopmentStorage=true', FUNCTIONS_INPROC_NET8_ENABLED: '1', + WORKFLOWS_AUTHENTICATION_METHOD: 'managedServiceIdentity', }, }); diff --git a/apps/vs-code-designer/src/app/utils/codeless/common.ts b/apps/vs-code-designer/src/app/utils/codeless/common.ts index 102f4356e8f..a9d29c28379 100644 --- a/apps/vs-code-designer/src/app/utils/codeless/common.ts +++ b/apps/vs-code-designer/src/app/utils/codeless/common.ts @@ -7,7 +7,6 @@ import { workflowManagementBaseURIKey, managementApiPrefix, workflowFileName, - workflowAuthenticationMethodKey, artifactsDirectory, mapsDirectory, schemasDirectory, @@ -193,10 +192,9 @@ export async function getArtifactsPathInLocalProject(projectPath: string): Promi const azureDetailsCache = new Map(); const AZURE_DETAILS_CACHE_TTL = 300000; // 5 minutes -async function defaultAzureConnectorDetailsToRawKeys(context: IActionContext, projectPath: string): Promise { +async function defaultAzureConnectorDetails(context: IActionContext, projectPath: string): Promise { await addOrUpdateLocalAppSettings(context, projectPath, { [workflowSubscriptionIdKey]: '', - [workflowAuthenticationMethodKey]: 'rawKeys', }); return { @@ -252,8 +250,8 @@ export async function getAzureConnectorDetailsForLocalProject( throw error; } - context.telemetry.properties.azureConnectorsDefaulted = 'rawKeys'; - const defaultDetails = await defaultAzureConnectorDetailsToRawKeys(context, projectPath); + context.telemetry.properties.useDefaultAzureConnectorDetails = 'true'; + const defaultDetails = await defaultAzureConnectorDetails(context, projectPath); azureDetailsCache.set(projectPath, { timestamp: now, details: defaultDetails }); return defaultDetails; } diff --git a/apps/vs-code-designer/src/app/utils/codeless/connection.ts b/apps/vs-code-designer/src/app/utils/codeless/connection.ts index 56cbdc70e0f..c224e03e1e4 100644 --- a/apps/vs-code-designer/src/app/utils/codeless/connection.ts +++ b/apps/vs-code-designer/src/app/utils/codeless/connection.ts @@ -4,6 +4,7 @@ import { localSettingsFileName, parameterizeConnectionsInProjectLoadSetting, workflowAuthenticationMethodKey, + workflowAuthenticationMethodMIValue, } from '../../../constants'; import { localize } from '../../../localize'; import { isCSharpProject } from '../detectProjectLanguage'; @@ -40,7 +41,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { parameterizeConnection } from './parameterizer'; import { window } from 'vscode'; -import { getGlobalSetting } from '../vsCodeConfig/settings'; +import { getGlobalSetting, isManagedIdentityAuthEnabled } from '../vsCodeConfig/settings'; import type { SlotTreeItem } from '../../tree/slotsTree/SlotTreeItem'; import { ext } from '../../../extensionVariables'; @@ -216,7 +217,7 @@ async function getConnectionReference( settingsToAdd: Record, parametersToAdd: any, parameterizeConnectionsSetting: any, - useMSI: boolean + isMIEnabled: boolean ): Promise { const { api: { id: apiId }, @@ -244,13 +245,13 @@ async function getConnectionReference( ) .then(({ data: response }) => { // Only add connection key to settings if NOT using MSI - if (!useMSI) { + if (!isMIEnabled) { const appSettingKey = `${referenceKey}-connectionKey`; settingsToAdd[appSettingKey] = response.connectionKey; } // Determine authentication based on ext.useMSI - const authValue = useMSI + const authValue = isMIEnabled ? { type: 'ManagedServiceIdentity' } : { type: 'Raw', @@ -303,7 +304,7 @@ export async function getConnectionsAndSettingsToUpdate( const connectionsData = connectionsDataString === '' ? {} : JSON.parse(connectionsDataString); const localSettingsPath: string = path.join(projectPath, localSettingsFileName); const localSettings: ILocalSettingsJson = await getLocalSettingsJson(context, localSettingsPath); - const useMsi = await isMSIEnabled(projectPath, context); + const isMIEnabled = await isMISettingEnabled(context, projectPath); let areKeysRefreshed = false; let areKeysGenerated = false; @@ -329,17 +330,17 @@ export async function getConnectionsAndSettingsToUpdate( settingsToAdd, parametersFromDefinition, parameterizeConnectionsSetting, - useMsi + isMIEnabled ); - context.telemetry.properties.connectionKeyGenerated = useMsi + context.telemetry.properties.connectionKeyGenerated = isMIEnabled ? `${referenceKey} configured for MSI authentication` : `${referenceKey}-connectionKey generated and is valid for 7 days`; - if (!useMsi) { + if (!isMIEnabled) { areKeysGenerated = true; } - } else if (!useMsi && isApiHubConnectionId(reference.connection.id) && !localSettings.Values[`${referenceKey}-connectionKey`]) { + } else if (!isMIEnabled && isApiHubConnectionId(reference.connection.id) && !localSettings.Values[`${referenceKey}-connectionKey`]) { const resolvedConnectionReference = resolveConnectionsReferences(JSON.stringify(reference), undefined, localSettings.Values); accessToken = accessToken ? accessToken : await getAuthorizationToken(azureTenantId); @@ -354,13 +355,13 @@ export async function getConnectionsAndSettingsToUpdate( settingsToAdd, parametersFromDefinition, parameterizeConnectionsSetting, - useMsi + isMIEnabled ); context.telemetry.properties.connectionKeyGenerated = `${referenceKey}-connectionKey generated and is valid for 7 days`; areKeysGenerated = true; } else if ( - !useMsi && + !isMIEnabled && isApiHubConnectionId(reference.connection.id) && localSettings.Values[`${referenceKey}-connectionKey`] && isKeyExpired(jwtTokenHelper, Date.now(), localSettings.Values[`${referenceKey}-connectionKey`], 3, context, referenceKey) @@ -379,7 +380,7 @@ export async function getConnectionsAndSettingsToUpdate( settingsToAdd, parametersFromDefinition, parameterizeConnectionsSetting, - useMsi + isMIEnabled ); context.telemetry.properties.connectionKeyRegenerate = `${referenceKey}-connectionKey regenerated and is valid for 7 days`; @@ -390,7 +391,7 @@ export async function getConnectionsAndSettingsToUpdate( } // Update MSI connection permissions if MSI is enabled - if (useMsi) { + if (isMIEnabled) { try { const updatedReferences = await updateConnectionReferencesLocalMSI( context, @@ -986,28 +987,19 @@ async function checkExistingPolicy( } /** - * Checks if Managed Service Identity (MSI) authentication is enabled for a Logic Apps project. + * Checks if managed identity authentication is enabled in the local settings. * - * @param projectPath - The absolute path to the project directory containing the local settings file - * @param context - The action context used for reading local settings - * @returns A promise that resolves to `true` if MSI authentication is enabled, `false` otherwise. - * Returns `false` if the settings file cannot be read or parsed. + * @param context - The action context. + * @param projectPath - The logic apps project path. + * @returns A promise that resolves to `true` if MSI authentication is enabled, `false` otherwise. Returns `false` if the settings file cannot be read or parsed. */ -async function isMSIEnabled(projectPath: string, context: IActionContext): Promise { +async function isMISettingEnabled(context: IActionContext, projectPath: string): Promise { try { const localSettingsPath = path.join(projectPath, localSettingsFileName); const localSettings = await getLocalSettingsJson(context, localSettingsPath); - - // Retrieve the configured authentication method const authMethod = localSettings.Values?.[workflowAuthenticationMethodKey]; - - // Explicitly check for MSI, using string literal comparison instead of enum - if (typeof authMethod === 'string' && authMethod.toLowerCase() === 'managedserviceidentity') { - return true; - } - - return false; // Not MSI or invalid value + return authMethod?.toLowerCase() === workflowAuthenticationMethodMIValue.toLowerCase() } catch { - return false; // Default to false if settings can't be read or parsed + return false; } } diff --git a/apps/vs-code-designer/src/app/utils/codeless/validateProjectArtifacts.ts b/apps/vs-code-designer/src/app/utils/codeless/validateProjectArtifacts.ts index f8f9a753866..30af5d24ee5 100644 --- a/apps/vs-code-designer/src/app/utils/codeless/validateProjectArtifacts.ts +++ b/apps/vs-code-designer/src/app/utils/codeless/validateProjectArtifacts.ts @@ -19,10 +19,12 @@ import { workerRuntimeKey, workflowFileName, workflowOperationDiscoveryHostModeKey, + workflowAuthenticationMethodKey, + workflowAuthenticationMethodMIValue, } from '../../../constants'; import { localize } from '../../../localize'; import { ext } from '../../../extensionVariables'; -import { useNodeDesignTimeWorker } from '../vsCodeConfig/settings'; +import { isManagedIdentityAuthEnabled, useNodeDesignTimeWorker } from '../vsCodeConfig/settings'; import { addOrUpdateLocalAppSettings, getLocalSettingsJson, getLocalSettingsSchema } from '../appSettings/localSettings'; import { writeFormattedJson } from '../fs'; import { parseJson } from '../parseJson'; @@ -197,6 +199,10 @@ export async function regenerateLocalSettings(context: IActionContext, projectPa } } + if (isManagedIdentityAuthEnabled() && currentValues[workflowAuthenticationMethodKey] !== workflowAuthenticationMethodMIValue) { + settingsToAdd[workflowAuthenticationMethodKey] = workflowAuthenticationMethodMIValue; + } + if (!fileExisted || Object.keys(settingsToAdd).length > 0) { await addOrUpdateLocalAppSettings(context, projectPath, settingsToAdd); ext.outputChannel.appendLog( diff --git a/apps/vs-code-designer/src/app/utils/vsCodeConfig/settings.ts b/apps/vs-code-designer/src/app/utils/vsCodeConfig/settings.ts index db0f7c206ec..fa836711b3e 100644 --- a/apps/vs-code-designer/src/app/utils/vsCodeConfig/settings.ts +++ b/apps/vs-code-designer/src/app/utils/vsCodeConfig/settings.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ext } from '../../../extensionVariables'; import { localize } from '../../../localize'; -import { useNodeDesignTimeWorkerSetting } from '../../../constants'; +import { enableManagedIdentityAuthSetting, useNodeDesignTimeWorkerSetting } from '../../../constants'; import { isString } from '@microsoft/logic-apps-shared'; import type { IActionContext, IAzureQuickPickItem, IAzureQuickPickOptions } from '@microsoft/vscode-azext-utils'; import { openUrl } from '@microsoft/vscode-azext-utils'; @@ -170,6 +170,19 @@ export function useNodeDesignTimeWorker(fsPath?: string | WorkspaceFolder): bool return getWorkspaceSetting(useNodeDesignTimeWorkerSetting, fsPath) === true; } +/** + * Indicates whether the extension should enforce `WORKFLOWS_AUTHENTICATION_METHOD = managedServiceIdentity` + * in local.settings.json. Controlled by the `azureLogicAppsStandard.enableManagedIdentityAuth` setting. + * Defaults to `true` when the setting is absent or unset, or when the VS Code API is unavailable (e.g. in tests). + */ +export function isManagedIdentityAuthEnabled(): boolean { + try { + return getGlobalSetting(enableManagedIdentityAuthSetting) !== false; + } catch { + return true; + } +} + function getScope(fsPath: WorkspaceFolder | string | undefined): Uri | WorkspaceFolder | undefined { return isString(fsPath) ? Uri.file(fsPath) : fsPath; } diff --git a/apps/vs-code-designer/src/constants.ts b/apps/vs-code-designer/src/constants.ts index 865bb7489e2..441477b0ca3 100644 --- a/apps/vs-code-designer/src/constants.ts +++ b/apps/vs-code-designer/src/constants.ts @@ -277,6 +277,7 @@ export const e2eStrictDependencyValidationSettingKey = 'e2eStrictDependencyValid export const useExperimentalExtensionBundleSettingKey = 'useExperimentalExtensionBundle'; export const experimentalExtensionBundleSourceUriSettingKey = 'experimentalExtensionBundleSourceUri'; export const experimentalExtensionBundleVersionSettingKey = 'experimentalExtensionBundleVersion'; +export const enableManagedIdentityAuthSetting = 'enableManagedIdentityAuth'; export const dependencyMetadataRequestTimeoutMs = 30 * 1000; export const dependencyIntegrityManifestFileName = '.logicapps-integrity.json'; export const verifyConnectionKeysSetting = 'verifyConnectionKeys'; @@ -292,6 +293,7 @@ export const bundleSourceMd5SidecarFile = '.bundle-source-md5'; export const localEmulatorConnectionString = 'UseDevelopmentStorage=true'; export const appKindSetting = 'APP_KIND'; export const sqlStorageConnectionStringKey = 'Workflows.Sql.ConnectionString'; +export const workflowAuthenticationMethodMIValue = 'managedServiceIdentity'; export const workerRuntimeKey = 'FUNCTIONS_WORKER_RUNTIME'; export const ProjectDirectoryPathKey = 'ProjectDirectoryPath'; diff --git a/apps/vs-code-designer/src/package.json b/apps/vs-code-designer/src/package.json index c0cc6e5c234..2c5042c068f 100644 --- a/apps/vs-code-designer/src/package.json +++ b/apps/vs-code-designer/src/package.json @@ -1068,6 +1068,11 @@ "type": "string", "default": "", "markdownDescription": "Experimental: optional pinned version (for example `1.21.0-preview`). **Only honored when `#azureLogicAppsStandard.useExperimentalExtensionBundle#` is `true`.** While set, no 'latest' feed is consulted; the version is fetched from the configured source URL with the same public-CDN fallback as `#azureLogicAppsStandard.experimentalExtensionBundleSourceUri#`. If empty, the highest local version is used." + }, + "azureLogicAppsStandard.enableManagedIdentityAuth": { + "type": "boolean", + "default": true, + "description": "When enabled, automatically sets WORKFLOWS_AUTHENTICATION_METHOD to managedServiceIdentity in local.settings.json for all projects. Disable to manage this setting manually." } } } diff --git a/apps/vs-code-designer/src/test/ui/SKILL.md b/apps/vs-code-designer/src/test/ui/SKILL.md index 85eeef0dfe1..01360602e67 100644 --- a/apps/vs-code-designer/src/test/ui/SKILL.md +++ b/apps/vs-code-designer/src/test/ui/SKILL.md @@ -327,13 +327,12 @@ await input.setText('logic app workspace'); // ❌ switches to file search ### Issue: Azure connector wizard blocks designer loading (FIXED) **Symptom**: Designer never loads for CustomCode and RulesEngine workspaces. VS Code shows QuickPick prompts that require manual intervention. -**Cause**: When `WORKFLOWS_SUBSCRIPTION_ID` is undefined in `local.settings.json`, the extension shows two blocking QuickPick prompts: +**Cause**: When `WORKFLOWS_SUBSCRIPTION_ID` is undefined in `local.settings.json`, the extension shows a blocking QuickPick prompt: 1. "Enable connectors in Azure for Logic App" → "Use connectors from Azure" / "Skip for now" - 2. "Select authentication method for Azure connectors" → "Managed Service Identity" / "Connection Keys" -**Code path**: `getAzureConnectorDetailsForLocalProject()` in `apps/vs-code-designer/src/app/utils/codeless/common.ts` triggers `azureConnectorWizard.ts` which calls `authenticationMethodStep.ts`. +**Code path**: `getAzureConnectorDetailsForLocalProject()` in `apps/vs-code-designer/src/app/utils/codeless/common.ts` triggers `azureConnectorWizard.ts`. **Fix applied (2026-02-24)**: Two-layer fix in both `designerOpen.test.ts` and `designerActions.test.ts`: 1. `ensureLocalSettingsForDesigner(appDir)` — patches `local.settings.json` with `WORKFLOWS_SUBSCRIPTION_ID: ""` before opening the designer. Setting it to empty string (not undefined) prevents the wizard from launching. - 2. `handleDesignerPrompts(workbench, driver)` — fallback safety net that polls for QuickPick dialogs and auto-selects "Skip for now" / "Connection Keys" if they still appear. + 2. `handleDesignerPrompts(workbench, driver)` — fallback safety net that polls for QuickPick dialogs and auto-selects "Skip for now" if they still appear. **Standard workspaces** already had `WORKFLOWS_SUBSCRIPTION_ID: ""` in their `local.settings.json` (set by the creation wizard), so they were unaffected. ### Issue: Command palette fails to open in designer tests (FIXED) diff --git a/apps/vs-code-designer/src/test/ui/connectionPromptFallback.test.ts b/apps/vs-code-designer/src/test/ui/connectionPromptFallback.test.ts index 1dbec0846b1..2cfed56d410 100644 --- a/apps/vs-code-designer/src/test/ui/connectionPromptFallback.test.ts +++ b/apps/vs-code-designer/src/test/ui/connectionPromptFallback.test.ts @@ -146,6 +146,10 @@ describe('Connection prompt fallback E2E', function () { await driver.switchTo().defaultContent(); const localSettings = JSON.parse(fs.readFileSync(path.join(entry.appDir, 'local.settings.json'), 'utf-8')); assert.strictEqual(localSettings.Values?.WORKFLOWS_SUBSCRIPTION_ID, '', 'Cancellation should persist Azure connectors disabled'); - assert.strictEqual(localSettings.Values?.WORKFLOWS_AUTHENTICATION_METHOD, 'rawKeys', 'Cancellation should default to connection keys'); + assert.strictEqual( + localSettings.Values?.WORKFLOWS_AUTHENTICATION_METHOD, + 'managedServiceIdentity', + 'Cancellation should default to managed service identity' + ); }); });