Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 103 additions & 128 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1371,8 +1371,8 @@
"@azure/core-client": "^1.7.3",
"@azure/core-rest-pipeline": "^1.11.0",
"@azure/storage-blob": "^12.5.0",
"@microsoft/vscode-azext-azureappservice": "^3.3.1",
"@microsoft/vscode-azext-azureappsettings": "^0.2.2",
"@microsoft/vscode-azext-azureappservice": "^3.5.3",
"@microsoft/vscode-azext-azureappsettings": "^0.2.3",
"@microsoft/vscode-azext-azureutils": "^3.1.6",
"@microsoft/vscode-azext-utils": "^2.6.3",
"@microsoft/vscode-azureresources-api": "^2.0.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { type ILocalSettingsJson } from "../../../funcConfig/local.settings";
import { type LocalProjectTreeItem } from "../../../tree/localProject/LocalProjectTreeItem";
import { decryptLocalSettings } from "./decryptLocalSettings";
import { encryptLocalSettings } from "./encryptLocalSettings";
import { getLocalSettingsFileNoPrompt } from "./getLocalSettingsFile";
import { tryGetLocalSettingsFileNoPrompt } from "./getLocalSettingsFile";

export class LocalSettingsClientProvider implements AppSettingsClientProvider {
private _node: LocalProjectTreeItem;
Expand All @@ -37,7 +37,7 @@ export class LocalSettingsClient implements IAppSettingsClient {

public async listApplicationSettings(): Promise<StringDictionary> {
const result = await callWithTelemetryAndErrorHandling<StringDictionary | undefined>('listApplicationSettings', async (context: IActionContext) => {
const localSettingsPath: string | undefined = await getLocalSettingsFileNoPrompt(context, this._node.workspaceFolder);
const localSettingsPath: string | undefined = await tryGetLocalSettingsFileNoPrompt(context, this._node.workspaceFolder);
if (localSettingsPath === undefined) {
return { properties: {} };
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export async function getLocalSettingsFile(context: IActionContext, message: str
});
}

export async function getLocalSettingsFileNoPrompt(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder): Promise<string | undefined> {
const projectPath: string | undefined = await tryGetFunctionProjectRoot(context, workspaceFolder);
export async function tryGetLocalSettingsFileNoPrompt(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder | string | undefined): Promise<string | undefined> {
const projectPath: string | undefined = await tryGetFunctionProjectRoot(context, workspaceFolder ?? '');
if (projectPath) {
const localSettingsFile: string = path.join(projectPath, localSettingsFileName);
if (await AzExtFsExtra.pathExists(localSettingsFile)) {
Expand Down
25 changes: 21 additions & 4 deletions src/commands/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type Site, type SiteConfigResource } from '@azure/arm-appservice';
import { type Site, type SiteConfigResource, type StringDictionary } from '@azure/arm-appservice';
import { getDeployFsPath, getDeployNode, deploy as innerDeploy, showDeployConfirmation, type IDeployContext, type IDeployPaths } from '@microsoft/vscode-azext-azureappservice';
import { DialogResponses, type ExecuteActivityContext, type IActionContext } from '@microsoft/vscode-azext-utils';
import { type AzureSubscription } from '@microsoft/vscode-azureresources-api';
Expand All @@ -27,6 +27,7 @@ import { validateEventHubsConnection } from '../appSettings/connectionSettings/e
import { validateSqlDbConnection } from '../appSettings/connectionSettings/sqlDatabase/validateSqlDbConnection';
import { tryGetFunctionProjectRoot } from '../createNewProject/verifyIsProject';
import { getOrCreateFunctionApp } from './getOrCreateFunctionApp';
import { getWarningsForConnectionSettings } from './getWarningsForConnectionSettings';
import { notifyDeployComplete } from './notifyDeployComplete';
import { runPreDeployTask } from './runPreDeployTask';
import { shouldValidateConnections } from './shouldValidateConnection';
Expand Down Expand Up @@ -141,9 +142,24 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
await validateSqlDbConnection(context, context.projectPath);
}

if (getWorkspaceSetting<boolean>('showDeployConfirmation', context.workspaceFolder.uri.fsPath) && !context.isNewApp && isZipDeploy) {
const appSettings: StringDictionary = await client.listApplicationSettings();

const deploymentWarningMessages: string[] = [];
const connectionStringWarningMessage = await getWarningsForConnectionSettings(context, {
appSettings,
node,
projectPath: context.projectPath
});

if (connectionStringWarningMessage) {
deploymentWarningMessages.push(connectionStringWarningMessage);
}

if ((getWorkspaceSetting<boolean>('showDeployConfirmation', context.workspaceFolder.uri.fsPath) && !context.isNewApp && isZipDeploy) ||
deploymentWarningMessages.length > 0) {
// if there is a warning message, we want to show the deploy confirmation regardless of the setting
const deployCommandId = 'azureFunctions.deploy';
await showDeployConfirmation(context, node.site, deployCommandId);
await showDeployConfirmation(context, node.site, deployCommandId, deploymentWarningMessages);
}

await runPreDeployTask(context, context.effectiveDeployFsPath, siteConfig.scmType);
Expand All @@ -166,7 +182,8 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
language,
languageModel,
bools: { doRemoteBuild, isConsumption },
durableStorageType
durableStorageType,
appSettings
});
}

Expand Down
98 changes: 98 additions & 0 deletions src/commands/deploy/getWarningsForConnectionSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { type StringDictionary } from "@azure/arm-appservice";
import { isSettingConvertible } from '@microsoft/vscode-azext-azureappsettings';
import { AzExtFsExtra, type IActionContext } from "@microsoft/vscode-azext-utils";
import { localEventHubsEmulatorConnectionRegExp, localStorageEmulatorConnectionString } from "../../constants";
import { type ILocalSettingsJson } from "../../funcConfig/local.settings";
import { localize } from "../../localize";
import { type SlotTreeItem } from "../../tree/SlotTreeItem";
import { tryGetLocalSettingsFileNoPrompt } from "../appSettings/localSettings/getLocalSettingsFile";

type ConnectionSetting = { name: string, value: string, type: 'ConnectionString' | 'ManagedIdentity' | 'Emulator' };

export async function getWarningsForConnectionSettings(context: IActionContext,
options: {
appSettings: StringDictionary,
node: SlotTreeItem,
projectPath: string | undefined
}): Promise<string | undefined> {

const localSettingsPath = await tryGetLocalSettingsFileNoPrompt(context, options.projectPath);
const localSettings: ILocalSettingsJson = localSettingsPath ? await AzExtFsExtra.readJSON(localSettingsPath) : { Values: {} };
const localConnectionSettings = await getConnectionSettings(localSettings.Values ?? {});
const remoteConnectionSettings = await getConnectionSettings(options.appSettings?.properties ?? {});

if (localConnectionSettings.some(setting => setting.type === 'ManagedIdentity')) {
if (!options.node.site.rawSite.identity ||
options.node.site.rawSite.identity.type === 'None') {
// if they have nothing in remote, warn them to connect a managed identity
return localize('configureManagedIdentityWarning',
'Your app is not connected to a managed identity. To ensure access, please configure a managed identity. Without it, your application may encounter authorization issues.');
}
}

if (localConnectionSettings.some(setting => setting.type === 'ConnectionString') || remoteConnectionSettings.some(setting => setting.type === 'ConnectionString')) {
// if they have connection strings, warn them about insecure connections but don't try to convert them
return localize('connectionStringWarning',
'Your app may be using connection strings for authentication. This may expose sensitive credentials and lead to security vulnerabilities. Consider using managed identities to enhance security.')
}

return;
}

function checkForConnectionSettings(property: { [propertyName: string]: string }): ConnectionSetting | undefined {
if (isSettingConvertible(property.propertyName, property.value)) {
// if the setting is convertible, we can assume it's a connection string
return {
name: property.propertyName,
value: property.value,
type: 'ConnectionString'
};
}

return undefined;
}
function checkForManagedIdentitySettings(property: { [propertyName: string]: string }): ConnectionSetting | undefined {

if (property.propertyName.includes('__accountName') || property.propertyName.includes('__blobServiceUri') ||
property.propertyName.includes('__queueServiceUri') || property.propertyName.includes('__tableServiceUri') ||
property.propertyName.includes('__accountEndpoint') || property.propertyName.includes('__fullyQualifiedNamespace')) {
return {
name: property.propertyName,
value: property.value,
type: 'ManagedIdentity'
};
}

return undefined;
}

function checkForEmulatorSettings(property: { [propertyName: string]: string }): ConnectionSetting | undefined {
Copy link
Contributor

@MicroFish91 MicroFish91 Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the future, if we need to add more emulator strings, we'll have to remember to update them here as well. I'm wondering if it might be more foolproof to create a centralized list of emulator strings as part of our constants and then loop over that list instead to perform the checks. This way, we would only need to maintain the single list at the centralized location. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's a good idea.

if (property.value.includes(localStorageEmulatorConnectionString) ||
localEventHubsEmulatorConnectionRegExp.test(property.value)) {
return {
name: property.propertyName,
value: property.value,
type: 'Emulator'
};
}

return undefined;
}

async function getConnectionSettings(properties: StringDictionary): Promise<ConnectionSetting[]> {
const settings: ConnectionSetting[] = [];
for (const [key, value] of Object.entries(properties)) {
const property = { propertyName: key, value: value as string };
const connectionSetting = checkForManagedIdentitySettings(property) ?? checkForConnectionSettings(property) ?? checkForEmulatorSettings(property);
if (connectionSetting) {
settings.push(connectionSetting);
}
}

return settings;
}
5 changes: 3 additions & 2 deletions src/commands/deploy/verifyAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ export async function verifyAppSettings(options: {
language: ProjectLanguage,
languageModel: number | undefined,
bools: VerifyAppSettingBooleans,
durableStorageType: DurableBackendValues | undefined
durableStorageType: DurableBackendValues | undefined,
appSettings: StringDictionary,
}): Promise<void> {

const { context, node, projectPath, version, language, bools, durableStorageType } = options;
const client = await node.site.createClient(context);
const appSettings: StringDictionary = await client.listApplicationSettings();
const appSettings: StringDictionary = options.appSettings;
if (appSettings.properties) {
const remoteRuntime: string | undefined = appSettings.properties[workerRuntimeKey];
await verifyVersionAndLanguage(context, projectPath, node.site.fullName, version, language, appSettings.properties);
Expand Down