Skip to content

Commit

Permalink
Add wizard steps to list/create UserAssignedIdentities and execute ro…
Browse files Browse the repository at this point in the history
…le definitions
  • Loading branch information
nturinski committed Jul 19, 2024
1 parent d936814 commit 7df6d33
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 5 deletions.
45 changes: 45 additions & 0 deletions azure/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -343,6 +350,40 @@ 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. 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<void>;
public shouldExecute(wizardContext: T): boolean;
}

export interface IAzureUtilsExtensionVariables extends UIExtensionVariables {
prefix: string;
}
Expand Down Expand Up @@ -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'
}
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);
}
2 changes: 1 addition & 1 deletion azure/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

export const resourcesProvider: string = 'Microsoft.Resources';
export const storageProvider: string = 'Microsoft.Storage';
export const storageProviderType = "Microsoft.Storage/storageAccounts";
export const storageProviderType = "Microsoft.Storage/storageAccounts";
3 changes: 3 additions & 0 deletions azure/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
42 changes: 42 additions & 0 deletions azure/src/wizard/RoleAssignmentExecuteStep.ts
Original file line number Diff line number Diff line change
@@ -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<T extends types.IResourceGroupWizardContext> extends AzureWizardExecuteStep<T> {
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<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 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;
}
}
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;
}
}
69 changes: 69 additions & 0 deletions azure/src/wizard/UserAssignedIdentityListStep.ts
Original file line number Diff line number Diff line change
@@ -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<T extends types.IResourceGroupWizardContext> extends AzureWizardPromptStep<T> {
private _suppressCreate: boolean | undefined;

public constructor(suppressCreate?: boolean) {
super();
this._suppressCreate = suppressCreate;
}

public async prompt(wizardContext: T): Promise<void> {
// 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<IWizardOptions<T> | undefined> {
if (!wizardContext.managedIdentity) {
return {
executeSteps: [new UserAssignedIdentityCreateStep()]
}
}

return undefined;
}

private async getQuickPicks(wizardContext: T): Promise<IAzureQuickPickItem<Identity | undefined>[]> {
const picks: IAzureQuickPickItem<Identity | undefined>[] = [];
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
};
}));
}
}

0 comments on commit 7df6d33

Please sign in to comment.