Skip to content

Commit

Permalink
azure: Add wizard steps to list/create UserAssignedIdentities and e…
Browse files Browse the repository at this point in the history
…xecute role definitions (#1757)

* Add wizard steps to list/create UserAssignedIdentities and execute role definitions

* Add output log to role assignment creation

* Fix create message

* PR feedback

* Better comments

* Slightly more clarification

* More logical line breaks

* Fix user identity assigned string

* Probably dont need the location code considering most wizards have a location
  • Loading branch information
nturinski authored Jul 25, 2024
1 parent f951f64 commit fad47ce
Show file tree
Hide file tree
Showing 9 changed files with 307 additions and 5 deletions.
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",
"@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);
} 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

0 comments on commit fad47ce

Please sign in to comment.