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

feat: deprecate azure account and support select subs and tenant #383

Merged
merged 10 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from 8 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
5 changes: 3 additions & 2 deletions package-lock.json

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

37 changes: 32 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "vscode-apimanagement",
"displayName": "Azure API Management",
"description": "An Azure API Management extension for Visual Studio Code.",
"version": "1.0.9",
"version": "1.0.10",
"publisher": "ms-azuretools",
"icon": "resources/apim-icon-newone.png",
"aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217",
Expand Down Expand Up @@ -190,13 +190,19 @@
"title": "%azureApiManagement.openInPortal%",
"category": "Azure API Management"
},
{
"command": "azureApiManagement.selectTenant",
"title": "%azureApiManagement.selectTenant%",
"category": "Azure API Management"
},
{
"command": "azureApiManagement.selectSubscriptions",
"title": "Select Subscription...",
"title": "%azureApiManagement.selectSubscriptions%",
"icon": {
"light": "resources/light/filter.svg",
"dark": "resources/dark/filter.svg"
}
},
"category": "Azure API Management"
},
{
"command": "azureApiManagement.Refresh",
Expand Down Expand Up @@ -782,6 +788,27 @@
"type": "boolean",
"default": false,
"description": "%azureApiManagement.advancedPolicyAuthoringExperience%"
},
"azureApiManagement.selectedSubscriptions": {
"type": "array",
"description": "Selected Azure subscriptions",
"items": {
"type": "string"
}
},
"azureApiManagement.selectedTenant": {
"type": "object",
"description": "A specific tenant to sign in to",
"properties": {
"name": {
"type": "string",
"description": "tenant name"
},
"id": {
"type": "string",
"description": "tenant id"
}
}
}
}
}
Expand Down Expand Up @@ -834,6 +861,7 @@
"@azure/arm-apimanagement": "^9.2.0",
"@azure/arm-appservice": "^15.0.0",
"@azure/arm-resources": "^4.0.0",
"@azure/arm-resources-subscriptions": "^2.1.0",
"@azure/ms-rest-nodeauth": "^3.1.1",
"@microsoft/vscode-azext-azureutils": "^3.1.2",
"@microsoft/vscode-azext-utils": "^2.5.12",
Expand All @@ -857,10 +885,9 @@
"xregexp": "^4.3.0"
},
"extensionDependencies": [
"ms-vscode.azure-account",
"humao.rest-client"
],
"overrides": {
"fsevents": "~2.3.2"
}
}
}
4 changes: 3 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,7 @@
"azureApiManagement.deleteAuthorizationAccessPolicy": "Delete Access Policy",
"azureApiManagement.showArmAuthorizationProvider": "Edit Authorization Provider",
"azureApiManagement.showArmAuthorization": "Edit Authorization",
"azureApiManagement.showArmAuthorizationAccessPolicy": "Edit Authorization Access Policy"
"azureApiManagement.showArmAuthorizationAccessPolicy": "Edit Authorization Access Policy",
"azureApiManagement.selectTenant": "Select Tenant...",
"azureApiManagement.selectSubscriptions": "Select Subscription..."
}
56 changes: 56 additions & 0 deletions src/azure/azureLogin/authTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { AuthenticationSession, Event } from "vscode";
import { GeneralUtils } from "../../utils/generalUtils";

export enum SignInStatus {
Initializing = 'Initializing',
SigningIn = 'SigningIn',
SignedIn = 'SignedIn',
SignedOut = 'SignedOut',
}

export enum SelectionType {
Filtered,
All,
AllIfNoFilters,
}

export interface SubscriptionFilter {
tenantId: string;
subscriptionId: string;
}

export type TokenInfo = {
token: string;
expiry: Date;
};

export type AzureAuthenticationSession = AuthenticationSession & {
tenantId: string;
};

export type Tenant = {
name: string;
id: string;
};

export type GetAuthSessionOptions = {
applicationClientId?: string;
scopes?: string[];
};

export type AzureSessionProvider = {
signIn(): Promise<void>;
signInStatus: SignInStatus;
availableTenants: Tenant[];
selectedTenant: Tenant | null;
signInStatusChangeEvent: Event<SignInStatus>;
getAuthSession(options?: GetAuthSessionOptions): Promise<GeneralUtils.Errorable<AzureAuthenticationSession>>;
dispose(): void;
};

