Skip to content

Commit df78db4

Browse files
committed
Improve support for assigning UAID. Supporting adding newly created UAID.
1 parent b55c245 commit df78db4

File tree

5 files changed

+81
-58
lines changed

5 files changed

+81
-58
lines changed

src/commands/appSettings/connectionSettings/durableTaskScheduler/IDTSConnectionWizardContext.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { type AzureSubscription } from "@microsoft/vscode-azureresources-api";
88
import { type ConnectionType } from "../../../../constants";
99
import { type DurableTaskHubResource, type DurableTaskSchedulerResource } from "../../../../tree/durableTaskScheduler/DurableTaskSchedulerClient";
1010
import { type DurableTaskSchedulerEmulator } from "../../../../tree/durableTaskScheduler/DurableTaskSchedulerEmulatorClient";
11-
import { type IFunctionAppUserAssignedIdentitiesContext } from "../../../identity/listUserAssignedIdentities/IFunctionAppUserAssignedIdentitiesContext";
11+
import { type ManagedIdentityAssignContext } from "../../../identity/ManagedIdentityAssignContext";
1212
import { type StorageConnectionType } from "../IConnectionTypesContext";
1313
import { type ISetConnectionSettingContext } from "../ISetConnectionSettingContext";
1414

@@ -24,7 +24,7 @@ export interface IDTSConnectionWizardContext extends IActionContext, ISetConnect
2424
// All properties from `IDTSConnectionSetSettingsContext` apply
2525
}
2626

