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: Changes to managed identity steps so they are compatible with functions settings conversion #1889

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions azure/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,14 +383,20 @@ export declare class RoleAssignmentExecuteStep<T extends IResourceGroupWizardCon
* 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.
* @param roles An array of roles. Each role is an object and include the ARM role definition id and name of the role definition.
* */
public constructor(getScopeId: () => string | undefined, roleDefinition: RoleDefinition);
public constructor(roles: () => Role[] | undefined);

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

export interface Role {
scopeId: string | undefined;
roleDefinitionId: string;
roleDefinitionName: string;
}
Copy link
Member

Choose a reason for hiding this comment

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

Can you add doc strings to this interface?


export interface IAzureUtilsExtensionVariables extends UIExtensionVariables {
prefix: string;
}
Expand Down Expand Up @@ -503,6 +509,10 @@ export function setupAzureLogger(logOutputChannel: LogOutputChannel): Disposable
*/
export function addBasicAuthenticationCredentialsToClient(client: ServiceClient, userName: string, password: string): void;

/**
* Common Roles that should be used to assign permissions to resources
* The role definitions can be found here: https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles
*/
export declare const CommonRoleDefinitions: {
readonly storageBlobDataContributor: {
readonly id: "/subscriptions/9b5c7ccb-9857-4307-843b-8875e83f65e9/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe";
Expand All @@ -512,7 +522,70 @@ export declare const CommonRoleDefinitions: {
readonly description: "Allows for read, write and delete access to Azure Storage blob containers and data";
readonly roleType: "BuiltInRole";
};
readonly storageBlobDataOwner: {
Copy link
Member

Choose a reason for hiding this comment

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

where'd you find all this info from? I would put a link in a comment so we know where to get it in the future

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll add a link

readonly name: "b7e6dc6d-f1e8-4753-8033-0f276bb0955b",
readonly type: "Microsoft.Authorization/roleDefinitions",
readonly roleName: "Storage Blob Data Owner",
readonly description: "Allows for full access to Azure Storage blob containers and data, including assigning POSIX access control.",
readonly roleType: "BuiltInRole"
};
readonly storageQueueDataContributor: {
readonly name: "974c5e8b-45b9-4653-ba55-5f855dd0fb88",
readonly type: "Microsoft.Authorization/roleDefinitions",
readonly roleName: "Storage Queue Data Contributor",
readonly description: "Read, write, and delete Azure Storage queues and queue messages.",
readonly roleType: "BuiltInRole"
};
readonly azureServiceBusDataReceiver: {
readonly name: "4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0",
readonly type: "Microsoft.Authorization/roleDefinitions",
readonly roleName: "Azure Service Bus Data Receiver",
readonly description: "Allows for receive access to Azure Service Bus resources.",
readonly sroleType: "BuiltInRole"
};
readonly azureServiceBusDataOwner: {
readonly name: "090c5cfd-751d-490a-894a-3ce6f1109419",
readonly type: "Microsoft.Authorization/roleDefinitions",
readonly roleName: "Azure Service Bus Data Owner",
readonly description: "Allows for full access to Azure Service Bus resources.",
readonly roleType: "BuiltInRole"
};
readonly azureEventHubsDataReceiver: {
readonly name: "a638d3c7-ab3a-418d-83e6-5f17a39d4fde",
readonly type: "Microsoft.Authorization/roleDefinitions",
readonly roleName: "Azure Event Hubs Data Receiver",
readonly description: "Allows receive access to Azure Event Hubs resources.",
readonly roleType: "BuiltInRole"
};
readonly azureEventHubsDataOwner: {
readonly name: "f526a384-b230-433a-b45c-95f59c4a2dec",
readonly type: "Microsoft.Authorization/roleDefinitions",
readonly roleName: "Azure Event Hubs Data Owner",
readonly description: "Allows for full access to Azure Event Hubs resources.",
readonly roleType: "BuiltInRole"
};
readonly cosmosDBAccountReader: {
readonly name: "fbdf93bf-df7d-467e-a4d2-9458aa1360c8",
readonly type: "Microsoft.Authorization/roleDefinitions",
readonly roleName: "Cosmos DB Account Reader",
readonly description: "Can read Azure Cosmos DB account data.",
readonly roleType: "BuiltInRole"
};
readonly documentDBAccountContributor: {
readonly name: "5bd9cd88-fe45-4216-938b-f97437e15450",
readonly type: "Microsoft.Authorization/roleDefinitions",
readonly roleName: "DocumentDB Account Contributor",
readonly description: "Can manage Azure Cosmos DB accounts.",
readonly roleType: "BuiltInRole"
}
};
/**
* Constructs the role id for a given subscription and role name id
*
* @param subscriptionId - Id for the subscription
* @param roleId - Name id for the role to be assigned (i.e CommonRoleDefinitions.storageBlobDataContributor.name)
*/
export function createRoleId(subscriptionId: string, RoleDefinition: RoleDefinition): string;

/**
* creates all RoleDefinitionsItem for an entire managed identity object
Expand Down
61 changes: 60 additions & 1 deletion azure/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,70 @@ 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,
storageBlobDataOwner: {
name: "b7e6dc6d-f1e8-4753-8033-0f276bb0955b",
type: "Microsoft.Authorization/roleDefinitions",
roleName: "Storage Blob Data Owner",
description: "Allows for full access to Azure Storage blob containers and data, including assigning POSIX access control.",
roleType: "BuiltInRole"
} as RoleDefinition,
storageQueueDataContributor: {
name: "974c5e8b-45b9-4653-ba55-5f855dd0fb88",
type: "Microsoft.Authorization/roleDefinitions",
roleName: "Storage Queue Data Contributor",
description: "Read, write, and delete Azure Storage queues and queue messages.",
roleType: "BuiltInRole"
} as RoleDefinition,
azureServiceBusDataReceiver: {
name: "4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0",
type: "Microsoft.Authorization/roleDefinitions",
roleName: "Azure Service Bus Data Receiver",
description: "Allows for receive access to Azure Service Bus resources.",
roleType: "BuiltInRole"
} as RoleDefinition,
azureServiceBusDataOwner: {
name: "090c5cfd-751d-490a-894a-3ce6f1109419",
type: "Microsoft.Authorization/roleDefinitions",
roleName: "Azure Service Bus Data Owner",
description: "Allows for full access to Azure Service Bus resources.",
roleType: "BuiltInRole"
} as RoleDefinition,
azureEventHubsDataReceiver: {
name: "a638d3c7-ab3a-418d-83e6-5f17a39d4fde",
type: "Microsoft.Authorization/roleDefinitions",
roleName: "Azure Event Hubs Data Receiver",
description: "Allows receive access to Azure Event Hubs resources.",
roleType: "BuiltInRole"
} as RoleDefinition,
azureEventHubsDataOwner: {
name: "f526a384-b230-433a-b45c-95f59c4a2dec",
type: "Microsoft.Authorization/roleDefinitions",
roleName: "Azure Event Hubs Data Owner",
description: "Allows for full access to Azure Event Hubs resources.",
roleType: "BuiltInRole"
} as RoleDefinition,
cosmosDBAccountReader: {
name: "fbdf93bf-df7d-467e-a4d2-9458aa1360c8",
type: "Microsoft.Authorization/roleDefinitions",
roleName: "Cosmos DB Account Reader",
description: "Can read Azure Cosmos DB account data.",
roleType: "BuiltInRole"
} as RoleDefinition,
documentDBAccountContributor: {
name: "5bd9cd88-fe45-4216-938b-f97437e15450",
type: "Microsoft.Authorization/roleDefinitions",
roleName: "DocumentDB Account Contributor",
description: "Can manage Azure Cosmos DB accounts.",
roleType: "BuiltInRole"
} as RoleDefinition
} as const;

export function createRoleId(subscriptionId: string, RoleDefinition: RoleDefinition): string {
return `/subscriptions/${subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/${RoleDefinition.name}`
}
3 changes: 1 addition & 2 deletions azure/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

export * from './clients';
export { CommonRoleDefinitions as const } from './constants';
export { CommonRoleDefinitions, createRoleId } from './constants';
export * from './createAzureClient';
export { registerAzureUtilsExtensionVariables } from './extensionVariables';
export * from './openInPortal';
Expand All @@ -26,5 +26,4 @@ export * from './wizard/StorageAccountNameStep';
export * from './wizard/UserAssignedIdentityCreateStep';
export * from './wizard/UserAssignedIdentityListStep';
export * from './wizard/VerifyProvidersStep';

// NOTE: The auto-fix action "source.organizeImports" does weird things with this file, but there doesn't seem to be a way to disable it on a per-file basis so we'll just let it happen
86 changes: 63 additions & 23 deletions azure/src/wizard/RoleAssignmentExecuteStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,42 +3,82 @@
* 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 { activityFailIcon, activitySuccessContext, activitySuccessIcon, AzureWizardExecuteStep, createUniversallyUniqueContextValue, ExecuteActivityContext, GenericTreeItem, nonNullValueAndProp, parseError } 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> {
export interface Role {
scopeId: string | undefined;
roleDefinitionId: string;
roleDefinitionName: string;
}

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

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 roles = this.roles();
if (roles) {
for (const role of roles) {
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we would want to make the whole step fail if one role fails. Maybe do a try/catch so it can continue?

const scope = role.scopeId;
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] ?? '';
try {
const guid = randomUUID();
const roleDefinitionId = role.roleDefinitionId;
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}".', role.roleDefinitionName ?? '', resourceName, resourceType);
progress.report({ message: roleAssignmentCreated });
ext.outputChannel.appendLog(roleAssignmentCreated);
if (wizardContext.activityChildren) {
wizardContext.activityChildren.push(
new GenericTreeItem(undefined, {
contextValue: createUniversallyUniqueContextValue(['successfullRoleAssignment', activitySuccessContext]),
label: l10n.t(`Role Assignment ${role.roleDefinitionName} created for ${resourceName}`),
iconPath: activitySuccessIcon
})
);
}
} catch (error) {
const roleAssignmentFailed = l10n.t('Failed to create role assignment "{0}" for the {2} resource "{1}".', role.roleDefinitionName ?? '', resourceName, resourceType);
progress.report({ message: roleAssignmentFailed });
ext.outputChannel.appendLog(roleAssignmentFailed);
const parsedError = parseError(error);
ext.outputChannel.appendLog(parsedError.message);
if (wizardContext.activityChildren) {
wizardContext.activityChildren.push(new GenericTreeItem(undefined, {
contextValue: createUniversallyUniqueContextValue(['failedRoleAssignment', activitySuccessContext]),
label: l10n.t(`Role Assignment ${role.roleDefinitionName} failed for ${resourceName}`),
iconPath: activityFailIcon
}));
}
}

}

if (wizardContext.activityChildren) {
for (const child of wizardContext.activityChildren) {
if (child.contextValue.includes('failedRoleAssignment')) {
throw new Error(l10n.t('Failed to create role assignment(s).'));
}
}
}
}
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 {
Expand Down
2 changes: 2 additions & 0 deletions azure/src/wizard/UserAssignedIdentityListStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as vscode from 'vscode';
import * as types from '../../index';
import { createManagedServiceIdentityClient } from '../clients';
import { uiUtils } from '../utils/uiUtils';
import { ResourceGroupListStep } from './ResourceGroupListStep';
import { UserAssignedIdentityCreateStep } from './UserAssignedIdentityCreateStep';

export class UserAssignedIdentityListStep<T extends types.IResourceGroupWizardContext> extends AzureWizardPromptStep<T> {
Expand All @@ -31,6 +32,7 @@ export class UserAssignedIdentityListStep<T extends types.IResourceGroupWizardCo
public async getSubWizard(wizardContext: T): Promise<IWizardOptions<T> | undefined> {
if (!wizardContext.managedIdentity) {
return {
promptSteps: [new ResourceGroupListStep()],
executeSteps: [new UserAssignedIdentityCreateStep()]
}
}
Expand Down