Skip to content

Commit

Permalink
Read-only Implementation of Managed Identities (#933)
Browse files Browse the repository at this point in the history
  • Loading branch information
nturinski authored Jan 28, 2025
1 parent bd9a493 commit b63a2bc
Show file tree
Hide file tree
Showing 19 changed files with 14,274 additions and 2,111 deletions.
15,908 changes: 13,816 additions & 2,092 deletions NOTICE.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions api/src/AzExtResourceType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export enum AzExtResourceType {
SpringApps = 'SpringApps',
SqlDatabases = 'SqlDatabases',
SqlServers = 'SqlServers',
Subscription = 'Subscription',
VirtualMachineScaleSets = 'VirtualMachineScaleSets',
VirtualNetworks = 'VirtualNetworks',
WebHostingEnvironments = 'WebHostingEnvironments',
Expand Down
2 changes: 2 additions & 0 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,8 @@
"webpack-cli": "^4.6.0"
},
"dependencies": {
"@azure/arm-authorization": "^9.0.0",
"@azure/arm-msi": "^2.1.0",
"@azure/arm-resources": "^5.2.0",
"@azure/arm-resources-profile-2020-09-01-hybrid": "^2.1.0",
"@azure/ms-rest-js": "^2.7.0",
Expand Down
36 changes: 18 additions & 18 deletions resources/azureIcons/ResourceGroup.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions resources/azureIcons/Subscription.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/azureExtensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ export const azureExtensions: IAzExtMetadata[] = [
],
reportIssueCommandId: 'azureWebPubSub.reportIssue'
},
{
name: 'vscode-azureresourcegroups',
label: 'Managed Identity',
resourceTypes: [
AzExtResourceType.ManagedIdentityUserAssignedIdentities
]
},
{
name: 'vscode-ai-foundry',
publisher: 'ms-toolsai',
Expand Down
2 changes: 2 additions & 0 deletions src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AzExtTreeItem, IActionContext, isAzExtTreeItem, openUrl, registerComman
import { commands } from 'vscode';
import { uploadFileToCloudShell } from '../cloudConsole/uploadFileToCloudShell';
import { ext } from '../extensionVariables';
import { loadAllSubscriptionRoleAssignments } from '../managedIdentity/loadAllSubscriptionRoleAssignments';
import { BranchDataItemWrapper } from '../tree/BranchDataItemWrapper';
import { ResourceGroupsItem } from '../tree/ResourceGroupsItem';
import { GroupingItem } from '../tree/azure/grouping/GroupingItem';
Expand Down Expand Up @@ -118,6 +119,7 @@ export function registerCommands(): void {
});

registerCommand('azureWorkspace.loadMore', async (context: IActionContext, node: AzExtTreeItem) => await ext.workspaceTree.loadMore(node, context));
registerCommand('azureResources.loadAllSubscriptionRoleAssignments', loadAllSubscriptionRoleAssignments);

registerCommand('azureTenantsView.configureSovereignCloud', configureSovereignCloud);
}
Expand Down
4 changes: 4 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AzExtTreeDataProvider, AzureExtensionApiFactory, IActionContext, callWi
import { AzureSubscription } from 'api/src';
import { GetApiOptions, apiUtils } from 'api/src/utils/apiUtils';
import * as vscode from 'vscode';
import { AzExtResourceType } from '../api/src/AzExtResourceType';
import { ActivityLogTreeItem } from './activityLog/ActivityLogsTreeItem';
import { registerActivity } from './activityLog/registerActivity';
import { DefaultAzureResourceProvider } from './api/DefaultAzureResourceProvider';
Expand All @@ -29,6 +30,7 @@ import { registerTagDiagnostics } from './commands/tags/registerTagDiagnostics';
import { ext } from './extensionVariables';
import { gitHubCopilotForAzureToast } from './gitHubCopilotForAzure';
import { AzureResourcesApiInternal } from './hostapi.v2.internal';
import { ManagedIdentityBranchDataProvider } from './managedIdentity/ManagedIdentityBranchDataProvider';
import { survey } from './nps';
import { getSubscriptionProviderFactory } from './services/getSubscriptionProviderFactory';
import { BranchDataItemCache } from './tree/BranchDataItemCache';
Expand Down Expand Up @@ -185,6 +187,8 @@ export async function activate(context: vscode.ExtensionContext, perfStats: { lo
};

ext.v2.api = v2ApiFactory.createApi({ extensionId: 'ms-azuretools.vscode-azureresourcegroups' });
ext.managedIdentityBranchDataProvider = new ManagedIdentityBranchDataProvider();
ext.v2.api.resources.registerAzureResourceBranchDataProvider(AzExtResourceType.ManagedIdentityUserAssignedIdentities, ext.managedIdentityBranchDataProvider);

ext.appResourceTree = new CompatibleAzExtTreeDataProvider(azureResourceTreeDataProvider);
ext.workspaceTree = new CompatibleAzExtTreeDataProvider(workspaceResourceTreeDataProvider);
Expand Down
2 changes: 2 additions & 0 deletions src/extensionVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DiagnosticCollection, Disposable, ExtensionContext, TreeView } from "vs
import { ActivityLogTreeItem } from "./activityLog/ActivityLogsTreeItem";
import { TagFileSystem } from "./commands/tags/TagFileSystem";
import { AzureResourcesApiInternal } from "./hostapi.v2.internal";
import { ManagedIdentityBranchDataProvider } from "./managedIdentity/ManagedIdentityBranchDataProvider";
import { AzureResourcesServiceFactory } from "./services/AzureResourcesService";
import { ResourceGroupsItem } from "./tree/ResourceGroupsItem";
import { TreeItemStateStore } from "./tree/TreeItemState";
Expand Down Expand Up @@ -48,6 +49,7 @@ export namespace ext {
export let azureTreeState: TreeItemStateStore;

export let subscriptionProviderFactory: () => Promise<AzureSubscriptionProvider>;
export let managedIdentityBranchDataProvider: ManagedIdentityBranchDataProvider;

export namespace v2 {
export let api: AzureResourcesApiInternal;
Expand Down
75 changes: 75 additions & 0 deletions src/managedIdentity/ManagedIdentityBranchDataProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { RoleAssignment } from '@azure/arm-authorization';
import { uiUtils } from '@microsoft/vscode-azext-azureutils';
import { callWithTelemetryAndErrorHandling, createSubscriptionContext, type IActionContext } from '@microsoft/vscode-azext-utils';
import { AzureResource, AzureResourceModel, BranchDataProvider } from '@microsoft/vscode-azureresources-api';
import * as vscode from 'vscode';
import { localize } from 'vscode-nls';
import { ext } from '../extensionVariables';
import { ResourceGroupsItem } from '../tree/ResourceGroupsItem';
import { createAuthorizationManagementClient } from '../utils/azureClients';
import { ManagedIdentityItem } from './ManagedIdentityItem';
export class ManagedIdentityBranchDataProvider extends vscode.Disposable implements BranchDataProvider<AzureResource, AzureResourceModel> {
private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter<ResourceGroupsItem | undefined>();
public roleAssignmentsTask: Promise<{ [id: string]: RoleAssignment[] }>;

constructor() {
super(
() => {
this.onDidChangeTreeDataEmitter.dispose();
});
this.roleAssignmentsTask = this.initialize();
}

get onDidChangeTreeData(): vscode.Event<ResourceGroupsItem | undefined> {
return this.onDidChangeTreeDataEmitter.event;
}

public async initialize(): Promise<{ [id: string]: RoleAssignment[] }> {
return await callWithTelemetryAndErrorHandling('initializeManagedIdentityBranchDataProvider', async (context: IActionContext) => {
const provider = await ext.subscriptionProviderFactory();
const allSubscriptions = await provider.getSubscriptions(false /*filter*/);
const roleAssignments: { [id: string]: RoleAssignment[] } = {};
await Promise.allSettled(allSubscriptions.map(async (subscription) => {
const subContext = createSubscriptionContext(subscription);
const authClient = await createAuthorizationManagementClient([context, subContext]);
roleAssignments[subscription.subscriptionId] = await uiUtils.listAllIterator(authClient.roleAssignments.listForSubscription());
}));

return roleAssignments;
}) ?? {};
}

async getChildren(element: ResourceGroupsItem): Promise<ResourceGroupsItem[] | null | undefined> {
return (await element.getChildren?.())?.map((child) => {
return ext.azureTreeState.wrapItemInStateHandling(child, () => this.refresh(child))
});
}

async getResourceItem(element: AzureResource): Promise<ResourceGroupsItem> {
const resourceItem = await callWithTelemetryAndErrorHandling(
'getResourceItem',
async (context: IActionContext) => {
context.errorHandling.rethrow = true;
return new ManagedIdentityItem(element.subscription, element);
});

if (!resourceItem) {
throw new Error(localize('failedToGetResourceItem', 'Failed to get resource item for "{0}"', element.id));
}

return ext.azureTreeState.wrapItemInStateHandling(resourceItem, () => this.refresh(resourceItem));
}

async getTreeItem(element: ResourceGroupsItem): Promise<vscode.TreeItem> {
return await element.getTreeItem();
}

refresh(element?: ResourceGroupsItem): void {
this.onDidChangeTreeDataEmitter.fire(element);
}
}
110 changes: 110 additions & 0 deletions src/managedIdentity/ManagedIdentityItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* 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 { uiUtils } from "@microsoft/vscode-azext-azureutils";
import { callWithTelemetryAndErrorHandling, createContextValue, createSubscriptionContext, nonNullProp, type IActionContext } from "@microsoft/vscode-azext-utils";
import { type AzureResource, type AzureSubscription, type ViewPropertiesModel } from "@microsoft/vscode-azureresources-api";
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode";
import { getAzExtResourceType } from "../../api/src/index";
import { getAzureResourcesService } from "../services/AzureResourcesService";
import { GenericItem } from "../tree/GenericItem";
import { ResourceGroupsItem } from "../tree/ResourceGroupsItem";
import { createAuthorizationManagementClient, createManagedServiceIdentityClient } from "../utils/azureClients";
import { getIconPath } from "../utils/azureUtils";
import { localize } from "../utils/localize";
import { RoleAssignmentsItem } from "./RoleAssignmentsItem";
import { RoleDefinitionsItem } from "./RoleDefinitionsItem";

export class ManagedIdentityItem implements ResourceGroupsItem {
static readonly contextValue: string = 'managedIdentityItem';
static readonly contextValueRegExp: RegExp = new RegExp(ManagedIdentityItem.contextValue);
name: string;
id: string;

constructor(public readonly subscription: AzureSubscription,
public readonly resource: AzureResource) {
this.id = resource.id;
this.name = resource.name;
}

viewProperties: ViewPropertiesModel = {
data: this.resource,
label: this.resource.name,
}

private get contextValue(): string {
const values: string[] = [];
values.push(ManagedIdentityItem.contextValue);
return createContextValue(values);
}

async getChildren(): Promise<(GenericItem | RoleDefinitionsItem | RoleAssignmentsItem)[]> {
const result = await callWithTelemetryAndErrorHandling('managedIdentityItem.getChildren', async (context: IActionContext) => {
const subContext = createSubscriptionContext(this.subscription);
const msiClient = await createManagedServiceIdentityClient([context, subContext]);
const msi: Identity = await msiClient.userAssignedIdentities.get(nonNullProp(this.resource, 'resourceGroup'), this.resource.name);

const resources = await getAzureResourcesService().listResources(context, this.subscription);
const assignedRoleAssignment = new RoleAssignmentsItem(localize('sourceResources', 'Source resources'), this.subscription, msi);
const accessRoleAssignment = new RoleAssignmentsItem(localize('targetServices', 'Target services'), this.subscription, msi);

const assignedResources = resources.filter((r) => {
// verify the msi is assigned to the resource by checking if the msi id is in the userAssignedIdentities
const userAssignedIdentities = r.identity?.userAssignedIdentities;
if (!userAssignedIdentities) {
return false;
}

if (!msi.id) {
return false;
}

return userAssignedIdentities[msi.id] !== undefined
}).map((r) => {
return new GenericItem(nonNullProp(r, 'name'), { id: `${msi.id}/${r.name}`, iconPath: getIconPath(r.type ? getAzExtResourceType({ type: r.type }) : undefined) });
});

const authClient = await createAuthorizationManagementClient([context, subContext]);
const roleAssignment = await uiUtils.listAllIterator(authClient.roleAssignments.listForSubscription());
// filter the role assignments to only show the ones that are assigned to the msi
const filteredBySub = roleAssignment.filter((ra) => ra.principalId === msi.principalId);

const targetResources = await accessRoleAssignment.getRoleDefinitionsItems(context, filteredBySub);
const children = [];

if (assignedResources.length > 0) {
// if there weren't any assigned resources, don't show that section
assignedRoleAssignment.addChildren(assignedResources);
children.push(assignedRoleAssignment);
}

accessRoleAssignment.addChildren(targetResources);
children.push(accessRoleAssignment);
accessRoleAssignment.addChild(new GenericItem(localize('showResources', 'Show resources from other subscriptions...'),
{
id: accessRoleAssignment.id + '/showResourcesFromOtherSubscriptions',
iconPath: new ThemeIcon('sync'),
commandId: 'azureResources.loadAllSubscriptionRoleAssignments',
commandArgs: [accessRoleAssignment]
}))
return children;
});

return result ?? [];
}

getTreeItem(): TreeItem {
return {
label: this.resource.name,
id: this.id,
iconPath: getIconPath(this.resource.resourceType),
contextValue: this.contextValue,
collapsibleState: TreeItemCollapsibleState.Collapsed,
}
}
}


Loading

0 comments on commit b63a2bc

Please sign in to comment.