export type ReadyAzureSessionProvider = AzureSessionProvider & {
signInStatus: SignInStatus.SignedIn;
selectedTenant: Tenant;
};
149 changes: 149 additions & 0 deletions src/azure/azureLogin/azureAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { SubscriptionClient, TenantIdDescription } from "@azure/arm-resources-subscriptions";
import { TokenCredential } from "@azure/core-auth";
import { AuthenticationSession, QuickPickItem, Uri, env, window, workspace, ConfigurationTarget } from "vscode";
import { UiStrings } from "../../uiStrings";
import { GeneralUtils } from "../../utils/generalUtils";
import { SelectionType, SignInStatus, SubscriptionFilter, Tenant } from "./authTypes";
import { AzureAuth } from "./azureAuth";
import { AzureSessionProviderHelper } from "./azureSessionProvider";
import { AzureSubscriptionHelper } from "./subscriptions";
import { AzureAccountUrl, extensionPrefix } from "../../constants";
import { AzureLoginConstantString } from "./constants";
export namespace AzureAccount {
export async function signInToAzure(): Promise<void> {
await AzureSessionProviderHelper.getSessionProvider().signIn();
}

export function getSelectedTenant(): Tenant | undefined {
return workspace.getConfiguration(extensionPrefix).get<Tenant>(AzureLoginConstantString.selectedTenant);
}

export async function updateSelectedTenant(value?: Tenant): Promise<void> {
await workspace.getConfiguration(extensionPrefix).update(AzureLoginConstantString.selectedTenant, value, ConfigurationTarget.Global, true);
}

export async function selectTenant(): Promise<void> {
const sessionProvider = AzureSessionProviderHelper.getSessionProvider();
if (sessionProvider.signInStatus !== SignInStatus.SignedIn) {
window.showInformationMessage(UiStrings.SelectTenantBeforeSignIn);
return;
}

if (sessionProvider.availableTenants.length === 1) {
sessionProvider.selectedTenant = sessionProvider.availableTenants[0];

// If this tenant wasn't previously selected, it was probably because it wasn't immediately
// accessible (the user's current token didn't have access to it). Calling getAuthSession
// will prompt the user to re-authenticate if necessary.
const sessionResult = await sessionProvider.getAuthSession();
if (GeneralUtils.failed(sessionResult)) {
window.showErrorMessage(sessionResult.error);
}

return;
}

const selectedTenant = await AzureAuth.quickPickTenant(sessionProvider.availableTenants);
if (!selectedTenant) {
window.showInformationMessage(UiStrings.NoTenantSelected);
return;
}

sessionProvider.selectedTenant = selectedTenant;
await updateSelectedTenant(selectedTenant);
}

type SubscriptionQuickPickItem = QuickPickItem & { subscription: SubscriptionFilter };

export async function selectSubscriptions(): Promise<void> {
const sessionProvider = await AzureAuth.getReadySessionProvider();
if (GeneralUtils.failed(sessionProvider)) {
window.showErrorMessage(sessionProvider.error);
return;
}

const allSubscriptions = await AzureSubscriptionHelper.getSubscriptions(sessionProvider.result, SelectionType.All);
if (GeneralUtils.failed(allSubscriptions)) {
window.showErrorMessage(allSubscriptions.error);
return;
}

if (allSubscriptions.result.length === 0) {
const noSubscriptionsFound = UiStrings.NoSubscriptionsFoundAndSetup;
const setupAccount = UiStrings.SetUpAzureAccount;
const response = await window.showInformationMessage(noSubscriptionsFound, setupAccount);
if (response === setupAccount) {
env.openExternal(Uri.parse(AzureAccountUrl.azureMicrosoftLink));
}

return;
}

const session = await sessionProvider.result.getAuthSession();
if (GeneralUtils.failed(session)) {
window.showErrorMessage(session.error);
return;
}

const filteredSubscriptions = await AzureSubscriptionHelper.getFilteredSubscriptions();

const subscriptionsInCurrentTenant = filteredSubscriptions.filter(
(sub) => sub.tenantId === session.result.tenantId,
);
const subscriptionsInOtherTenants = filteredSubscriptions.filter((sub) => sub.tenantId !== session.result.tenantId);

const quickPickItems: SubscriptionQuickPickItem[] = allSubscriptions.result.map((sub) => {
return {
label: sub.displayName || "",
description: sub.subscriptionId,
picked: subscriptionsInCurrentTenant.some((filtered) => filtered.subscriptionId === sub.subscriptionId),
subscription: {
subscriptionId: sub.subscriptionId || "",
tenantId: sub.tenantId || "",
},
};
});

const selectedItems = await window.showQuickPick(quickPickItems, {
canPickMany: true,
placeHolder: UiStrings.SelectSubscription,
});

if (!selectedItems) {
return;
}

const newFilteredSubscriptions = [
...selectedItems.map((item) => item.subscription),
...subscriptionsInOtherTenants, // Retain filters in any other tenants.
];

await AzureSubscriptionHelper.setFilteredSubscriptions(newFilteredSubscriptions);
}
export async function getTenants(session: AuthenticationSession): Promise<GeneralUtils.Errorable<Tenant[]>> {
const armEndpoint = AzureAuth.getConfiguredAzureEnv().resourceManagerEndpointUrl;
const credential: TokenCredential = {
getToken: async () => {
return { token: session.accessToken, expiresOnTimestamp: 0 };
},
};
const subscriptionClient = new SubscriptionClient(credential, { endpoint: armEndpoint });

const tenantsResult = await AzureAuth.listAll(subscriptionClient.tenants.list());
return GeneralUtils.errMap(tenantsResult, (t) => t.filter(isTenant).map((t) => ({ name: t.displayName, id: t.tenantId })));
}
export function findTenant(tenants: Tenant[], tenantId: string): Tenant | null {
return tenants.find((t) => t.id === tenantId) || null;
}
export function isTenant(tenant: TenantIdDescription): tenant is { tenantId: string; displayName: string } {
return tenant.tenantId !== undefined && tenant.displayName !== undefined;
}
export function getIdString(tenants: Tenant[]): string {
return tenants
.map((t) => t.id)
.sort()
.join(",");
}
}
Loading