Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
MicroFish91 committed Jul 25, 2024
2 parents 992c318 + fad47ce commit 81cc3b0
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 12 deletions.
24 changes: 18 additions & 6 deletions appservice/src/createAppService/AppServicePlanRedundancyStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class AppServicePlanRedundancyStep extends AzureWizardPromptStep<IAppServ
}

// TODO(ccastrotrejo): This will be changed to use orgdomain with WI 12845265 once georegions API is updated with ANT78.
private isZoneRedundancyEnabled(location: string): boolean {
public static isZoneRedundancySupportedLocation(location: string): boolean {
const zoneRedundancySupportedLocations = [
'westus2',
'westus3',
Expand All @@ -46,25 +46,37 @@ export class AppServicePlanRedundancyStep extends AzureWizardPromptStep<IAppServ
'eastus2euap',
];

location = location.replace(/\s/, "").toLowerCase(); // Todo: Replace with LocationListStep's `generalizeLocationName` once exported and released
return zoneRedundancySupportedLocations.includes(location);
}

private isAllowedServicePlan(newPlanSku: SkuDescription): boolean {
const { family } = newPlanSku;
const allowedServicePlan = [
public static isZoneRedundancySupportedServicePlan(newPlanSkuOrFamily: SkuDescription | string): boolean {
const allowedServicePlans: string[] = [
'Pv2',
'Pv3',
'WS',
];

return !!family && allowedServicePlan.includes(family);
let family: string;
if ((newPlanSkuOrFamily as SkuDescription)?.family) {
// Nullish coallescing operator should be logically unnecessary, but helps TS compiler understand that this value won't be undefined
family = (newPlanSkuOrFamily as SkuDescription).family ?? '';
} else {
family = newPlanSkuOrFamily as string;
}

return allowedServicePlans.includes(family);
}

public static isZoneRedundancySupported(location: string, newPlanSkuOrFamily: SkuDescription | string): boolean {
return this.isZoneRedundancySupportedLocation(location) && this.isZoneRedundancySupportedServicePlan(newPlanSkuOrFamily);
}

public shouldPrompt(context: AppServiceWizardContext): boolean {
const { customLocation, _location, plan, newPlanSku } = context;
const { name } = _location || {};
if (plan === undefined && customLocation === undefined && name && newPlanSku) {
return this.isZoneRedundancyEnabled(name) && this.isAllowedServicePlan(newPlanSku);
return AppServicePlanRedundancyStep.isZoneRedundancySupported(name, newPlanSku);
}
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion appservice/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './createAppService/AppInsightsListStep';
export { AppKind, WebsiteOS } from './createAppService/AppKind';
export * from './createAppService/AppServicePlanCreateStep';
export * from './createAppService/AppServicePlanListStep';
export * from './createAppService/AppServicePlanRedundancyStep';
export * from './createAppService/AppServicePlanSkuStep';
export * from './createAppService/CustomLocationListStep';
export * from './createAppService/IAppServiceWizardContext';
Expand Down Expand Up @@ -49,4 +50,3 @@ export * from './tree/LogFilesTreeItem';
export * from './tree/SiteFilesTreeItem';
export * from './tryGetSiteResource';
export * from './utils/azureClients';

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;
}
}
Loading

0 comments on commit 81cc3b0

Please sign in to comment.