Skip to content

Commit 7cf49f7

Browse files
authored
Check for connection strings and managed identity settings on deployment (#4423)
* Detect local/remote connection settings * Detect local/remote connection settings * Warn users about connection strings/mi usage * Remove comment * Update appservice package
1 parent ccf44ea commit 7cf49f7

File tree

8 files changed

+136
-16
lines changed

8 files changed

+136
-16
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1372,7 +1372,7 @@
13721372
"@azure/core-rest-pipeline": "^1.11.0",
13731373
"@azure/storage-blob": "^12.5.0",
13741374
"@microsoft/vscode-azext-azureappservice": "^3.5.3",
1375-
"@microsoft/vscode-azext-azureappsettings": "^0.2.2",
1375+
"@microsoft/vscode-azext-azureappsettings": "^0.2.3",
13761376
"@microsoft/vscode-azext-azureutils": "^3.1.6",
13771377
"@microsoft/vscode-azext-utils": "^2.6.3",
13781378
"@microsoft/vscode-azureresources-api": "^2.0.4",

src/commands/appSettings/localSettings/LocalSettingsClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { type ILocalSettingsJson } from "../../../funcConfig/local.settings";
1111
import { type LocalProjectTreeItem } from "../../../tree/localProject/LocalProjectTreeItem";
1212
import { decryptLocalSettings } from "./decryptLocalSettings";
1313
import { encryptLocalSettings } from "./encryptLocalSettings";
14-
import { getLocalSettingsFileNoPrompt } from "./getLocalSettingsFile";
14+
import { tryGetLocalSettingsFileNoPrompt } from "./getLocalSettingsFile";
1515

1616
export class LocalSettingsClientProvider implements AppSettingsClientProvider {
1717
private _node: LocalProjectTreeItem;
@@ -37,7 +37,7 @@ export class LocalSettingsClient implements IAppSettingsClient {
3737

3838
public async listApplicationSettings(): Promise<StringDictionary> {
3939
const result = await callWithTelemetryAndErrorHandling<StringDictionary | undefined>('listApplicationSettings', async (context: IActionContext) => {
40-
const localSettingsPath: string | undefined = await getLocalSettingsFileNoPrompt(context, this._node.workspaceFolder);
40+
const localSettingsPath: string | undefined = await tryGetLocalSettingsFileNoPrompt(context, this._node.workspaceFolder);
4141
if (localSettingsPath === undefined) {
4242
return { properties: {} };
4343
} else {

src/commands/appSettings/localSettings/getLocalSettingsFile.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ export async function getLocalSettingsFile(context: IActionContext, message: str
3232
});
3333
}
3434

35-
export async function getLocalSettingsFileNoPrompt(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder): Promise<string | undefined> {
36-
const projectPath: string | undefined = await tryGetFunctionProjectRoot(context, workspaceFolder);
35+
export async function tryGetLocalSettingsFileNoPrompt(context: IActionContext, workspaceFolder: vscode.WorkspaceFolder | string | undefined): Promise<string | undefined> {
36+
const projectPath: string | undefined = await tryGetFunctionProjectRoot(context, workspaceFolder ?? '');
3737
if (projectPath) {
3838
const localSettingsFile: string = path.join(projectPath, localSettingsFileName);
3939
if (await AzExtFsExtra.pathExists(localSettingsFile)) {

src/commands/createFunctionApp/FunctionAppCreateStep.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { type Sku } from './stacks/models/FlexSkuModel';
2727
import { type FunctionAppRuntimeSettings, } from './stacks/models/FunctionAppStackModel';
2828

2929
export class FunctionAppCreateStep extends AzureWizardExecuteStep<IFunctionAppWizardContext> {
30-
public priority: number = 140;
30+
public priority: number = 1000;
3131

3232
public async execute(context: IFlexFunctionAppWizardContext, progress: Progress<{ message?: string; increment?: number }>): Promise<void> {
3333
const os: WebsiteOS = nonNullProp(context, 'newSiteOS');

src/commands/deploy/deploy.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { type Site, type SiteConfigResource } from '@azure/arm-appservice';
6+
import { type Site, type SiteConfigResource, type StringDictionary } from '@azure/arm-appservice';
77
import { getDeployFsPath, getDeployNode, deploy as innerDeploy, showDeployConfirmation, type IDeployContext, type IDeployPaths } from '@microsoft/vscode-azext-azureappservice';
88
import { DialogResponses, type ExecuteActivityContext, type IActionContext, type ISubscriptionActionContext } from '@microsoft/vscode-azext-utils';
99
import { type AzureSubscription } from '@microsoft/vscode-azureresources-api';
@@ -28,6 +28,7 @@ import { validateSqlDbConnection } from '../appSettings/connectionSettings/sqlDa
2828
import { getEolWarningMessages } from '../createFunctionApp/stacks/getStackPicks';
2929
import { tryGetFunctionProjectRoot } from '../createNewProject/verifyIsProject';
3030
import { getOrCreateFunctionApp } from './getOrCreateFunctionApp';
31+
import { getWarningsForConnectionSettings } from './getWarningsForConnectionSettings';
3132
import { notifyDeployComplete } from './notifyDeployComplete';
3233
import { runPreDeployTask } from './runPreDeployTask';
3334
import { shouldValidateConnections } from './shouldValidateConnection';
@@ -142,6 +143,19 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
142143
await validateSqlDbConnection(context, context.projectPath);
143144
}
144145

146+
const appSettings: StringDictionary = await client.listApplicationSettings();
147+
148+
const deploymentWarningMessages: string[] = [];
149+
const connectionStringWarningMessage = await getWarningsForConnectionSettings(context, {
150+
appSettings,
151+
node,
152+
projectPath: context.projectPath
153+
});
154+
155+
if (connectionStringWarningMessage) {
156+
deploymentWarningMessages.push(connectionStringWarningMessage);
157+
}
158+
145159
const subContext = {
146160
...context
147161
} as unknown as ISubscriptionActionContext;
@@ -153,9 +167,15 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
153167
client
154168
});
155169

156-
if ((getWorkspaceSetting<boolean>('showDeployConfirmation', context.workspaceFolder.uri.fsPath) && !context.isNewApp && isZipDeploy) || eolWarningMessage) {
170+
if (eolWarningMessage) {
171+
deploymentWarningMessages.push(eolWarningMessage);
172+
}
173+
174+
if ((getWorkspaceSetting<boolean>('showDeployConfirmation', context.workspaceFolder.uri.fsPath) && !context.isNewApp && isZipDeploy) ||
175+
deploymentWarningMessages.length > 0) {
176+
// if there is a warning message, we want to show the deploy confirmation regardless of the setting
157177
const deployCommandId = 'azureFunctions.deploy';
158-
await showDeployConfirmation(context, node.site, deployCommandId, [eolWarningMessage]);
178+
await showDeployConfirmation(context, node.site, deployCommandId, deploymentWarningMessages);
159179
}
160180

161181
await runPreDeployTask(context, context.effectiveDeployFsPath, siteConfig.scmType);
@@ -178,7 +198,8 @@ async function deploy(actionContext: IActionContext, arg1: vscode.Uri | string |
178198
language,
179199
languageModel,
180200
bools: { doRemoteBuild, isConsumption },
181-
durableStorageType
201+
durableStorageType,
202+
appSettings
182203
});
183204
}
184205

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { type StringDictionary } from "@azure/arm-appservice";
7+
import { isSettingConvertible } from '@microsoft/vscode-azext-azureappsettings';
8+
import { AzExtFsExtra, type IActionContext } from "@microsoft/vscode-azext-utils";
9+
import { localEventHubsEmulatorConnectionRegExp, localStorageEmulatorConnectionString } from "../../constants";
10+
import { type ILocalSettingsJson } from "../../funcConfig/local.settings";
11+
import { localize } from "../../localize";
12+
import { type SlotTreeItem } from "../../tree/SlotTreeItem";
13+
import { tryGetLocalSettingsFileNoPrompt } from "../appSettings/localSettings/getLocalSettingsFile";
14+
15+
type ConnectionSetting = { name: string, value: string, type: 'ConnectionString' | 'ManagedIdentity' | 'Emulator' };
16+
17+
export async function getWarningsForConnectionSettings(context: IActionContext,
18+
options: {
19+
appSettings: StringDictionary,
20+
node: SlotTreeItem,
21+
projectPath: string | undefined
22+
}): Promise<string | undefined> {
23+
24+
const localSettingsPath = await tryGetLocalSettingsFileNoPrompt(context, options.projectPath);
25+
const localSettings: ILocalSettingsJson = localSettingsPath ? await AzExtFsExtra.readJSON(localSettingsPath) : { Values: {} };
26+
const localConnectionSettings = await getConnectionSettings(localSettings.Values ?? {});
27+
const remoteConnectionSettings = await getConnectionSettings(options.appSettings?.properties ?? {});
28+
29+
if (localConnectionSettings.some(setting => setting.type === 'ManagedIdentity')) {
30+
if (!options.node.site.rawSite.identity ||
31+
options.node.site.rawSite.identity.type === 'None') {
32+
// if they have nothing in remote, warn them to connect a managed identity
33+
return localize('configureManagedIdentityWarning',
34+
'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.');
35+
}
36+
}
37+
38+
if (localConnectionSettings.some(setting => setting.type === 'ConnectionString') || remoteConnectionSettings.some(setting => setting.type === 'ConnectionString')) {
39+
// if they have connection strings, warn them about insecure connections but don't try to convert them
40+
return localize('connectionStringWarning',
41+
'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.')
42+
}
43+
44+
return;
45+
}
46+
47+
function checkForConnectionSettings(property: { [propertyName: string]: string }): ConnectionSetting | undefined {
48+
if (isSettingConvertible(property.propertyName, property.value)) {
49+
// if the setting is convertible, we can assume it's a connection string
50+
return {
51+
name: property.propertyName,
52+
value: property.value,
53+
type: 'ConnectionString'
54+
};
55+
}
56+
57+
return undefined;
58+
}
59+
function checkForManagedIdentitySettings(property: { [propertyName: string]: string }): ConnectionSetting | undefined {
60+
61+
if (property.propertyName.includes('__accountName') || property.propertyName.includes('__blobServiceUri') ||
62+
property.propertyName.includes('__queueServiceUri') || property.propertyName.includes('__tableServiceUri') ||
63+
property.propertyName.includes('__accountEndpoint') || property.propertyName.includes('__fullyQualifiedNamespace')) {
64+
return {
65+
name: property.propertyName,
66+
value: property.value,
67+
type: 'ManagedIdentity'
68+
};
69+
}
70+
71+
return undefined;
72+
}
73+
74+
function checkForEmulatorSettings(property: { [propertyName: string]: string }): ConnectionSetting | undefined {
75+
if (property.value.includes(localStorageEmulatorConnectionString) ||
76+
localEventHubsEmulatorConnectionRegExp.test(property.value)) {
77+
return {
78+
name: property.propertyName,
79+
value: property.value,
80+
type: 'Emulator'
81+
};
82+
}
83+
84+
return undefined;
85+
}
86+
87+
async function getConnectionSettings(properties: StringDictionary): Promise<ConnectionSetting[]> {
88+
const settings: ConnectionSetting[] = [];
89+
for (const [key, value] of Object.entries(properties)) {
90+
const property = { propertyName: key, value: value as string };
91+
const connectionSetting = checkForManagedIdentitySettings(property) ?? checkForConnectionSettings(property) ?? checkForEmulatorSettings(property);
92+
if (connectionSetting) {
93+
settings.push(connectionSetting);
94+
}
95+
}
96+
97+
return settings;
98+
}

src/commands/deploy/verifyAppSettings.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ export async function verifyAppSettings(options: {
2929
language: ProjectLanguage,
3030
languageModel: number | undefined,
3131
bools: VerifyAppSettingBooleans,
32-
durableStorageType: DurableBackendValues | undefined
32+
durableStorageType: DurableBackendValues | undefined,
33+
appSettings: StringDictionary,
3334
}): Promise<void> {
3435

3536
const { context, node, projectPath, version, language, bools, durableStorageType } = options;
3637
const client = await node.site.createClient(context);
37-
const appSettings: StringDictionary = await client.listApplicationSettings();
38+
const appSettings: StringDictionary = options.appSettings;
3839
if (appSettings.properties) {
3940
const remoteRuntime: string | undefined = appSettings.properties[workerRuntimeKey];
4041
await verifyVersionAndLanguage(context, projectPath, node.site.fullName, version, language, appSettings.properties);

0 commit comments

Comments
 (0)