27-
export interface IDTSAzureConnectionWizardContext extends IFunctionAppUserAssignedIdentitiesContext, IDTSConnectionWizardContext, Partial<ExecuteActivityContext> {
27+
export interface IDTSAzureConnectionWizardContext extends ManagedIdentityAssignContext, IDTSConnectionWizardContext, ExecuteActivityContext {
2828
subscription?: AzureSubscription;
2929

3030
newDTSName?: string;

src/commands/appSettings/connectionSettings/durableTaskScheduler/azure/DurableTaskSchedulerListStep.ts

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

6-
import { CommonRoleDefinitions, createRoleId, LocationListStep, parseAzureResourceId, RoleAssignmentExecuteStep, type ILocationWizardContext, type Role } from '@microsoft/vscode-azext-azureutils';
6+
import { CommonRoleDefinitions, createAuthorizationManagementClient, createRoleId, LocationListStep, parseAzureResourceId, RoleAssignmentExecuteStep, uiUtils, type ILocationWizardContext, type Role } from '@microsoft/vscode-azext-azureutils';
77
import { AzureWizardPromptStep, nonNullProp, type AzureWizardExecuteStep, type IAzureQuickPickItem, type IWizardOptions } from '@microsoft/vscode-azext-utils';
88
import { localSettingsDescription } from '../../../../../constants-nls';
99
import { localize } from '../../../../../localize';
1010
import { HttpDurableTaskSchedulerClient, type DurableTaskSchedulerClient, type DurableTaskSchedulerResource } from '../../../../../tree/durableTaskScheduler/DurableTaskSchedulerClient';
11-
import { FunctionAppUserAssignedIdentitiesListStep } from '../../../../identity/listUserAssignedIdentities/FunctionAppUserAssignedIdentitiesListStep';
11+
import { FunctionAppUserAssignedIdentitiesListStep } from '../../../../identity/FunctionAppUserAssignedIdentitiesListStep';
1212
import { type IDTSAzureConnectionWizardContext } from '../IDTSConnectionWizardContext';
1313
import { DurableTaskHubListStep } from './DurableTaskHubListStep';
1414
import { DurableTaskSchedulerCreateStep } from './DurableTaskSchedulerCreateStep';
@@ -58,22 +58,35 @@ export class DurableTaskSchedulerListStep<T extends IDTSAzureConnectionWizardCon
5858
roleDefinitionName: CommonRoleDefinitions.durableTaskDataContributor.roleName,
5959
};
6060

61-
const identitiesListStep = new FunctionAppUserAssignedIdentitiesListStep(dtsContributorRole /** targetRole */);
62-
promptSteps.push(identitiesListStep);
63-
executeSteps.push(new RoleAssignmentExecuteStep(getDTSRoleAssignmentCallback(context, identitiesListStep, dtsContributorRole)));
61+
promptSteps.push(new FunctionAppUserAssignedIdentitiesListStep(dtsContributorRole /** targetRole */, { identityAssignStepPriority: 180 }));
62+
executeSteps.push(new RoleAssignmentExecuteStep(getDTSRoleAssignmentCallback(context, dtsContributorRole), { priority: 190 }));
6463

6564
return { promptSteps, executeSteps };
6665

67-
function getDTSRoleAssignmentCallback(context: T, functionAppIdentitiesListStep: FunctionAppUserAssignedIdentitiesListStep<T>, role: Role): () => Role[] {
68-
return () => {
66+
function getDTSRoleAssignmentCallback(context: T, role: Role): () => Promise<Role[]> {
67+
return async () => {
6968
const roleAssignment: Role = {
7069
...role,
7170
// This id may be missing when the role is initially passed in,
7271
// but by the time we run the step, we should have the populated id ready.
7372
scopeId: context.dts?.id,
7473
};
7574

76-
return functionAppIdentitiesListStep.hasIdentityWithTargetRole ? [] : [roleAssignment];
75+
if (!roleAssignment.scopeId) {
76+
return [];
77+
}
78+
79+
const amClient = await createAuthorizationManagementClient(context);
80+
const roleAssignments = await uiUtils.listAllIterator(amClient.roleAssignments.listForScope(
81+
roleAssignment.scopeId,
82+
{
83+
// $filter=principalId eq {id}
84+
filter: `principalId eq '{${context.managedIdentity?.principalId}}'`,
85+
}
86+
));
87+
88+
const hasRoleAssignment = roleAssignments.some(r => !!r.roleDefinitionId?.endsWith(role.roleDefinitionId));
89+
return hasRoleAssignment ? [] : [roleAssignment];
7790
};
7891
}
7992
}
Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55

66
import { type ManagedServiceIdentityClient } from '@azure/arm-msi';
77
import { type ParsedSite } from '@microsoft/vscode-azext-azureappservice';
8-
import { createAuthorizationManagementClient, createManagedServiceIdentityClient, parseAzureResourceId, uiUtils, type ParsedAzureResourceId, type Role } from '@microsoft/vscode-azext-azureutils';
9-
import { ActivityChildItem, ActivityChildType, activityInfoContext, activityInfoIcon, AzureWizardPromptStep, createContextValue, nonNullProp, prependOrInsertAfterLastInfoChild, type ActivityInfoChild, type IAzureQuickPickItem } from '@microsoft/vscode-azext-utils';
10-
import { ext } from '../../../extensionVariables';
11-
import { localize } from '../../../localize';
12-
import { type IFunctionAppUserAssignedIdentitiesContext } from './IFunctionAppUserAssignedIdentitiesContext';
8+
import { createAuthorizationManagementClient, createManagedServiceIdentityClient, parseAzureResourceId, uiUtils, UserAssignedIdentityListStep, type ParsedAzureResourceId, type Role } from '@microsoft/vscode-azext-azureutils';
9+
import { ActivityChildItem, ActivityChildType, activityInfoContext, activityInfoIcon, AzureWizardPromptStep, createContextValue, nonNullProp, prependOrInsertAfterLastInfoChild, type ActivityInfoChild, type IAzureQuickPickItem, type IWizardOptions } from '@microsoft/vscode-azext-utils';
10+
import { ext } from '../../extensionVariables';
11+
import { localize } from '../../localize';
12+
import { type ManagedIdentityAssignContext } from './ManagedIdentityAssignContext';
13+
import { ManagedIdentityAssignStep } from './ManagedIdentityAssignStep';
1314

1415
/**
1516
* Wizard step to select a user-assigned managed identity from the parsed site of a function app.
@@ -20,30 +21,22 @@ import { type IFunctionAppUserAssignedIdentitiesContext } from './IFunctionAppUs
2021
*
2122
* @populates `context.managedIdentity`
2223
*/
23-
export class FunctionAppUserAssignedIdentitiesListStep<T extends IFunctionAppUserAssignedIdentitiesContext> extends AzureWizardPromptStep<T> {
24+
export class FunctionAppUserAssignedIdentitiesListStep<T extends ManagedIdentityAssignContext> extends AzureWizardPromptStep<T> {
2425
private _msiClient: ManagedServiceIdentityClient;
25-
private _hasTargetRole?: boolean;
2626

27-
constructor(readonly targetRole?: Role) {
27+
constructor(
28+
readonly targetRole?: Role,
29+
readonly options?: { identityAssignStepPriority?: number },
30+
) {
2831
super();
2932
}
3033

31-
/**
32-
* Indicates whether there is at least one user-assigned identity on the function app with the provided role.
33-
* If no role is provided, or if the step has not yet been run, this will return `undefined`.
34-
*/
35-
get hasIdentityWithTargetRole(): boolean | undefined {
36-
return this._hasTargetRole;
37-
}
38-
3934
// Verify if any of the existing user assigned identities for the function app have the required role already
4035
public async configureBeforePrompt(context: T): Promise<void> {
41-
if (!this.targetRole || !this.targetRole?.scopeId) {
42-
this._hasTargetRole = undefined;
36+
if (!this.targetRole?.scopeId) {
4337
return;
4438
}
4539

46-
this._hasTargetRole = false;
4740
this._msiClient ??= await createManagedServiceIdentityClient(context);
4841
const amClient = await createAuthorizationManagementClient(context);
4942

@@ -52,6 +45,7 @@ export class FunctionAppUserAssignedIdentitiesListStep<T extends IFunctionAppUse
5245
const identityIds: string[] = Object.keys(site.identity?.userAssignedIdentities ?? {}) ?? [];
5346
context.telemetry.properties.functionAppUserAssignedIdentityCount = String(identityIds.length);
5447

48+
let hasTargetRole: boolean = false;
5549
for (const identityId of identityIds) {
5650
const uaid = site.identity?.userAssignedIdentities?.[identityId];
5751
const roleAssignments = await uiUtils.listAllIterator(amClient.roleAssignments.listForScope(
@@ -65,20 +59,21 @@ export class FunctionAppUserAssignedIdentitiesListStep<T extends IFunctionAppUse
6559
if (roleAssignments.some(r => !!r.roleDefinitionId?.endsWith(role.roleDefinitionId))) {
6660
const parsedIdentity = parseAzureResourceId(identityId);
6761
context.managedIdentity = await this._msiClient.userAssignedIdentities.get(parsedIdentity.resourceGroup, parsedIdentity.resourceName);
68-
this._hasTargetRole = true;
62+
hasTargetRole = true;
6963
break;
7064
}
7165
}
7266

73-
context.telemetry.properties.functionAppHasIdentityWithTargetRole = String(this.hasIdentityWithTargetRole);
67+
context.telemetry.properties.functionAppHasIdentityWithTargetRole = String(hasTargetRole);
7468

75-
if (this.hasIdentityWithTargetRole) {
69+
if (hasTargetRole) {
7670
prependOrInsertAfterLastInfoChild(context,
7771
new ActivityChildItem({
72+
stepId: this.id,
7873
label: localize('useIdentityWithRole', 'Use identity "{0}" with role "{1}"', context.managedIdentity?.name, this.targetRole.roleDefinitionName),
7974
contextValue: createContextValue(['functionAppUserAssignedIdentitiesListStepItem', activityInfoContext]),
8075
activityType: ActivityChildType.Info,
81-
iconPath: activityInfoIcon
76+
iconPath: activityInfoIcon,
8277
}) as ActivityInfoChild,
8378
);
8479
ext.outputChannel.appendLog(localize('foundIdentity', 'Located existing user assigned identity "{0}" with role "{1}".', context.managedIdentity?.name, this.targetRole.roleDefinitionName));
@@ -89,12 +84,14 @@ export class FunctionAppUserAssignedIdentitiesListStep<T extends IFunctionAppUse
8984

9085
public async prompt(context: T): Promise<void> {
9186
const site: ParsedSite = nonNullProp(context, 'site');
92-
const identityId: string = (await context.ui.showQuickPick(await this.getPicks(site), {
87+
const identityId: string | undefined = (await context.ui.showQuickPick(await this.getPicks(site), {
9388
placeHolder: localize('selectFunctionAppIdentity', 'Select a function app identity for new role assignments'),
94-
// Todo: Remove when create + assign is implemented
95-
noPicksMessage: localize('noUserAssignedIdentities', 'No identities found. Add a user assigned identity to the function app before proceeding.'),
9689
})).data;
9790

91+
if (!identityId) {
92+
return;
93+
}
94+
9895
const parsedIdentity: ParsedAzureResourceId = parseAzureResourceId(identityId);
9996
this._msiClient ??= await createManagedServiceIdentityClient(context);
10097

@@ -106,14 +103,34 @@ export class FunctionAppUserAssignedIdentitiesListStep<T extends IFunctionAppUse
106103
return !context.managedIdentity;
107104
}
108105

109-
private async getPicks(site: ParsedSite): Promise<IAzureQuickPickItem<string>[]> {
110-
return Object.keys(site.identity?.userAssignedIdentities ?? {}).map((id) => {
111-
const parsedResource: ParsedAzureResourceId = parseAzureResourceId(id);
112-
return {
113-
label: parsedResource.resourceName,
114-
description: parsedResource.resourceGroup,
115-
data: id,
116-
};
117-
});
106+
public async getSubWizard(context: T): Promise<IWizardOptions<T> | undefined> {
107+
if (context.managedIdentity) {
108+
return undefined;
109+
}
110+
111+
return {
112+
promptSteps: [new UserAssignedIdentityListStep()],
113+
executeSteps: [new ManagedIdentityAssignStep({ priority: this.options?.identityAssignStepPriority })],
114+
};
115+
}
116+
117+
private async getPicks(site: ParsedSite): Promise<IAzureQuickPickItem<string | undefined>[]> {
118+
const picks: IAzureQuickPickItem<string | undefined>[] = [{
119+
label: localize('assignIdentity', '$(plus) Assign new user-assigned identity'),
120+
data: undefined,
121+
}];
122+
123+
return picks.concat(
124+
Object
125+
.keys(site.identity?.userAssignedIdentities ?? {})
126+
.map((id) => {
127+
const parsedResource: ParsedAzureResourceId = parseAzureResourceId(id);
128+
return {
129+
label: parsedResource.resourceName,
130+
description: parsedResource.resourceGroup,
131+
data: id,
132+
};
133+
}),
134+
);
118135
}
119136
}

src/commands/identity/ManagedIdentityAssignStep.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import { localize } from "../../localize";
1212
import { type ManagedIdentityAssignContext } from "./ManagedIdentityAssignContext";
1313

1414
export class ManagedIdentityAssignStep extends AzureWizardExecuteStep<ManagedIdentityAssignContext> {
15-
public priority: number = 500;
15+
public priority: number;
16+
17+
constructor(options?: { priority?: number }) {
18+
super();
19+
this.priority = options?.priority ?? 500;
20+
}
1621

1722
public async execute(context: ManagedIdentityAssignContext, _progress: Progress<{ message?: string | undefined; increment?: number | undefined; }>): Promise<void> {
1823
const site: ParsedSite = nonNullProp(context, 'site');

src/commands/identity/listUserAssignedIdentities/IFunctionAppUserAssignedIdentitiesContext.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)