From 7df6d333ded7085e7e2155ec6034608392c75a06 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Fri, 19 Jul 2024 15:14:18 -0700 Subject: [PATCH 1/9] Add wizard steps to list/create UserAssignedIdentities and execute role definitions --- azure/index.d.ts | 45 ++++++++++++ azure/package-lock.json | 52 +++++++++++++- azure/package.json | 5 +- azure/src/clients.ts | 16 ++++- azure/src/constants.ts | 2 +- azure/src/index.ts | 3 + azure/src/wizard/RoleAssignmentExecuteStep.ts | 42 +++++++++++ .../wizard/UserAssignedIdentityCreateStep.ts | 52 ++++++++++++++ .../wizard/UserAssignedIdentityListStep.ts | 69 +++++++++++++++++++ 9 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 azure/src/wizard/RoleAssignmentExecuteStep.ts create mode 100644 azure/src/wizard/UserAssignedIdentityCreateStep.ts create mode 100644 azure/src/wizard/UserAssignedIdentityListStep.ts diff --git a/azure/index.d.ts b/azure/index.d.ts index a74cf1e13b..5eecd4ea44 100644 --- a/azure/index.d.ts +++ b/azure/index.d.ts @@ -5,6 +5,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { Identity } from '@azure/arm-msi'; import type { ExtendedLocation, ResourceGroup } from '@azure/arm-resources'; import type { Location } from '@azure/arm-resources-subscriptions'; import type { StorageAccount } from '@azure/arm-storage'; @@ -212,6 +213,12 @@ export interface IResourceGroupWizardContext extends ILocationWizardContext, IRe */ suppress403Handling?: boolean; + /** + * The managed identity to be used for the new target resource + * Service resource, such as storage, should be add a role assignment + */ + managedIdentity?: Identity; + ui: IAzureUserInput; } @@ -343,6 +350,40 @@ export declare class StorageAccountCreateStep extends AzureWizardPromptStep { + public constructor(suppressCreate?: boolean); + + public prompt(wizardContext: T): Promise; + public shouldPrompt(wizardContext: T): boolean; +} + +export declare class UserAssignedIdentityCreateStep extends AzureWizardExecuteStep { + /** + * 140 + */ + public priority: number; + public constructor(); + + public execute(wizardContext: T, progress: Progress<{ message?: string; increment?: number }>): Promise; + public shouldExecute(wizardContext: T): boolean; +} + +export declare class RoleAssignmentExecuteStep extends AzureWizardExecuteStep { + /** + * 900 + */ + public priority: number; + /** + * @param getScopeId A function that returns the scope id for the role assignment. This typically won't exist until _after_ the resource is created, + * which is why it's a function that returns a string. If the scope id is undefined, the step will throw an error. + * @param roleDefinitionId The id of the role definition to assign. Use RoleDefinitionId enum for common role definitions + * */ + public constructor(getScopeId: () => string | undefined, roleDefinitionId: RoleDefinitionId); + + public execute(wizardContext: T, progress: Progress<{ message?: string; increment?: number }>): Promise; + public shouldExecute(wizardContext: T): boolean; +} + export interface IAzureUtilsExtensionVariables extends UIExtensionVariables { prefix: string; } @@ -448,3 +489,7 @@ export function setupAzureLogger(logOutputChannel: LogOutputChannel): Disposable * @param password - Password. Gets encoded before being set in the header */ export function addBasicAuthenticationCredentialsToClient(client: ServiceClient, userName: string, password: string): void; + +export declare enum RoleDefinitionId { + StorageBlobDataContributor = '/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe' +} diff --git a/azure/package-lock.json b/azure/package-lock.json index fb1d10e141..0b32fe1bf3 100644 --- a/azure/package-lock.json +++ b/azure/package-lock.json @@ -1,14 +1,17 @@ { "name": "@microsoft/vscode-azext-azureutils", - "version": "3.0.1", + "version": "3.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@microsoft/vscode-azext-azureutils", - "version": "3.0.1", + "version": "3.1.0", "license": "MIT", "dependencies": { + "@azure/arm-authorization": "^9.0.0", + "@azure/arm-authorization-profile-2020-09-01-hybrid": "^2.1.0", + "@azure/arm-msi": "^2.1.0", "@azure/arm-resources": "^5.0.0", "@azure/arm-resources-profile-2020-09-01-hybrid": "^2.0.0", "@azure/arm-resources-subscriptions": "^2.0.0", @@ -57,6 +60,51 @@ "node": ">=12.0.0" } }, + "node_modules/@azure/arm-authorization": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@azure/arm-authorization/-/arm-authorization-9.0.0.tgz", + "integrity": "sha512-GdiCA8IA1gO+qcCbFEPj+iLC4+3ByjfKzmeAnkP7MdlL84Yo30Huo/EwbZzwRjYybXYUBuFxGPBB+yeTT4Ebxg==", + "dependencies": { + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.7.0", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/arm-authorization-profile-2020-09-01-hybrid": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/arm-authorization-profile-2020-09-01-hybrid/-/arm-authorization-profile-2020-09-01-hybrid-2.1.0.tgz", + "integrity": "sha512-uOXhcj6Dv+TB8Yn2fguQQhoBZhafTf0ir5/QIZ8C7Rb2vS0nLcnoeesWRj9jmS0SerE7y2AF3qEhrowEZotX1Q==", + "dependencies": { + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.6.1", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/arm-msi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/arm-msi/-/arm-msi-2.1.0.tgz", + "integrity": "sha512-qP2BXpXkZ/I5gO479Dj6HFVkgSrAlYOG2AGVTqxTu0YIVdZdmrlZLH7SNl1Um9id+p7uNGNRdVKIBO5rcQgeJw==", + "dependencies": { + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.6.1", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@azure/arm-resources": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@azure/arm-resources/-/arm-resources-5.2.0.tgz", diff --git a/azure/package.json b/azure/package.json index ca4ff01c6d..ca3b41858b 100644 --- a/azure/package.json +++ b/azure/package.json @@ -1,7 +1,7 @@ { "name": "@microsoft/vscode-azext-azureutils", "author": "Microsoft Corporation", - "version": "3.0.1", + "version": "3.1.0", "description": "Common Azure utils for developing Azure extensions for VS Code", "tags": [ "azure", @@ -31,6 +31,9 @@ "package": "npm pack" }, "dependencies": { + "@azure/arm-authorization": "^9.0.0", + "@azure/arm-authorization-profile-2020-09-01-hybrid": "^2.1.0", + "@azure/arm-msi": "^2.1.0", "@azure/arm-resources": "^5.0.0", "@azure/arm-resources-profile-2020-09-01-hybrid": "^2.0.0", "@azure/arm-resources-subscriptions": "^2.0.0", diff --git a/azure/src/clients.ts b/azure/src/clients.ts index 666af4bd2e..0901b4584f 100644 --- a/azure/src/clients.ts +++ b/azure/src/clients.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { AuthorizationManagementClient } from '@azure/arm-authorization'; +import { ManagedServiceIdentityClient } from '@azure/arm-msi'; import type { ResourceManagementClient } from '@azure/arm-resources'; -import type { StorageManagementClient } from '@azure/arm-storage'; import type { SubscriptionClient } from '@azure/arm-resources-subscriptions'; +import type { StorageManagementClient } from '@azure/arm-storage'; import { createAzureClient, createAzureSubscriptionClient, InternalAzExtClientContext, parseClientContext } from './createAzureClient'; // Lazy-load @azure packages to improve startup performance. @@ -27,6 +29,18 @@ export async function createResourcesClient(context: InternalAzExtClientContext) } } +export async function createManagedServiceIdentityClient(context: InternalAzExtClientContext): Promise { + return createAzureClient(context, (await import('@azure/arm-msi')).ManagedServiceIdentityClient); +} + +export async function createAuthorizationManagementClient(context: InternalAzExtClientContext): Promise { + if (parseClientContext(context).isCustomCloud) { + return createAzureClient(context, (await import('@azure/arm-authorization-profile-2020-09-01-hybrid')).AuthorizationManagementClient); + } else { + return createAzureClient(context, (await import('@azure/arm-authorization')).AuthorizationManagementClient); + } +} + export async function createSubscriptionsClient(context: InternalAzExtClientContext): Promise { return createAzureSubscriptionClient(context, (await import('@azure/arm-resources-subscriptions')).SubscriptionClient); } diff --git a/azure/src/constants.ts b/azure/src/constants.ts index 7d72a88850..dc5c694c3f 100644 --- a/azure/src/constants.ts +++ b/azure/src/constants.ts @@ -5,4 +5,4 @@ export const resourcesProvider: string = 'Microsoft.Resources'; export const storageProvider: string = 'Microsoft.Storage'; -export const storageProviderType = "Microsoft.Storage/storageAccounts"; \ No newline at end of file +export const storageProviderType = "Microsoft.Storage/storageAccounts"; diff --git a/azure/src/index.ts b/azure/src/index.ts index bd13471f3f..bd397f4769 100644 --- a/azure/src/index.ts +++ b/azure/src/index.ts @@ -14,9 +14,12 @@ export * from './wizard/LocationListStep'; export * from './wizard/ResourceGroupCreateStep'; export * from './wizard/ResourceGroupListStep'; export * from './wizard/ResourceGroupNameStep'; +export * from './wizard/RoleAssignmentExecuteStep'; export * from './wizard/StorageAccountCreateStep'; export * from './wizard/StorageAccountListStep'; export * from './wizard/StorageAccountNameStep'; +export * from './wizard/UserAssignedIdentityCreateStep'; +export * from './wizard/UserAssignedIdentityListStep'; export * from './wizard/VerifyProvidersStep'; export * from './utils/setupAzureLogger'; export { registerAzureUtilsExtensionVariables } from './extensionVariables'; diff --git a/azure/src/wizard/RoleAssignmentExecuteStep.ts b/azure/src/wizard/RoleAssignmentExecuteStep.ts new file mode 100644 index 0000000000..acc39480da --- /dev/null +++ b/azure/src/wizard/RoleAssignmentExecuteStep.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardExecuteStep, nonNullValueAndProp } from '@microsoft/vscode-azext-utils'; +import { randomUUID } from 'crypto'; +import { l10n, Progress } from 'vscode'; +import * as types from '../../index'; +import { createAuthorizationManagementClient } from '../clients'; + +export enum RoleDefinitionId { + StorageBlobDataContributor = '/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe' +} + +export class RoleAssignmentExecuteStep extends AzureWizardExecuteStep { + public priority: number = 900; + private getScopeId: () => string | undefined; + private _roleDefinitionId: types.RoleDefinitionId; + public constructor(getScopeId: () => string | undefined, roleDefinitionId: types.RoleDefinitionId) { + super(); + this.getScopeId = getScopeId; + this._roleDefinitionId = roleDefinitionId; + } + + public async execute(wizardContext: T, _progress: Progress<{ message?: string; increment?: number }>): Promise { + const amClient = await createAuthorizationManagementClient(wizardContext) + const scope = this.getScopeId(); + if (!scope) { + throw new Error(l10n.t('No scope was provided for the role assignment.')); + } + + const guid = randomUUID(); + const roleDefinitionId = this._roleDefinitionId as unknown as string; + const principalId = nonNullValueAndProp(wizardContext.managedIdentity, 'principalId'); + await amClient.roleAssignments.create(scope, guid, { roleDefinitionId, principalId }); + } + + public shouldExecute(wizardContext: T): boolean { + return !!wizardContext.managedIdentity; + } +} diff --git a/azure/src/wizard/UserAssignedIdentityCreateStep.ts b/azure/src/wizard/UserAssignedIdentityCreateStep.ts new file mode 100644 index 0000000000..71a1c9b357 --- /dev/null +++ b/azure/src/wizard/UserAssignedIdentityCreateStep.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ManagedServiceIdentityClient } from '@azure/arm-msi'; +import { AzureWizardExecuteStep, nonNullValueAndProp } from '@microsoft/vscode-azext-utils'; +import { l10n, Progress } from 'vscode'; +import * as types from '../../index'; +import { createManagedServiceIdentityClient } from '../clients'; +import { storageProvider } from '../constants'; +import { ext } from '../extensionVariables'; +import { LocationListStep } from './LocationListStep'; + +/** + * Naming constraints: + * The resource name must start with a letter or number, + * have a length between 3 and 128 characters and + * can only contain a combination of alphanumeric characters, hyphens and underscores + * But since we are appending "-identities" to the resource group name and that has the same constraints and a 90 character limit, + * we don't need to do any verification + **/ +export class UserAssignedIdentityCreateStep extends AzureWizardExecuteStep { + public priority: number = 140; + + public constructor() { + super(); + } + + public async execute(wizardContext: T, progress: Progress<{ message?: string; increment?: number }>): Promise { + const newLocation: string = (await LocationListStep.getLocation(wizardContext, storageProvider)).name; + const rgName: string = nonNullValueAndProp(wizardContext.resourceGroup, 'name'); + const newName: string = `${rgName}-identities`; + const creatingUserAssignedIdentity: string = l10n.t('Creating user assigned identity "{0}" in location "{1}""...', newName, newLocation); + ext.outputChannel.appendLog(creatingUserAssignedIdentity); + progress.report({ message: creatingUserAssignedIdentity }); + const msiClient: ManagedServiceIdentityClient = await createManagedServiceIdentityClient(wizardContext); + wizardContext.managedIdentity = await msiClient.userAssignedIdentities.createOrUpdate( + rgName, + newName, + { + location: newLocation + } + ); + const createdUserAssignedIdentity: string = l10n.t('Successfully created user assigned identity "{0}".', newName); + ext.outputChannel.appendLog(createdUserAssignedIdentity); + } + + public shouldExecute(wizardContext: T): boolean { + return !wizardContext.managedIdentity; + } +} diff --git a/azure/src/wizard/UserAssignedIdentityListStep.ts b/azure/src/wizard/UserAssignedIdentityListStep.ts new file mode 100644 index 0000000000..daf92e9832 --- /dev/null +++ b/azure/src/wizard/UserAssignedIdentityListStep.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Identity } from '@azure/arm-msi'; +import { AzureWizardPromptStep, IAzureQuickPickItem, IAzureQuickPickOptions, IWizardOptions, nonNullProp } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; +import * as types from '../../index'; +import { createManagedServiceIdentityClient } from '../clients'; +import { uiUtils } from '../utils/uiUtils'; +import { LocationListStep } from './LocationListStep'; +import { UserAssignedIdentityCreateStep } from './UserAssignedIdentityCreateStep'; + +export class UserAssignedIdentityListStep extends AzureWizardPromptStep { + private _suppressCreate: boolean | undefined; + + public constructor(suppressCreate?: boolean) { + super(); + this._suppressCreate = suppressCreate; + } + + public async prompt(wizardContext: T): Promise { + // Cache resource group separately per subscription + const options: IAzureQuickPickOptions = { placeHolder: 'Select a resource group for new resources.', id: `ResourceGroupListStep/${wizardContext.subscriptionId}` }; + wizardContext.managedIdentity = (await wizardContext.ui.showQuickPick(this.getQuickPicks(wizardContext), options)).data; + if (wizardContext.managedIdentity && !LocationListStep.hasLocation(wizardContext)) { + await LocationListStep.setLocation(wizardContext, nonNullProp(wizardContext.managedIdentity, 'location')); + } + } + + public shouldPrompt(wizardContext: T): boolean { + return !wizardContext.managedIdentity; + } + + public async getSubWizard(wizardContext: T): Promise | undefined> { + if (!wizardContext.managedIdentity) { + return { + executeSteps: [new UserAssignedIdentityCreateStep()] + } + } + + return undefined; + } + + private async getQuickPicks(wizardContext: T): Promise[]> { + const picks: IAzureQuickPickItem[] = []; + const miClient = await createManagedServiceIdentityClient(wizardContext); + const uai = await uiUtils.listAllIterator(miClient.userAssignedIdentities.listBySubscription()); + + if (!this._suppressCreate) { + picks.push({ + label: vscode.l10n.t('$(plus) Create new user assigned identity'), + description: '', + data: undefined + }); + } + + return picks.concat(uai.map((i: Identity) => { + return { + id: i.id, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + label: i.name!, + description: i.location, + data: i + }; + })); + } +} From 8e4e2e2739dd9710a314522f010c34bc8183fa84 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Fri, 19 Jul 2024 15:33:12 -0700 Subject: [PATCH 2/9] Add output log to role assignment creation --- azure/src/wizard/RoleAssignmentExecuteStep.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/azure/src/wizard/RoleAssignmentExecuteStep.ts b/azure/src/wizard/RoleAssignmentExecuteStep.ts index acc39480da..9a93f3e8eb 100644 --- a/azure/src/wizard/RoleAssignmentExecuteStep.ts +++ b/azure/src/wizard/RoleAssignmentExecuteStep.ts @@ -8,6 +8,7 @@ import { randomUUID } from 'crypto'; import { l10n, Progress } from 'vscode'; import * as types from '../../index'; import { createAuthorizationManagementClient } from '../clients'; +import { ext } from '../extensionVariables'; export enum RoleDefinitionId { StorageBlobDataContributor = '/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe' @@ -31,9 +32,12 @@ export class RoleAssignmentExecuteStep Date: Mon, 22 Jul 2024 16:43:23 -0700 Subject: [PATCH 3/9] Fix create message --- azure/index.d.ts | 2 +- azure/src/wizard/RoleAssignmentExecuteStep.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/azure/index.d.ts b/azure/index.d.ts index 5eecd4ea44..c0f5c3e61d 100644 --- a/azure/index.d.ts +++ b/azure/index.d.ts @@ -491,5 +491,5 @@ export function setupAzureLogger(logOutputChannel: LogOutputChannel): Disposable export function addBasicAuthenticationCredentialsToClient(client: ServiceClient, userName: string, password: string): void; export declare enum RoleDefinitionId { - StorageBlobDataContributor = '/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe' + 'Storage Blob Data Contributor' = '/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe' } diff --git a/azure/src/wizard/RoleAssignmentExecuteStep.ts b/azure/src/wizard/RoleAssignmentExecuteStep.ts index 9a93f3e8eb..cc0bac0f34 100644 --- a/azure/src/wizard/RoleAssignmentExecuteStep.ts +++ b/azure/src/wizard/RoleAssignmentExecuteStep.ts @@ -11,7 +11,7 @@ import { createAuthorizationManagementClient } from '../clients'; import { ext } from '../extensionVariables'; export enum RoleDefinitionId { - StorageBlobDataContributor = '/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe' + 'Storage Blob Data Contributor' = '/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe' } export class RoleAssignmentExecuteStep extends AzureWizardExecuteStep { @@ -24,19 +24,24 @@ export class RoleAssignmentExecuteStep): Promise { + public async execute(wizardContext: T, progress: Progress<{ message?: string; increment?: number }>): Promise { const amClient = await createAuthorizationManagementClient(wizardContext) const scope = this.getScopeId(); if (!scope) { throw new Error(l10n.t('No scope was provided for the role assignment.')); } + const scopeSplit = scope.split('/'); + const resourceName = scopeSplit[scopeSplit.length - 1] ?? ''; + const resourceType = scopeSplit[scopeSplit.length - 2] ?? ''; const guid = randomUUID(); + const roleDefinitionDisplayName = Object.keys(RoleDefinitionId)[Object.values(RoleDefinitionId).indexOf(this._roleDefinitionId)]; const roleDefinitionId = this._roleDefinitionId as unknown as string; const principalId = nonNullValueAndProp(wizardContext.managedIdentity, 'principalId'); await amClient.roleAssignments.create(scope, guid, { roleDefinitionId, principalId }); - const roleAssignmentCreated = l10n.t('Role assignment "{1}" created with resource "{0}".', scope, roleDefinitionDisplayName); + const roleAssignmentCreated = l10n.t('Role assignment "{0}" created for resource "{1}" with provider "{2}".', roleDefinitionDisplayName, resourceName, resourceType); + progress.report({ message: roleAssignmentCreated }); ext.outputChannel.appendLog(roleAssignmentCreated); } From 6fd51057deae7631f575fec3302e8aee9ba8f294 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Tue, 23 Jul 2024 10:15:24 -0700 Subject: [PATCH 4/9] PR feedback --- azure/index.d.ts | 21 +++++++++++++----- azure/src/constants.ts | 13 +++++++++++ azure/src/index.ts | 1 + azure/src/wizard/RoleAssignmentExecuteStep.ts | 22 ++++++++----------- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/azure/index.d.ts b/azure/index.d.ts index c0f5c3e61d..f4f601fd7f 100644 --- a/azure/index.d.ts +++ b/azure/index.d.ts @@ -5,6 +5,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { type RoleDefinition } from '@azure/arm-authorization'; import { Identity } from '@azure/arm-msi'; import type { ExtendedLocation, ResourceGroup } from '@azure/arm-resources'; import type { Location } from '@azure/arm-resources-subscriptions'; @@ -214,8 +215,9 @@ export interface IResourceGroupWizardContext extends ILocationWizardContext, IRe suppress403Handling?: boolean; /** - * The managed identity to be used for the new target resource - * Service resource, such as storage, should be add a role assignment + * The managed identity that will be assigned to the resource such as a function app or container app + * If you need to grant access to a resource, such as a storage account or SQL database, you can use this managed identity to create a role assignment + * with the RoleAssignmentExecuteStep */ managedIdentity?: Identity; @@ -378,7 +380,7 @@ export declare class RoleAssignmentExecuteStep string | undefined, roleDefinitionId: RoleDefinitionId); + public constructor(getScopeId: () => string | undefined, roleDefinition: RoleDefinition); public execute(wizardContext: T, progress: Progress<{ message?: string; increment?: number }>): Promise; public shouldExecute(wizardContext: T): boolean; @@ -490,6 +492,13 @@ export function setupAzureLogger(logOutputChannel: LogOutputChannel): Disposable */ export function addBasicAuthenticationCredentialsToClient(client: ServiceClient, userName: string, password: string): void; -export declare enum RoleDefinitionId { - 'Storage Blob Data Contributor' = '/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe' -} +export declare const CommonRoleDefinitions: { + readonly storageBlobDataContributor: { + readonly id: "/subscriptions/9b5c7ccb-9857-4307-843b-8875e83f65e9/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe"; + readonly name: "ba92f5b4-2d11-453d-a403-e96b0029c9fe"; + readonly type: "Microsoft.Authorization/roleDefinitions"; + readonly roleName: "Storage Blob Data Contributor"; + readonly description: "Allows for read, write and delete access to Azure Storage blob containers and data"; + readonly roleType: "BuiltInRole"; + }; +}; diff --git a/azure/src/constants.ts b/azure/src/constants.ts index dc5c694c3f..fb9128a1b1 100644 --- a/azure/src/constants.ts +++ b/azure/src/constants.ts @@ -3,6 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { type RoleDefinition } from "@azure/arm-authorization"; + export const resourcesProvider: string = 'Microsoft.Resources'; export const storageProvider: string = 'Microsoft.Storage'; export const storageProviderType = "Microsoft.Storage/storageAccounts"; + +export const CommonRoleDefinitions = { + storageBlobDataContributor: { + id: "/subscriptions/9b5c7ccb-9857-4307-843b-8875e83f65e9/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe", + name: "ba92f5b4-2d11-453d-a403-e96b0029c9fe", + type: "Microsoft.Authorization/roleDefinitions", + roleName: "Storage Blob Data Contributor", + description: "Allows for read, write and delete access to Azure Storage blob containers and data", + roleType: "BuiltInRole" + } as RoleDefinition +} as const; diff --git a/azure/src/index.ts b/azure/src/index.ts index bd397f4769..1a31fedc0c 100644 --- a/azure/src/index.ts +++ b/azure/src/index.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +export { CommonRoleDefinitions as const } from './constants'; export * from './createAzureClient'; export * from './openInPortal'; export * from './tree/AzureAccountTreeItemBase'; diff --git a/azure/src/wizard/RoleAssignmentExecuteStep.ts b/azure/src/wizard/RoleAssignmentExecuteStep.ts index cc0bac0f34..6aedc37221 100644 --- a/azure/src/wizard/RoleAssignmentExecuteStep.ts +++ b/azure/src/wizard/RoleAssignmentExecuteStep.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { type RoleDefinition } from '@azure/arm-authorization'; import { AzureWizardExecuteStep, nonNullValueAndProp } from '@microsoft/vscode-azext-utils'; import { randomUUID } from 'crypto'; import { l10n, Progress } from 'vscode'; @@ -10,18 +11,14 @@ import * as types from '../../index'; import { createAuthorizationManagementClient } from '../clients'; import { ext } from '../extensionVariables'; -export enum RoleDefinitionId { - 'Storage Blob Data Contributor' = '/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe' -} - export class RoleAssignmentExecuteStep extends AzureWizardExecuteStep { public priority: number = 900; private getScopeId: () => string | undefined; - private _roleDefinitionId: types.RoleDefinitionId; - public constructor(getScopeId: () => string | undefined, roleDefinitionId: types.RoleDefinitionId) { + private _roleDefinition: RoleDefinition; + public constructor(getScopeId: () => string | undefined, roleDefinition: RoleDefinition) { super(); this.getScopeId = getScopeId; - this._roleDefinitionId = roleDefinitionId; + this._roleDefinition = roleDefinition; } public async execute(wizardContext: T, progress: Progress<{ message?: string; increment?: number }>): Promise { @@ -30,17 +27,16 @@ export class RoleAssignmentExecuteStep Date: Tue, 23 Jul 2024 10:26:19 -0700 Subject: [PATCH 5/9] Better comments --- azure/index.d.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/azure/index.d.ts b/azure/index.d.ts index f4f601fd7f..a8f0bf720a 100644 --- a/azure/index.d.ts +++ b/azure/index.d.ts @@ -376,9 +376,12 @@ export declare class RoleAssignmentExecuteStep string | undefined, roleDefinition: RoleDefinition); From 73626ab7a770f06855442882bbfe27a6f5e066da Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Tue, 23 Jul 2024 10:28:25 -0700 Subject: [PATCH 6/9] Slightly more clarification --- azure/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure/index.d.ts b/azure/index.d.ts index a8f0bf720a..b685101e15 100644 --- a/azure/index.d.ts +++ b/azure/index.d.ts @@ -377,9 +377,9 @@ export declare class RoleAssignmentExecuteStep Date: Tue, 23 Jul 2024 10:30:55 -0700 Subject: [PATCH 7/9] More logical line breaks --- azure/src/wizard/RoleAssignmentExecuteStep.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure/src/wizard/RoleAssignmentExecuteStep.ts b/azure/src/wizard/RoleAssignmentExecuteStep.ts index 6aedc37221..25fc20d646 100644 --- a/azure/src/wizard/RoleAssignmentExecuteStep.ts +++ b/azure/src/wizard/RoleAssignmentExecuteStep.ts @@ -32,9 +32,9 @@ export class RoleAssignmentExecuteStep Date: Tue, 23 Jul 2024 10:33:33 -0700 Subject: [PATCH 8/9] Fix user identity assigned string --- azure/src/wizard/UserAssignedIdentityListStep.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure/src/wizard/UserAssignedIdentityListStep.ts b/azure/src/wizard/UserAssignedIdentityListStep.ts index daf92e9832..1cf6b477e1 100644 --- a/azure/src/wizard/UserAssignedIdentityListStep.ts +++ b/azure/src/wizard/UserAssignedIdentityListStep.ts @@ -22,7 +22,7 @@ export class UserAssignedIdentityListStep { // Cache resource group separately per subscription - const options: IAzureQuickPickOptions = { placeHolder: 'Select a resource group for new resources.', id: `ResourceGroupListStep/${wizardContext.subscriptionId}` }; + const options: IAzureQuickPickOptions = { placeHolder: 'Select a user assigned identity.', id: `UserAssignedIdentityListStep` }; wizardContext.managedIdentity = (await wizardContext.ui.showQuickPick(this.getQuickPicks(wizardContext), options)).data; if (wizardContext.managedIdentity && !LocationListStep.hasLocation(wizardContext)) { await LocationListStep.setLocation(wizardContext, nonNullProp(wizardContext.managedIdentity, 'location')); From 35dc8c2712ef2a500de14a4d98dc9c4da137ee96 Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Tue, 23 Jul 2024 13:51:16 -0700 Subject: [PATCH 9/9] Probably dont need the location code considering most wizards have a location --- azure/src/wizard/UserAssignedIdentityListStep.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/azure/src/wizard/UserAssignedIdentityListStep.ts b/azure/src/wizard/UserAssignedIdentityListStep.ts index 1cf6b477e1..cd6407e05d 100644 --- a/azure/src/wizard/UserAssignedIdentityListStep.ts +++ b/azure/src/wizard/UserAssignedIdentityListStep.ts @@ -4,12 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Identity } from '@azure/arm-msi'; -import { AzureWizardPromptStep, IAzureQuickPickItem, IAzureQuickPickOptions, IWizardOptions, nonNullProp } from '@microsoft/vscode-azext-utils'; +import { AzureWizardPromptStep, IAzureQuickPickItem, IAzureQuickPickOptions, IWizardOptions } from '@microsoft/vscode-azext-utils'; import * as vscode from 'vscode'; import * as types from '../../index'; import { createManagedServiceIdentityClient } from '../clients'; import { uiUtils } from '../utils/uiUtils'; -import { LocationListStep } from './LocationListStep'; import { UserAssignedIdentityCreateStep } from './UserAssignedIdentityCreateStep'; export class UserAssignedIdentityListStep extends AzureWizardPromptStep { @@ -21,12 +20,8 @@ export class UserAssignedIdentityListStep { - // Cache resource group separately per subscription const options: IAzureQuickPickOptions = { placeHolder: 'Select a user assigned identity.', id: `UserAssignedIdentityListStep` }; wizardContext.managedIdentity = (await wizardContext.ui.showQuickPick(this.getQuickPicks(wizardContext), options)).data; - if (wizardContext.managedIdentity && !LocationListStep.hasLocation(wizardContext)) { - await LocationListStep.setLocation(wizardContext, nonNullProp(wizardContext.managedIdentity, 'location')); - } } public shouldPrompt(wizardContext: T): boolean {