Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

azure: Add wizard steps to list/create UserAssignedIdentities and execute role definitions #1757

Merged
merged 9 commits into from
Jul 25, 2024
57 changes: 57 additions & 0 deletions azure/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

/* 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';
import type { StorageAccount } from '@azure/arm-storage';
Expand Down Expand Up @@ -212,6 +214,13 @@ export interface IResourceGroupWizardContext extends ILocationWizardContext, IRe
*/
suppress403Handling?: boolean;

/**
* 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;

ui: IAzureUserInput;
}

Expand Down Expand Up @@ -343,6 +352,43 @@ export declare class StorageAccountCreateStep<T extends IStorageAccountWizardCon
public shouldExecute(wizardContext: T): boolean;
}

export declare class UserAssignedIdentityListStep<T extends IResourceGroupWizardContext> extends AzureWizardPromptStep<T> {
public constructor(suppressCreate?: boolean);

public prompt(wizardContext: T): Promise<void>;
public shouldPrompt(wizardContext: T): boolean;
}

export declare class UserAssignedIdentityCreateStep<T extends IResourceGroupWizardContext> extends AzureWizardExecuteStep<T> {
/**
* 140
*/
public priority: number;
public constructor();

public execute(wizardContext: T, progress: Progress<{ message?: string; increment?: number }>): Promise<void>;
public shouldExecute(wizardContext: T): boolean;
}

export declare class RoleAssignmentExecuteStep<T extends IResourceGroupWizardContext, TKey extends keyof T> extends AzureWizardExecuteStep<T> {
/**
* 900
*/
public priority: number;
/**
* @param getScopeId A function that returns the scope id for the role assignment.
* The scope ID is the Azure ID of the resource that we are granting access to such as a storage account.
* Example: `/subscriptions/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/rgName/providers/Microsoft.Storage/storageAccounts/resourceName`
* This typically won't exist until _after_ the wizard executes and the resource is created, so we need to pass in a function that returns the ID.
* If the scope ID is undefined, the step will throw an error.
* @param roleDefinition The ARM role definition to assign. Use CommonRoleDefinition constant for role defintions that don't require user input.
* */
public constructor(getScopeId: () => string | undefined, roleDefinition: RoleDefinition);

public execute(wizardContext: T, progress: Progress<{ message?: string; increment?: number }>): Promise<void>;
public shouldExecute(wizardContext: T): boolean;
}

export interface IAzureUtilsExtensionVariables extends UIExtensionVariables {
prefix: string;
}
Expand Down Expand Up @@ -448,3 +494,14 @@ 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 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";
};
};
52 changes: 50 additions & 2 deletions azure/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion azure/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
alexweininger marked this conversation as resolved.
Show resolved Hide resolved
"@azure/arm-resources": "^5.0.0",
"@azure/arm-resources-profile-2020-09-01-hybrid": "^2.0.0",
"@azure/arm-resources-subscriptions": "^2.0.0",
Expand Down
16 changes: 15 additions & 1 deletion azure/src/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -27,6 +29,18 @@ export async function createResourcesClient(context: InternalAzExtClientContext)
}
}

export async function createManagedServiceIdentityClient(context: InternalAzExtClientContext): Promise<ManagedServiceIdentityClient> {
return createAzureClient(context, (await import('@azure/arm-msi')).ManagedServiceIdentityClient);
}

export async function createAuthorizationManagementClient(context: InternalAzExtClientContext): Promise<AuthorizationManagementClient> {
if (parseClientContext(context).isCustomCloud) {
return <AuthorizationManagementClient><unknown>createAzureClient(context, (await import('@azure/arm-authorization-profile-2020-09-01-hybrid')).AuthorizationManagementClient);
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh nice, so the hybrid profile version was made to support custom clouds?

Copy link
Member

Choose a reason for hiding this comment

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

Is there a downside to just always using the one that supports custom clouds?

Copy link
Member Author

@nturinski nturinski Jul 23, 2024

Choose a reason for hiding this comment

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

Discussed offline, but the api-versions for custom clouds aren't kept up to date as quickly as standard Azure SDKs.

} else {
return createAzureClient(context, (await import('@azure/arm-authorization')).AuthorizationManagementClient);
}
}

export async function createSubscriptionsClient(context: InternalAzExtClientContext): Promise<SubscriptionClient> {
return createAzureSubscriptionClient(context, (await import('@azure/arm-resources-subscriptions')).SubscriptionClient);
}
15 changes: 14 additions & 1 deletion azure/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 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;
4 changes: 4 additions & 0 deletions azure/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -14,9 +15,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';
Expand Down
47 changes: 47 additions & 0 deletions azure/src/wizard/RoleAssignmentExecuteStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* 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';
import * as types from '../../index';
import { createAuthorizationManagementClient } from '../clients';
import { ext } from '../extensionVariables';

export class RoleAssignmentExecuteStep<T extends types.IResourceGroupWizardContext> extends AzureWizardExecuteStep<T> {
public priority: number = 900;
private getScopeId: () => string | undefined;
private _roleDefinition: RoleDefinition;
public constructor(getScopeId: () => string | undefined, roleDefinition: RoleDefinition) {
super();
this.getScopeId = getScopeId;
this._roleDefinition = roleDefinition;
}

public async execute(wizardContext: T, progress: Progress<{ message?: string; increment?: number }>): Promise<void> {
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 scopeSplitArr = scope.split('/');
const resourceName = scopeSplitArr[scopeSplitArr.length - 1] ?? '';
const resourceType = scopeSplitArr[scopeSplitArr.length - 2] ?? '';

const guid = randomUUID();
const roleDefinitionId = this._roleDefinition.id as string;
const principalId = nonNullValueAndProp(wizardContext.managedIdentity, 'principalId');

await amClient.roleAssignments.create(scope, guid, { roleDefinitionId, principalId });
const roleAssignmentCreated = l10n.t('Role assignment "{0}" created for the {2} resource "{1}".', this._roleDefinition.roleName ?? '', resourceName, resourceType);
progress.report({ message: roleAssignmentCreated });
ext.outputChannel.appendLog(roleAssignmentCreated);
}

public shouldExecute(wizardContext: T): boolean {
return !!wizardContext.managedIdentity;
}
}
52 changes: 52 additions & 0 deletions azure/src/wizard/UserAssignedIdentityCreateStep.ts
Original file line number Diff line number Diff line change
@@ -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<T extends types.IResourceGroupWizardContext> extends AzureWizardExecuteStep<T> {
public priority: number = 140;

public constructor() {
super();
}

public async execute(wizardContext: T, progress: Progress<{ message?: string; increment?: number }>): Promise<void> {
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;
}
}
Loading
Loading