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

Read-only Implementation of Managed Identities #933

Merged
merged 23 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1034a9d
Bump version, changelog, and notice for release
nturinski Aug 8, 2024
72df9ec
Read-only implementation of managed identities
nturinski Sep 30, 2024
0aacc56
Merge from main
nturinski Sep 30, 2024
8fede13
Save notice
nturinski Sep 30, 2024
e365bce
Fix package-lock.json
nturinski Oct 1, 2024
a582bbf
Reinit package-lock
nturinski Oct 1, 2024
dd53107
Clean up some role definitions code
nturinski Oct 2, 2024
12b8874
Merge branch 'main' into nat/managedIdentityReadOnly
nturinski Oct 8, 2024
775c644
Merge from main
nturinski Dec 10, 2024
47dd35a
Merge branch 'nat/managedIdentityReadOnly' of https://github.com/micr…
nturinski Dec 10, 2024
d1936d6
PR feedback
nturinski Dec 11, 2024
499ec6a
WIP
nturinski Dec 31, 2024
85119a8
Merge from main
nturinski Jan 14, 2025
f0fd37f
Fix typings
nturinski Jan 14, 2025
a3cc389
Nest the target and source resources
nturinski Jan 23, 2025
765f0d6
Merge branch 'main' into nat/managedIdentityReadOnly
nturinski Jan 23, 2025
a6ee1c9
Make role defintion id unique
nturinski Jan 23, 2025
7d2a04a
Merge branch 'nat/managedIdentityReadOnly' of https://github.com/micr…
nturinski Jan 23, 2025
07e06ac
Merge branch 'main' into nat/managedIdentityReadOnly
nturinski Jan 23, 2025
0ed6e1b
Some cleanup/comments for clarity
nturinski Jan 23, 2025
237549d
Merge branch 'nat/managedIdentityReadOnly' of https://github.com/micr…
nturinski Jan 23, 2025
fd22daf
Couple more clean-ups
nturinski Jan 23, 2025
aef9c22
Merge branch 'main' into nat/managedIdentityReadOnly
nturinski Jan 23, 2025
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
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
Copy link
Member

Choose a reason for hiding this comment

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

What changed with the icon?

Copy link
Member Author

Choose a reason for hiding this comment

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

I am honestly not entirely sure. I think at one point, I changed the name but I ended up reverting that. 🤔

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
Loading