From d7331135716e8321f6de92bdbe3a3c907d46086b Mon Sep 17 00:00:00 2001 From: wenyutang Date: Tue, 11 Feb 2025 14:25:50 +0800 Subject: [PATCH 01/10] feat: remove azure account --- package-lock.json | 5 +- package.json | 2 +- src/azure/azureLogin/authTypes.ts | 56 ++++ src/azure/azureLogin/azureAccount.ts | 138 ++++++++++ src/azure/azureLogin/azureAuth.ts | 149 ++++++++++ src/azure/azureLogin/azureSessionProvider.ts | 274 +++++++++++++++++++ src/azure/azureLogin/subscriptions.ts | 88 ++++++ src/constants.ts | 6 + src/explorer/ApiManagementProvider.ts | 7 + src/explorer/AzureAccountTreeItem.ts | 213 +++++++++++++- src/explorer/SubscriptionsTreeItem.ts | 9 +- src/uiStrings.ts | 103 +++++++ src/utils/generalUtils.ts | 49 ++++ 13 files changed, 1085 insertions(+), 14 deletions(-) create mode 100644 src/azure/azureLogin/authTypes.ts create mode 100644 src/azure/azureLogin/azureAccount.ts create mode 100644 src/azure/azureLogin/azureAuth.ts create mode 100644 src/azure/azureLogin/azureSessionProvider.ts create mode 100644 src/azure/azureLogin/subscriptions.ts create mode 100644 src/uiStrings.ts create mode 100644 src/utils/generalUtils.ts diff --git a/package-lock.json b/package-lock.json index afabb18..c1cf678 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "vscode-apimanagement", - "version": "1.0.8", + "version": "1.0.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "vscode-apimanagement", - "version": "1.0.8", + "version": "1.0.9", "dependencies": { "@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", diff --git a/package.json b/package.json index 98d31fa..830da7c 100644 --- a/package.json +++ b/package.json @@ -834,6 +834,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", @@ -857,7 +858,6 @@ "xregexp": "^4.3.0" }, "extensionDependencies": [ - "ms-vscode.azure-account", "humao.rest-client" ], "overrides": { diff --git a/src/azure/azureLogin/authTypes.ts b/src/azure/azureLogin/authTypes.ts new file mode 100644 index 0000000..033c82d --- /dev/null +++ b/src/azure/azureLogin/authTypes.ts @@ -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; + signInStatus: SignInStatus; + availableTenants: Tenant[]; + selectedTenant: Tenant | null; + signInStatusChangeEvent: Event; + getAuthSession(options?: GetAuthSessionOptions): Promise>; + dispose(): void; +}; + +export type ReadyAzureSessionProvider = AzureSessionProvider & { + signInStatus: SignInStatus.SignedIn; + selectedTenant: Tenant; +}; diff --git a/src/azure/azureLogin/azureAccount.ts b/src/azure/azureLogin/azureAccount.ts new file mode 100644 index 0000000..97fefb2 --- /dev/null +++ b/src/azure/azureLogin/azureAccount.ts @@ -0,0 +1,138 @@ +// 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 } 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"; +export namespace AzureAccount { + export async function signInToAzure(): Promise { + await AzureSessionProviderHelper.getSessionProvider().signIn(); + } + + export async function selectTenant(): Promise { + 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; + } + + type SubscriptionQuickPickItem = QuickPickItem & { subscription: SubscriptionFilter }; + + export async function selectSubscriptions(): Promise { + 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("https://azure.microsoft.com/")); + } + + 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> { + 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(","); + } +} \ No newline at end of file diff --git a/src/azure/azureLogin/azureAuth.ts b/src/azure/azureLogin/azureAuth.ts new file mode 100644 index 0000000..7807634 --- /dev/null +++ b/src/azure/azureLogin/azureAuth.ts @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +import { TokenCredential } from "@azure/core-auth"; +import { PagedAsyncIterableIterator } from "@azure/core-paging"; +import { Environment, EnvironmentParameters } from "@azure/ms-rest-azure-env"; +import * as vscode from "vscode"; +import { UiStrings } from "../../uiStrings"; +import { GeneralUtils } from "../../utils/generalUtils"; +import { AzureSessionProvider, GetAuthSessionOptions, ReadyAzureSessionProvider, SignInStatus, Tenant } from "./authTypes"; +import { AzureSessionProviderHelper } from "./azureSessionProvider"; +export namespace AzureAuth { + export function getEnvironment(): Environment { + return getConfiguredAzureEnv(); + } + + export function getCredential(sessionProvider: ReadyAzureSessionProvider): TokenCredential { + return { + getToken: async () => { + const session = await sessionProvider.getAuthSession(); + if (GeneralUtils.failed(session)) { + throw new Error(vscode.l10n.t(UiStrings.NoMSAuthSessionFound, session.error)); + } + + return { token: session.result.accessToken, expiresOnTimestamp: 0 }; + }, + }; + } + + export function getDefaultScope(endpointUrl: string): string { + // Endpoint URL is that of the audience, e.g. for ARM in the public cloud + // it would be "https://management.azure.com". + return endpointUrl.endsWith("/") ? `${endpointUrl}.default` : `${endpointUrl}/.default`; + } + + export async function quickPickTenant(tenants: Tenant[]): Promise { + const items: (vscode.QuickPickItem & { tenant: Tenant })[] = tenants.map((t) => ({ + label: `${t.name} (${t.id})`, + tenant: t, + })); + const result = await vscode.window.showQuickPick(items, { + placeHolder: UiStrings.SelectATenant, + }); + return result ? result.tenant : undefined; + } + + export async function getReadySessionProvider(): Promise> { + const sessionProvider = AzureSessionProviderHelper.getSessionProvider(); + if (AzureAuth.isReady(sessionProvider)) { + return { succeeded: true, result: sessionProvider }; + } + + switch (sessionProvider.signInStatus) { + case SignInStatus.Initializing: + case SignInStatus.SigningIn: + await waitForSignIn(sessionProvider); + break; + case SignInStatus.SignedOut: + await sessionProvider.signIn(); + break; + case SignInStatus.SignedIn: + break; + } + + // Get a session, which will prompt the user to select a tenant if necessary. + const session = await sessionProvider.getAuthSession(); + if (GeneralUtils.failed(session)) { + return { succeeded: false, error: `Failed to get authentication session: ${session.error}` }; + } + + if (!AzureAuth.isReady(sessionProvider)) { + return { succeeded: false, error: "Not signed in." }; + } + return { succeeded: true, result: sessionProvider }; + } + + async function waitForSignIn(sessionProvider: AzureSessionProvider): Promise { + const options: vscode.ProgressOptions = { + location: vscode.ProgressLocation.Notification, + title: UiStrings.WaitForSignIn, + cancellable: true, + }; + + await vscode.window.withProgress(options, (_, token) => { + let listener: vscode.Disposable | undefined; + token.onCancellationRequested(listener?.dispose()); + return new Promise((resolve) => { + listener = sessionProvider.signInStatusChangeEvent((status) => { + if (status === SignInStatus.SignedIn) { + listener?.dispose(); + resolve(undefined); + } + }); + }); + }); + } + + export function getConfiguredAzureEnv(): Environment { + // See: + // https://github.com/microsoft/vscode/blob/eac16e9b63a11885b538db3e0b533a02a2fb8143/extensions/microsoft-authentication/package.json#L40-L99 + const section = "microsoft-sovereign-cloud"; + const settingName = "environment"; + const authProviderConfig = vscode.workspace.getConfiguration(section); + const environmentSettingValue = authProviderConfig.get(settingName); + + if (environmentSettingValue === "ChinaCloud") { + return Environment.ChinaCloud; + } else if (environmentSettingValue === "USGovernment") { + return Environment.USGovernment; + } else if (environmentSettingValue === "custom") { + const customCloud = authProviderConfig.get("customEnvironment"); + if (customCloud) { + return new Environment(customCloud); + } + + throw new Error(vscode.l10n.t(UiStrings.CustomCloudChoiseNotConfigured, section, settingName)); + } + + return Environment.get(Environment.AzureCloud.name); + } + + export async function listAll(iterator: PagedAsyncIterableIterator): Promise> { + const result: T[] = []; + try { + for await (const page of iterator.byPage()) { + result.push(...page); + } + return { succeeded: true, result }; + } catch (e) { + return { succeeded: false, error: vscode.l10n.t(UiStrings.FailedToListGroup, GeneralUtils.getErrorMessage(e)) }; + } + } + + export function getScopes(tenantId: string | null, options: GetAuthSessionOptions): string[] { + const defaultScopes = options.scopes || [AzureAuth.getDefaultScope(AzureAuth.getConfiguredAzureEnv().resourceManagerEndpointUrl)]; + const tenantScopes = tenantId ? [`VSCODE_TENANT:${tenantId}`] : []; + const clientIdScopes = options.applicationClientId ? [`VSCODE_CLIENT_ID:${options.applicationClientId}`] : []; + return [...defaultScopes, ...tenantScopes, ...clientIdScopes]; + } + + type AuthProviderId = "microsoft" | "microsoft-sovereign-cloud"; + + export function getConfiguredAuthProviderId(): AuthProviderId { + return AzureAuth.getConfiguredAzureEnv().name === Environment.AzureCloud.name ? "microsoft" : "microsoft-sovereign-cloud"; + } + + export function isReady(provider: AzureSessionProvider): provider is ReadyAzureSessionProvider { + return provider.signInStatus === SignInStatus.SignedIn && provider.selectedTenant !== null; + } +} diff --git a/src/azure/azureLogin/azureSessionProvider.ts b/src/azure/azureLogin/azureSessionProvider.ts new file mode 100644 index 0000000..a37df6e --- /dev/null +++ b/src/azure/azureLogin/azureSessionProvider.ts @@ -0,0 +1,274 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + AuthenticationGetSessionOptions, + AuthenticationSession, + Event, + EventEmitter, + ExtensionContext, + Disposable as VsCodeDisposable, + authentication, + l10n +} from "vscode"; +import { UiStrings } from "../../uiStrings"; +import { GeneralUtils } from "../../utils/generalUtils"; +import { AzureAuthenticationSession, AzureSessionProvider, GetAuthSessionOptions, SignInStatus, Tenant } from "./authTypes"; +import { AzureAccount } from "./azureAccount"; +import { AzureAuth } from "./azureAuth"; + +enum AuthScenario { + Initialization, + SignIn, + GetSession, +} + +export namespace AzureSessionProviderHelper { + + let sessionProvider: AzureSessionProvider; + + export function activateAzureSessionProvider(context: ExtensionContext) { + sessionProvider = new AzureSessionProviderImpl(); + context.subscriptions.push(sessionProvider); + } + + export function getSessionProvider(): AzureSessionProvider { + return sessionProvider; + } + + class AzureSessionProviderImpl extends VsCodeDisposable implements AzureSessionProvider { + private readonly initializePromise: Promise; + private handleSessionChanges: boolean = true; + private tenants: Tenant[] = []; + private selectedTenantValue: Tenant | null = null; + + public readonly onSignInStatusChangeEmitter = new EventEmitter(); + public signInStatusValue: SignInStatus = SignInStatus.Initializing; + + public constructor() { + const disposable = authentication.onDidChangeSessions(async (e) => { + // Ignore events for non-microsoft providers + if (e.provider.id !== AzureAuth.getConfiguredAuthProviderId()) { + return; + } + + // Ignore events that we triggered. + if (!this.handleSessionChanges) { + return; + } + + // Silently check authentication status and tenants + await this.signInAndUpdateTenants(AuthScenario.Initialization); + }); + + super(() => { + this.onSignInStatusChangeEmitter.dispose(); + disposable.dispose(); + }); + + this.initializePromise = this.initialize(); + } + + public get signInStatus(): SignInStatus { + return this.signInStatusValue; + } + + public get signInStatusChangeEvent(): Event { + return this.onSignInStatusChangeEmitter.event; + } + + public get availableTenants(): Tenant[] { + return [...this.tenants]; + } + + public get selectedTenant(): Tenant | null { + return this.selectedTenantValue; + } + + public set selectedTenant(tenant: Tenant | null) { + const isValid = tenant === null || this.tenants.some((t) => t.id === tenant.id); + const isChanged = this.selectedTenantValue !== tenant; + if (isValid && isChanged) { + this.selectedTenantValue = tenant; + this.onSignInStatusChangeEmitter.fire(this.signInStatusValue); + } + } + + private async initialize(): Promise { + await this.signInAndUpdateTenants(AuthScenario.Initialization); + } + + /** + * Sign in to Azure interactively, i.e. prompt the user to sign in even if they have an active session. + * This allows the user to choose a different account or tenant. + */ + public async signIn(): Promise { + await this.initializePromise; + + const newSignInStatus = SignInStatus.SigningIn; + if (newSignInStatus !== this.signInStatusValue) { + this.signInStatusValue = newSignInStatus; + this.onSignInStatusChangeEmitter.fire(this.signInStatusValue); + } + + await this.signInAndUpdateTenants(AuthScenario.SignIn); + } + + private async signInAndUpdateTenants(authScenario: AuthScenario): Promise { + // Initially, try to get a session using the 'organizations' tenant/authority: + // https://learn.microsoft.com/en-us/entra/identity-platform/msal-client-application-configuration#authority + // This allows the user to sign in to the Microsoft provider and list tenants, + // but the resulting session will not allow tenant-level operations. For that, + // we need to get a session for a specific tenant. + const orgTenantId = "organizations"; + const scopes = AzureAuth.getScopes(orgTenantId, {}); + const getSessionResult = await this.getArmSession(orgTenantId, scopes, authScenario); + + // Get the tenants + const getTenantsResult = await GeneralUtils.bindAsync(getSessionResult, (session) => AzureAccount.getTenants(session)); + const newTenants = GeneralUtils.succeeded(getTenantsResult) ? getTenantsResult.result : []; + const tenantsChanged = AzureAccount.getIdString(newTenants) !== AzureAccount.getIdString(this.tenants); + + // Determine which tenant should be selected. We can't force the user to choose at this stage, + // so this can be null, and will be set when the user tries to get a session. + const newSelectedTenant = await this.getNewSelectedTenant(newTenants, this.selectedTenantValue, authScenario); + const selectedTenantChanged = newSelectedTenant?.id !== this.selectedTenantValue?.id; + + // Get the overall sign-in status. If the user has access to any tenants they are signed in. + const newSignInStatus = newTenants.length > 0 ? SignInStatus.SignedIn : SignInStatus.SignedOut; + const signInStatusChanged = newSignInStatus !== this.signInStatusValue; + + // Update the state and fire event if anything has changed. + this.selectedTenantValue = newSelectedTenant; + this.tenants = newTenants; + this.signInStatusValue = newSignInStatus; + if (signInStatusChanged || tenantsChanged || selectedTenantChanged) { + this.onSignInStatusChangeEmitter.fire(this.signInStatusValue); + } + } + + /** + * Get the current Azure session, silently if possible. + * @returns The current Azure session, if available. If the user is not signed in, or there are no tenants, + * an error message is returned. + */ + public async getAuthSession(options?: GetAuthSessionOptions): Promise> { + await this.initializePromise; + if (this.signInStatusValue !== SignInStatus.SignedIn) { + return { succeeded: false, error: l10n.t(UiStrings.NotSignInStatus, this.signInStatusValue) }; + } + + if (this.tenants.length === 0) { + return { succeeded: false, error: UiStrings.NoTenantFound }; + } + + if (!this.selectedTenantValue) { + if (this.tenants.length > 1) { + const selectedTenant = await AzureAuth.quickPickTenant(this.tenants); + if (!selectedTenant) { + return { succeeded: false, error: UiStrings.NoTenantSelected }; + } + + this.selectedTenantValue = selectedTenant; + } else { + this.selectedTenantValue = this.tenants[0]; + } + } + + // Get a session for a specific tenant. + const tenantId = this.selectedTenantValue.id; + const scopes = AzureAuth.getScopes(tenantId, options || {}); + return await this.getArmSession(tenantId, scopes, AuthScenario.GetSession); + } + + private async getNewSelectedTenant( + newTenants: Tenant[], + currentSelectedTenant: Tenant | null, + authScenario: AuthScenario, + ): Promise { + // For sign-in we ignore the current selected tenant because the user must be able to change it. + // For all other scenarios, we prefer to retain the current selected tenant if it is still valid. + const ignoreCurrentSelection = authScenario === AuthScenario.SignIn; + if (!ignoreCurrentSelection && currentSelectedTenant !== null) { + const isCurrentSelectedTenantValid = newTenants.some((t) => t.id === currentSelectedTenant.id); + if (isCurrentSelectedTenantValid) { + return currentSelectedTenant; + } + } + + // For sign-in, if there are multiple tenants, we should prompt the user to select one. + if (authScenario === AuthScenario.SignIn && newTenants.length > 1) { + return null; + } + + // For all other (non-interactive) scenarios, see if we can determine a default tenant to use. + const defaultTenant = await this.getDefaultTenantId(newTenants); + return defaultTenant; + } + + private async getDefaultTenantId(tenants: Tenant[]): Promise { + if (tenants.length === 1) { + return tenants[0]; + } + + // It may be the case that the user has access to multiple tenants, but only has a valid token for one of them. + // This might happen if the user has signed in to one recently, but not the others. In this case, we would want + // to default to the tenant that the user has a valid token for. + // Use the 'Initialization' scenario to ensure this is silent (no user interaction). + const getSessionPromises = tenants.map((t) => + this.getArmSession(t.id, AzureAuth.getScopes(t.id, {}), AuthScenario.Initialization), + ); + const results = await Promise.all(getSessionPromises); + const accessibleTenants = results.filter(GeneralUtils.succeeded).map((r) => r.result); + return accessibleTenants.length === 1 ? AzureAccount.findTenant(tenants, accessibleTenants[0].tenantId) : null; + } + + private async getArmSession( + tenantId: string, + scopes: string[], + authScenario: AuthScenario, + ): Promise> { + this.handleSessionChanges = false; + try { + let options: AuthenticationGetSessionOptions; + let silentFirst = false; + switch (authScenario) { + case AuthScenario.Initialization: + options = { createIfNone: false, clearSessionPreference: false, silent: true }; + break; + case AuthScenario.SignIn: + options = { createIfNone: true, clearSessionPreference: true, silent: false }; + break; + case AuthScenario.GetSession: + // the 'createIfNone' option cannot be used with 'silent', but really we want both + // flags here (i.e. create a session silently, but do create one if it doesn't exist). + // To allow this, we first try to get a session silently. + silentFirst = true; + options = { createIfNone: true, clearSessionPreference: false, silent: false }; + break; + } + + let session: AuthenticationSession | undefined; + if (silentFirst) { + // The 'silent' option is incompatible with most other options, so we completely replace the options object here. + session = await authentication.getSession(AzureAuth.getConfiguredAuthProviderId(), scopes, { silent: true }); + } + + if (!session) { + session = await authentication.getSession(AzureAuth.getConfiguredAuthProviderId(), scopes, options); + } + + if (!session) { + return { succeeded: false, error: UiStrings.NoAzureSessionFound }; + } + + return { succeeded: true, result: Object.assign(session, { tenantId }) }; + } catch (e) { + return { succeeded: false, error: l10n.t(UiStrings.FailedTo, GeneralUtils.getErrorMessage(e)) }; + } finally { + this.handleSessionChanges = true; + } + } + } + +} diff --git a/src/azure/azureLogin/subscriptions.ts b/src/azure/azureLogin/subscriptions.ts new file mode 100644 index 0000000..9b90e02 --- /dev/null +++ b/src/azure/azureLogin/subscriptions.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +import { Subscription, SubscriptionClient } from "@azure/arm-resources-subscriptions"; +import * as vscode from "vscode"; +import { GeneralUtils } from "../../utils/generalUtils"; +import { ReadyAzureSessionProvider, SelectionType, SubscriptionFilter } from "./authTypes"; +import { AzureAuth } from "./azureAuth"; + +export namespace AzureSubscriptionHelper { + const onFilteredSubscriptionsChangeEmitter = new vscode.EventEmitter(); + + export function getFilteredSubscriptionsChangeEvent() { + return onFilteredSubscriptionsChangeEmitter.event; + } + + export function getFilteredSubscriptions(): SubscriptionFilter[] { + try { + let values = vscode.workspace.getConfiguration("azure-api-center").get("selectedSubscriptions", []); + return values.map(asSubscriptionFilter).filter((v) => v !== null) as SubscriptionFilter[]; + } catch (e) { + return []; + } + } + + function asSubscriptionFilter(value: string): SubscriptionFilter | null { + try { + const parts = value.split("/"); + return { tenantId: parts[0], subscriptionId: parts[1] }; + } catch (e) { + return null; + } + } + + /** + * A subscription with the subscriptionId and displayName properties guaranteed to be defined. + */ + export type DefinedSubscription = Subscription & Required>; + + export async function getSubscriptions( + sessionProvider: ReadyAzureSessionProvider, + selectionType: SelectionType, + ): Promise> { + const client = getSubscriptionClient(sessionProvider); + const subsResult = await AzureAuth.listAll(client.subscriptions.list()); + return GeneralUtils.errMap(subsResult, (subs: any) => sortAndFilter(subs.filter(isDefinedSubscription), selectionType)); + } + + function sortAndFilter(subscriptions: DefinedSubscription[], selectionType: SelectionType): DefinedSubscription[] { + const attemptFilter = selectionType === SelectionType.Filtered || selectionType === SelectionType.AllIfNoFilters; + if (attemptFilter) { + const filters = getFilteredSubscriptions(); + const filteredSubs = subscriptions.filter((s) => filters.some((f: any) => f.subscriptionId === s.subscriptionId)); + const returnAll = selectionType === SelectionType.AllIfNoFilters && filteredSubs.length === 0; + if (!returnAll) { + subscriptions = filteredSubs; + } + } + + return subscriptions.sort((a, b) => a.displayName.localeCompare(b.displayName)); + } + + function isDefinedSubscription(sub: Subscription): sub is DefinedSubscription { + return sub.subscriptionId !== undefined && sub.displayName !== undefined; + } + + export async function setFilteredSubscriptions(filters: SubscriptionFilter[]): Promise { + const existingFilters = getFilteredSubscriptions(); + const filtersChanged = + existingFilters.length !== filters.length || + !filters.every((f) => existingFilters.some((ef) => ef.subscriptionId === f.subscriptionId)); + + const values = filters.map((f) => `${f.tenantId}/${f.subscriptionId}`).sort(); + + if (filtersChanged) { + await vscode.workspace + .getConfiguration("azure-api-center") + .update("selectedSubscriptions", values, vscode.ConfigurationTarget.Global, true); + onFilteredSubscriptionsChangeEmitter.fire(); + } + } + + export function getSubscriptionClient(sessionProvider: ReadyAzureSessionProvider): SubscriptionClient { + return new SubscriptionClient(AzureAuth.getCredential(sessionProvider), { endpoint: getArmEndpoint() }); + } + function getArmEndpoint(): string { + return AzureAuth.getEnvironment().resourceManagerEndpointUrl; + } +} diff --git a/src/constants.ts b/src/constants.ts index 483e896..ddae7db 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -39,6 +39,12 @@ export enum GatewayKeyType { secondary = "secondary" } +export const APIMAccount = { + createAzureAccount: "Create an Azure Account...", + createAzureStudentAccount: "Create an Azure for Students Account...", + +} + // constants for extractor export const templatesFolder = "templates"; diff --git a/src/explorer/ApiManagementProvider.ts b/src/explorer/ApiManagementProvider.ts index 850a1f3..02c960a 100644 --- a/src/explorer/ApiManagementProvider.ts +++ b/src/explorer/ApiManagementProvider.ts @@ -18,6 +18,13 @@ import { getWorkspaceSetting, updateGlobalSetting } from '../vsCodeConfig/settin import { ServiceTreeItem } from './ServiceTreeItem'; import { treeUtils } from '../utils/treeUtils'; +export function createSubscriptionTreeItem( + parent: AzExtParentTreeItem, + subscription: ISubscriptionContext, +): AzExtTreeItem { + return new ApiManagementProvider(parent, subscription); +} + export class ApiManagementProvider extends SubscriptionTreeItemBase { public readonly childTypeLabel: string = localize('azureApiManagement.ApimService', 'API Management Service'); diff --git a/src/explorer/AzureAccountTreeItem.ts b/src/explorer/AzureAccountTreeItem.ts index e38da59..3d591f5 100644 --- a/src/explorer/AzureAccountTreeItem.ts +++ b/src/explorer/AzureAccountTreeItem.ts @@ -2,17 +2,210 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { UiStrings } from "../uiStrings"; +import * as vscode from "vscode"; +import { AzExtParentTreeItem, AzExtTreeItem, registerEvent, GenericTreeItem, ISubscriptionContext } from '@microsoft/vscode-azext-utils'; +import { AzureSubscriptionHelper } from "../azure/azureLogin/subscriptions"; +import { AzureSessionProvider, ReadyAzureSessionProvider, SelectionType, SignInStatus } from "../azure/azureLogin/authTypes"; +import { GeneralUtils } from "../utils/generalUtils"; +import { AzureAuth } from "../azure/azureLogin/azureAuth"; +import { APIMAccount } from "../constants"; +import { Subscription } from "@azure/arm-resources-subscriptions"; +import { createSubscriptionTreeItem } from "./ApiManagementProvider"; +// export class AzureAccountTreeItem extends AzureAccountTreeItemBase { +// public constructor(testAccount?: {}) { +// super(undefined, testAccount); +// } -import { ISubscriptionContext } from '@microsoft/vscode-azext-utils'; -import { AzureAccountTreeItemBase } from '@microsoft/vscode-azext-azureutils'; -import { ApiManagementProvider } from './ApiManagementProvider'; +// public createSubscriptionTreeItem(root: ISubscriptionContext): ApiManagementProvider { +// return new ApiManagementProvider(this, root); +// } +// } -export class AzureAccountTreeItem extends AzureAccountTreeItemBase { - public constructor(testAccount?: {}) { - super(undefined, testAccount); - } +export function createAzureAccountTreeItem( + sessionProvider: AzureSessionProvider, +): AzExtParentTreeItem & { dispose(): unknown } { + return new AzureAccountTreeItem(sessionProvider); +} - public createSubscriptionTreeItem(root: ISubscriptionContext): ApiManagementProvider { - return new ApiManagementProvider(this, root); +export class AzureAccountTreeItem extends AzExtParentTreeItem { + private subscriptionTreeItems: AzExtTreeItem[] | undefined; + public static contextValue: string = "azureApiCenterAzureAccount"; + public readonly contextValue: string = AzureAccountTreeItem.contextValue; + constructor(private readonly sessionProvider: AzureSessionProvider) { + super(undefined); + this.autoSelectInTreeItemPicker = true; + + const onStatusChange = this.sessionProvider.signInStatusChangeEvent; + const onFilteredSubscriptionsChange = AzureSubscriptionHelper.getFilteredSubscriptionsChangeEvent(); + registerEvent("azureAccountTreeItem.onSignInStatusChange", onStatusChange, (context) => this.refresh(context)); + registerEvent("azureAccountTreeItem.onSubscriptionFilterChange", onFilteredSubscriptionsChange, (context) => + this.refresh(context), + ); } -} + + public override get label() { + return UiStrings.AzureAccount; + } + + public dispose(): void { } + + public hasMoreChildrenImpl(): boolean { + return false; + } + + // no need to sort the array + public compareChildrenImpl(item1: AzExtTreeItem, item2: AzExtTreeItem): number { + return 0; + } + + public async loadMoreChildrenImpl(): Promise { + const existingSubscriptionTreeItems: AzExtTreeItem[] = this.subscriptionTreeItems || []; + this.subscriptionTreeItems = []; + + switch (this.sessionProvider.signInStatus) { + case SignInStatus.Initializing: + return [ + new GenericTreeItem(this, { + label: UiStrings.Loading, + contextValue: "azureCommand", + id: "azureapicenterAccountLoading", + iconPath: new vscode.ThemeIcon("loading~spin"), + }), + ]; + case SignInStatus.SignedOut: + return [ + new GenericTreeItem(this, { + label: UiStrings.SignIntoAzure, + commandId: "azure-api-center.signInToAzure", + contextValue: "azureCommand", + id: "azureapicenterAccountSignIn", + iconPath: new vscode.ThemeIcon("sign-in"), + includeInTreeItemPicker: true, + }), + new GenericTreeItem(this, { + label: UiStrings.CreateAzureAccount, + commandId: "azure-api-center.openUrl", + contextValue: "azureCommand", + id: APIMAccount.createAzureAccount, + iconPath: new vscode.ThemeIcon("add"), + includeInTreeItemPicker: true, + }), + new GenericTreeItem(this, { + label: UiStrings.CreateAzureStudentAccount, + commandId: "azure-api-center.openUrl", + contextValue: "azureCommand", + id: APIMAccount.createAzureStudentAccount, + iconPath: new vscode.ThemeIcon("mortar-board"), + includeInTreeItemPicker: true, + }) + ]; + case SignInStatus.SigningIn: + return [ + new GenericTreeItem(this, { + label: UiStrings.WaitForAzureSignIn, + contextValue: "azureCommand", + id: "azureapicenterAccountSigningIn", + iconPath: new vscode.ThemeIcon("loading~spin"), + }), + ]; + } + + if (this.sessionProvider.selectedTenant === null && this.sessionProvider.availableTenants.length > 1) { + // Signed in, but no tenant selected, AND there is more than one tenant to choose from. + return [ + new GenericTreeItem(this, { + label: UiStrings.SelectTenant, + commandId: "azure-api-center.selectTenant", + contextValue: "azureCommand", + id: "azureapicenterAccountSelectTenant", + iconPath: new vscode.ThemeIcon("account"), + includeInTreeItemPicker: true, + }), + ]; + } + + // Either we have a selected tenant, or there is only one available tenant and it's not selected + // because it requires extra interaction. Calling `getAuthSession` will complete that process. + // We will need the returned auth session in any case for creating a subscription context. + const session = await this.sessionProvider.getAuthSession(); + if (GeneralUtils.failed(session) || !AzureAuth.isReady(this.sessionProvider)) { + return [ + new GenericTreeItem(this, { + label: UiStrings.ErrorAuthenticating, + contextValue: "azureCommand", + id: "AzureAccountError", + iconPath: new vscode.ThemeIcon("error"), + }), + ]; + } + + const subscriptions = await AzureSubscriptionHelper.getSubscriptions(this.sessionProvider, SelectionType.AllIfNoFilters); + if (GeneralUtils.failed(subscriptions)) { + return [ + new GenericTreeItem(this, { + label: UiStrings.ErrorLoadingSubscriptions, + contextValue: "azureCommand", + id: "AzureAccountError", + iconPath: new vscode.ThemeIcon("error"), + description: subscriptions.error, + }), + ]; + } + + if (subscriptions.result.length === 0) { + return [ + new GenericTreeItem(this, { + label: UiStrings.NoSubscriptionsFound, + contextValue: "azureCommand", + id: "AzureAccountError", + iconPath: new vscode.ThemeIcon("info"), + }), + ]; + } + + // We've confirmed above that the provider is ready. + const readySessionProvider: ReadyAzureSessionProvider = this.sessionProvider; + + this.subscriptionTreeItems = await Promise.all( + subscriptions.result.map(async (subscription: any) => { + const existingTreeItem: AzExtTreeItem | undefined = existingSubscriptionTreeItems.find( + (ti) => ti.id === subscription.subscriptionId, + ); + if (existingTreeItem) { + return existingTreeItem; + } else { + const subscriptionContext = getSubscriptionContext( + readySessionProvider, + session.result, + subscription, + ); + return await createSubscriptionTreeItem(this, subscriptionContext); + } + }), + ); + + return this.subscriptionTreeItems!; + } + } + + function getSubscriptionContext( + sessionProvider: ReadyAzureSessionProvider, + session: vscode.AuthenticationSession, + subscription: Subscription, + ): ISubscriptionContext { + const credentials = AzureAuth.getCredential(sessionProvider); + const environment = AzureAuth.getEnvironment(); + + return { + credentials, + createCredentialsForScopes: ()=> { return Promise.resolve(credentials); }, + subscriptionDisplayName: subscription.displayName || "", + subscriptionId: subscription.subscriptionId || "", + subscriptionPath: `/subscriptions/${subscription.subscriptionId}`, + tenantId: subscription.tenantId || "", + userId: session.account.id, + environment, + isCustomCloud: environment.name === "AzureCustomCloud", + }; + } \ No newline at end of file diff --git a/src/explorer/SubscriptionsTreeItem.ts b/src/explorer/SubscriptionsTreeItem.ts index 6bb6578..4541a6f 100644 --- a/src/explorer/SubscriptionsTreeItem.ts +++ b/src/explorer/SubscriptionsTreeItem.ts @@ -4,12 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { SubscriptionContract } from "@azure/arm-apimanagement"; -import { AzExtTreeItem, AzExtParentTreeItem } from "@microsoft/vscode-azext-utils"; +import { AzExtTreeItem, AzExtParentTreeItem, ISubscriptionContext } from "@microsoft/vscode-azext-utils"; import { uiUtils } from "@microsoft/vscode-azext-azureutils"; import { treeUtils } from "../utils/treeUtils"; import { IServiceTreeRoot } from "./IServiceTreeRoot"; import { SubscriptionTreeItem } from "./SubscriptionTreeItem"; +export function createSubscriptionTreeItem( + parent: AzExtParentTreeItem, + subscription: ISubscriptionContext, +): AzExtTreeItem { + return new SubscriptionsTreeItem(parent, subscription); +} + export class SubscriptionsTreeItem extends AzExtParentTreeItem { public get iconPath(): { light: string, dark: string } { return treeUtils.getThemedIconPath('list'); diff --git a/src/uiStrings.ts b/src/uiStrings.ts new file mode 100644 index 0000000..dfcaef2 --- /dev/null +++ b/src/uiStrings.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +import * as vscode from "vscode"; + +export class UiStrings { + static readonly ApiTitle = vscode.l10n.t("API Title"); + static readonly ApiType = vscode.l10n.t("API Type"); + static readonly ApiVersionTitle = vscode.l10n.t("API Version Title"); + static readonly ApiVersionLifecycle = vscode.l10n.t("API Version Lifecycle"); + static readonly ApiDefinitionTitle = vscode.l10n.t("API Definition Title"); + static readonly ApiSpecificationName = vscode.l10n.t("API Specification Name"); + static readonly SelectFile = vscode.l10n.t("Select File"); + static readonly SelectApiDefinitionFile = vscode.l10n.t("Select API Definition File To Import"); + static readonly Import = vscode.l10n.t("Import"); + static readonly RegisterApi = vscode.l10n.t("Register API"); + static readonly CreatingApi = vscode.l10n.t("Creating API..."); + static readonly CreatingApiVersion = vscode.l10n.t("Creating API Version..."); + static readonly CreatingApiVersionDefinition = vscode.l10n.t("Creating API Version Definition..."); + static readonly ImportingApiDefinition = vscode.l10n.t("Importing API Definition..."); + static readonly ApiIsRegistered = vscode.l10n.t("API is registered."); + static readonly FailedToRegisterApi = vscode.l10n.t("Failed to register API."); + static readonly ValueNotBeEmpty = vscode.l10n.t("The value should not be empty."); + static readonly ValueAtLeast2Char = vscode.l10n.t("The value should have at least 2 characters of numbers or letters."); + static readonly ValueStartWithAlphanumeric = vscode.l10n.t("The value should start with letter or number."); + static readonly OpenWorkspace = vscode.l10n.t("Open a workspace in Visual Studio Code to generate a CI/CD pipeline."); + static readonly SelectCiCdProvider = vscode.l10n.t("Select CI/CD Provider"); + static readonly NoKiotaExtension = vscode.l10n.t("Please install the Microsoft Kiota extension to generate an API library."); + static readonly NoRestClientExtension = vscode.l10n.t("Please install the REST Client extension to test APIs with a HTTP file."); + static readonly NoSpectralExtension = vscode.l10n.t("Please install the Spectral extension to lint APIs."); + static readonly SetApiStyleGuide = vscode.l10n.t("Set API Style Guide"); + static readonly SelectRulesetFile = vscode.l10n.t("Select Ruleset File"); + static readonly Ruleset = vscode.l10n.t("Ruleset"); + static readonly RemoteUrlRuleset = vscode.l10n.t('Remote URL of Ruleset File'); + static readonly ValidUrlStart = vscode.l10n.t('Please enter a valid URL.'); + static readonly ValidUrlType = vscode.l10n.t('Please enter a valid URL to a JSON, YAML, or JavaScript file.'); + static readonly RulesetFileSet = vscode.l10n.t("API Style Guide is set to '{0}'."); + static readonly CopilotNoCmd = vscode.l10n.t("Hi! What can I help you with? Please use `/list` or `/find` to chat with me!"); + static readonly CopilotQueryData = vscode.l10n.t("Querying data from Azure API Center..."); + static readonly CopilotNoMoreApiSpec = vscode.l10n.t("⚠️ There are no more API specification documents."); + static readonly CopilotParseApiSpec = vscode.l10n.t("Parsing API Specifications..."); + static readonly CopilotParseApiSpecFor = vscode.l10n.t("Parsing API Specifications for '{0}'..."); + static readonly CopilotExceedsTokenLimit = vscode.l10n.t("The contents of the current file are too large for GitHub Copilot. Please try again with a smaller file."); + static readonly RegisterApiOptionStepByStep = vscode.l10n.t("Manual"); + static readonly RegisterApiOptionCicd = vscode.l10n.t("CI/CD"); + static readonly ApiRulesetOptionDefault = vscode.l10n.t("Default"); + static readonly ApiRulesetOptionAzureApiGuideline = vscode.l10n.t("Microsoft Azure REST API"); + static readonly ApiRulesetOptionSpectralOwasp = vscode.l10n.t("OWASP API Security Top 10"); + static readonly ApiRulesetOptionSelectFile = vscode.l10n.t("Select Local File"); + static readonly ApiRulesetOptionInputUrl = vscode.l10n.t("Input Remote URL"); + static readonly CICDTypeGitHub = vscode.l10n.t("GitHub"); + static readonly CICDTypeAzure = vscode.l10n.t("Azure DevOps"); + static readonly ApiSpecificationOptionApiCenter = vscode.l10n.t("Azure API Center"); + static readonly ApiSpecificationOptionLocalFile = vscode.l10n.t("Local File"); + static readonly ApiSpecificationOptionActiveEditor = vscode.l10n.t("Active Editor"); + static readonly TreeitemLabelApis = vscode.l10n.t("APIs"); + static readonly TreeitemLabelDefinitions = vscode.l10n.t("Definitions"); + static readonly TreeitemLabelVersions = vscode.l10n.t("Versions"); + static readonly TreeitemLabelEnvironments = vscode.l10n.t("Environments"); + static readonly SubscriptionTreeItemChildTypeLabel = vscode.l10n.t("API Center Service"); + static readonly ApiCenterTreeItemTreeItemChildTypeLabel = vscode.l10n.t("APIs or Environments"); + static readonly ApisTreeItemChildTypeLabel = vscode.l10n.t("API"); + static readonly ApiTreeItemChildTypeLabel = vscode.l10n.t("Deployments or Versions"); + static readonly ApiVersionsChildTypeLabel = vscode.l10n.t("API Version"); + static readonly ApiVersionChildTypeLabel = vscode.l10n.t("Definitions"); + static readonly ApiVersionDefinitionsTreeItemChildTypeLabel = vscode.l10n.t("API Definition"); + static readonly NoFolderOpened = vscode.l10n.t("No folder is opened. Please open a folder to use this feature."); + static readonly NoNodeInstalled = vscode.l10n.t("Node.js is not installed. Please install Node.js to use this feature."); + static readonly SelectFirstApiSpecification = vscode.l10n.t("Select first API specification document"); + static readonly SelectSecondApiSpecification = vscode.l10n.t("Select second API specification document"); + static readonly SelectApiSpecification = vscode.l10n.t("Select API specification document"); + static readonly OpticTaskName = vscode.l10n.t("Breaking Change Detection"); + static readonly OpticTaskSource = vscode.l10n.t("Azure API Center"); + static readonly SearchAPI = vscode.l10n.t('Search API'); + static readonly SearchAPIsResult = vscode.l10n.t("Search Result for '{0}'"); + static readonly SearchContentHint = vscode.l10n.t("Search for API name, kind, lifecycle stage"); + static readonly AIContentIncorrect = vscode.l10n.t("AI-generated content may be incorrect"); + static readonly NoActiveFileOpen = vscode.l10n.t("'No active file is open."); + static readonly GeneratingOpenAPI = vscode.l10n.t("Generating OpenAPI Specification from Current File..."); + static readonly SelectTenantBeforeSignIn = vscode.l10n.t("You must sign in before selecting a tenant."); + static readonly NoTenantSelected = vscode.l10n.t("No tenant selected."); + static readonly NoSubscriptionsFoundAndSetup = vscode.l10n.t("No subscriptions were found. Set up your account if you have yet to do so."); + static readonly SetUpAzureAccount = vscode.l10n.t("Set up Account"); + static readonly SelectSubscription = vscode.l10n.t("Select Subscriptions"); + static readonly Loading = vscode.l10n.t("Loading..."); + static readonly SignIntoAzure = vscode.l10n.t("Sign in to Azure..."); + static readonly WaitForAzureSignIn = vscode.l10n.t("Waiting for Azure sign-in..."); + static readonly SelectTenant = vscode.l10n.t("Select tenant..."); + static readonly ErrorAuthenticating = vscode.l10n.t("Error authenticating"); + static readonly ErrorLoadingSubscriptions = vscode.l10n.t("Error loading subscriptions"); + static readonly NoSubscriptionsFound = vscode.l10n.t("No subscriptions found"); + static readonly AzureAccount = vscode.l10n.t("Azure"); + static readonly CreateAzureAccount = vscode.l10n.t("Create an Azure Account..."); + static readonly CreateAzureStudentAccount = vscode.l10n.t("Create an Azure for Students Account..."); + static readonly WaitForSignIn = vscode.l10n.t("Waiting for sign-in"); + static readonly SelectATenant = vscode.l10n.t("Select a tenant"); + static readonly NoMSAuthSessionFound = vscode.l10n.t("No Microsoft authentication session found: {0}"); + static readonly CustomCloudChoiseNotConfigured = vscode.l10n.t("The custom cloud choice is not configured. Please configure the setting {0}.{1}."); + static readonly FailedToListGroup = vscode.l10n.t("Failed to list resources: {0}"); + static readonly NotSignInStatus = vscode.l10n.t("Not signed in {0}"); + static readonly NoTenantFound = vscode.l10n.t("No tenants found."); + static readonly NoAzureSessionFound = vscode.l10n.t("No Azure session found."); + static readonly FailedTo = vscode.l10n.t("Failed to retrieve Azure session: {0}"); +} diff --git a/src/utils/generalUtils.ts b/src/utils/generalUtils.ts new file mode 100644 index 0000000..127f3d5 --- /dev/null +++ b/src/utils/generalUtils.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export namespace GeneralUtils { + export async function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + export interface Succeeded { + readonly succeeded: true; + readonly result: T; + } + + export interface Failed { + readonly succeeded: false; + readonly error: string; + } + + export type Errorable = Succeeded | Failed; + + export function succeeded(e: Errorable): e is Succeeded { + return e.succeeded; + } + + export function failed(e: Errorable): e is Failed { + return !e.succeeded; + } + + export function errMap(e: Errorable, fn: (t: T) => U): Errorable { + if (failed(e)) { + return { succeeded: false, error: e.error }; + } + return { succeeded: true, result: fn(e.result) }; + } + + export function getErrorMessage(error: unknown) { + if (error instanceof Error) { + return error.message; + } + return String(error); + } + + export function bindAsync(e: Errorable, fn: (t: T) => Promise>): Promise> { + if (failed(e)) { + return Promise.resolve(e); + } + return fn(e.result); + } +} From c4758aefbbf525e3630ad15a99a0e53ce25f2af8 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Tue, 11 Feb 2025 15:54:24 +0800 Subject: [PATCH 02/10] feat: update the code --- package.json | 9 ++++++++- src/azure/azureLogin/subscriptions.ts | 4 ++-- src/commands/openUrl.ts | 18 ++++++++++++++++++ src/constants.ts | 6 +++++- src/debugger/apimDebug.ts | 5 +++++ src/explorer/AzureAccountTreeItem.ts | 10 ++++++---- src/explorer/SubscriptionsTreeItem.ts | 9 +-------- src/extension.ts | 16 ++++++++++++---- src/extensionVariables.ts | 4 ++-- src/utils/azureClientUtil.ts | 21 ++++++--------------- test/createService.test.ts | 8 ++++---- 11 files changed, 69 insertions(+), 41 deletions(-) create mode 100644 src/commands/openUrl.ts diff --git a/package.json b/package.json index 830da7c..5d23697 100644 --- a/package.json +++ b/package.json @@ -782,6 +782,13 @@ "type": "boolean", "default": false, "description": "%azureApiManagement.advancedPolicyAuthoringExperience%" + }, + "azureApiManagement.selectedSubscriptions": { + "type": "array", + "description": "Selected Azure subscriptions", + "items": { + "type": "string" + } } } } @@ -863,4 +870,4 @@ "overrides": { "fsevents": "~2.3.2" } -} +} \ No newline at end of file diff --git a/src/azure/azureLogin/subscriptions.ts b/src/azure/azureLogin/subscriptions.ts index 9b90e02..1b17481 100644 --- a/src/azure/azureLogin/subscriptions.ts +++ b/src/azure/azureLogin/subscriptions.ts @@ -15,7 +15,7 @@ export namespace AzureSubscriptionHelper { export function getFilteredSubscriptions(): SubscriptionFilter[] { try { - let values = vscode.workspace.getConfiguration("azure-api-center").get("selectedSubscriptions", []); + let values = vscode.workspace.getConfiguration("azureApiManagement").get("selectedSubscriptions", []); return values.map(asSubscriptionFilter).filter((v) => v !== null) as SubscriptionFilter[]; } catch (e) { return []; @@ -73,7 +73,7 @@ export namespace AzureSubscriptionHelper { if (filtersChanged) { await vscode.workspace - .getConfiguration("azure-api-center") + .getConfiguration("azureApiManagement") .update("selectedSubscriptions", values, vscode.ConfigurationTarget.Global, true); onFilteredSubscriptionsChangeEmitter.fire(); } diff --git a/src/commands/openUrl.ts b/src/commands/openUrl.ts new file mode 100644 index 0000000..f62648e --- /dev/null +++ b/src/commands/openUrl.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +import { AzExtTreeItem, IActionContext, openUrl } from '@microsoft/vscode-azext-utils'; +import { APIMAccount, AzureAccountCreateUrl } from "../constants"; + +export async function openUrlFromTreeNode(context: IActionContext, node?: AzExtTreeItem) { + context; + switch (node?.id) { + case APIMAccount.createAzureAccount: { + await openUrl(AzureAccountCreateUrl.createAzureAccountUrl); + break; + } + case APIMAccount.createAzureStudentAccount: { + await openUrl(AzureAccountCreateUrl.createAzureStudentUrl); + break; + } + } +} diff --git a/src/constants.ts b/src/constants.ts index ddae7db..1123d1a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -42,9 +42,13 @@ export enum GatewayKeyType { export const APIMAccount = { createAzureAccount: "Create an Azure Account...", createAzureStudentAccount: "Create an Azure for Students Account...", - } +export const AzureAccountCreateUrl = { + createAzureAccountUrl: "https://aka.ms/VSCodeCreateAzureAccount", + createAzureStudentUrl: "https://aka.ms/student-account" +}; + // constants for extractor export const templatesFolder = "templates"; diff --git a/src/debugger/apimDebug.ts b/src/debugger/apimDebug.ts index 1ca0943..edaf834 100644 --- a/src/debugger/apimDebug.ts +++ b/src/debugger/apimDebug.ts @@ -386,6 +386,11 @@ export class ApimDebugSession extends LoggingDebugSession { } const creds = azureAccount.filters.filter(filter => filter.subscription.subscriptionId === subscriptionId).map(filter => filter.session.credentials); return creds[0]; + // const session = await AzureAuth.getReadySessionProvider(); + // if(!session.succeeded) { + // throw new Error("ERROR!"); + // } + // return AzureAuth.getCredential(session.result); } private async getMasterSubscriptionKey(managementAddress: string, credential?: TokenCredentialsBase, managementAuth?: string) { diff --git a/src/explorer/AzureAccountTreeItem.ts b/src/explorer/AzureAccountTreeItem.ts index 3d591f5..daf75ec 100644 --- a/src/explorer/AzureAccountTreeItem.ts +++ b/src/explorer/AzureAccountTreeItem.ts @@ -56,6 +56,8 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { // no need to sort the array public compareChildrenImpl(item1: AzExtTreeItem, item2: AzExtTreeItem): number { + item1; + item2; return 0; } @@ -77,7 +79,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { return [ new GenericTreeItem(this, { label: UiStrings.SignIntoAzure, - commandId: "azure-api-center.signInToAzure", + commandId: "azureApiManagement.signInToAzure", contextValue: "azureCommand", id: "azureapicenterAccountSignIn", iconPath: new vscode.ThemeIcon("sign-in"), @@ -85,7 +87,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { }), new GenericTreeItem(this, { label: UiStrings.CreateAzureAccount, - commandId: "azure-api-center.openUrl", + commandId: "azureApiManagement.openUrl", contextValue: "azureCommand", id: APIMAccount.createAzureAccount, iconPath: new vscode.ThemeIcon("add"), @@ -93,7 +95,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { }), new GenericTreeItem(this, { label: UiStrings.CreateAzureStudentAccount, - commandId: "azure-api-center.openUrl", + commandId: "azureApiManagement.openUrl", contextValue: "azureCommand", id: APIMAccount.createAzureStudentAccount, iconPath: new vscode.ThemeIcon("mortar-board"), @@ -116,7 +118,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { return [ new GenericTreeItem(this, { label: UiStrings.SelectTenant, - commandId: "azure-api-center.selectTenant", + commandId: "azureApiManagement.selectTenant", contextValue: "azureCommand", id: "azureapicenterAccountSelectTenant", iconPath: new vscode.ThemeIcon("account"), diff --git a/src/explorer/SubscriptionsTreeItem.ts b/src/explorer/SubscriptionsTreeItem.ts index 4541a6f..6bb6578 100644 --- a/src/explorer/SubscriptionsTreeItem.ts +++ b/src/explorer/SubscriptionsTreeItem.ts @@ -4,19 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { SubscriptionContract } from "@azure/arm-apimanagement"; -import { AzExtTreeItem, AzExtParentTreeItem, ISubscriptionContext } from "@microsoft/vscode-azext-utils"; +import { AzExtTreeItem, AzExtParentTreeItem } from "@microsoft/vscode-azext-utils"; import { uiUtils } from "@microsoft/vscode-azext-azureutils"; import { treeUtils } from "../utils/treeUtils"; import { IServiceTreeRoot } from "./IServiceTreeRoot"; import { SubscriptionTreeItem } from "./SubscriptionTreeItem"; -export function createSubscriptionTreeItem( - parent: AzExtParentTreeItem, - subscription: ISubscriptionContext, -): AzExtTreeItem { - return new SubscriptionsTreeItem(parent, subscription); -} - export class SubscriptionsTreeItem extends AzExtParentTreeItem { public get iconPath(): { light: string, dark: string } { return treeUtils.getThemedIconPath('list'); diff --git a/src/extension.ts b/src/extension.ts index 27e493b..850f880 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -51,7 +51,7 @@ import { AuthorizationProvidersTreeItem } from './explorer/AuthorizationProvider import { AuthorizationProviderTreeItem } from './explorer/AuthorizationProviderTreeItem'; import { AuthorizationsTreeItem } from './explorer/AuthorizationsTreeItem'; import { AuthorizationTreeItem } from './explorer/AuthorizationTreeItem'; -import { AzureAccountTreeItem } from './explorer/AzureAccountTreeItem'; +import { createAzureAccountTreeItem } from './explorer/AzureAccountTreeItem'; import { ApiResourceEditor } from './explorer/editors/arm/ApiResourceEditor'; import { AuthorizationAccessPolicyResourceEditor } from './explorer/editors/arm/AuthorizationAccessPolicyResourceEditor'; import { AuthorizationProviderResourceEditor } from './explorer/editors/arm/AuthorizationProviderResourceEditor'; @@ -79,7 +79,9 @@ import { SubscriptionTreeItem } from './explorer/SubscriptionTreeItem'; import { ext } from './extensionVariables'; import { localize } from './localize'; import { registerAzureUtilsExtensionVariables } from '@microsoft/vscode-azext-azureutils'; - +import { AzureSessionProviderHelper } from "./azure/azureLogin/azureSessionProvider"; +import { AzureAccount } from "./azure/azureLogin/azureAccount"; +import { openUrlFromTreeNode } from './commands/openUrl'; // this method is called when your extension is activated // your extension is activated the very first time the command is executed // tslint:disable-next-line:typedef @@ -96,7 +98,10 @@ export async function activateInternal(context: vscode.ExtensionContext) { await callWithTelemetryAndErrorHandling('azureApiManagement.Activate', async (activateContext: IActionContext) => { activateContext.telemetry.properties.isActivationEvent = 'true'; - ext.azureAccountTreeItem = new AzureAccountTreeItem(); + // ext.azureAccountTreeItem = new AzureAccountTreeItem(); + AzureSessionProviderHelper.activateAzureSessionProvider(context); + const sessionProvider = AzureSessionProviderHelper.getSessionProvider(); + ext.azureAccountTreeItem = createAzureAccountTreeItem(sessionProvider); context.subscriptions.push(ext.azureAccountTreeItem); ext.tree = new AzExtTreeDataProvider(ext.azureAccountTreeItem, 'azureApiManagement.LoadMore'); @@ -115,8 +120,11 @@ export async function activateInternal(context: vscode.ExtensionContext) { } function registerCommands(tree: AzExtTreeDataProvider): void { + registerCommand('azureApiManagement.signInToAzure', async () => { await AzureAccount.signInToAzure(); }); + registerCommand('azureApiManagement.openUrl', async(context: IActionContext, node?: AzExtTreeItem) => { await openUrlFromTreeNode(context, node); }); registerCommand('azureApiManagement.Refresh', async (context: IActionContext, node?: AzExtTreeItem) => await tree.refresh(context, node)); // need to double check - registerCommand('azureApiManagement.selectSubscriptions', () => vscode.commands.executeCommand("azure-account.selectSubscriptions")); + registerCommand('azureApiManagement.selectSubscriptions', async() => { await AzureAccount.selectSubscriptions();}); + registerCommand('azureApiManagement.selectTenant', async() => { await AzureAccount.selectTenant();}); registerCommand('azureApiManagement.LoadMore', async (context: IActionContext, node: AzExtTreeItem) => await tree.loadMore(node, context)); // need to double check registerCommand('azureApiManagement.openInPortal', openInPortal); registerCommand('azureApiManagement.createService', createService); diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index aa5a698..9c77d0d 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionContext } from "vscode"; import { AzExtTreeDataProvider, IAzExtOutputChannel } from "@microsoft/vscode-azext-utils"; -import { AzureAccountTreeItem } from "./explorer/AzureAccountTreeItem"; +import { AzExtParentTreeItem } from "@microsoft/vscode-azext-utils"; /** * Namespace for common variables used throughout the extension. They must be initialized in the activate() method of extension.ts @@ -13,7 +13,7 @@ export namespace ext { export let context: ExtensionContext; export let tree: AzExtTreeDataProvider; export let outputChannel: IAzExtOutputChannel; - export let azureAccountTreeItem: AzureAccountTreeItem; + export let azureAccountTreeItem: AzExtParentTreeItem & { dispose(): unknown }; export const prefix: string = 'azureAPIM'; //export let reporter: ITelemetryContext; } diff --git a/src/utils/azureClientUtil.ts b/src/utils/azureClientUtil.ts index 8907751..3b54a3b 100644 --- a/src/utils/azureClientUtil.ts +++ b/src/utils/azureClientUtil.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { WebSiteManagementClient } from "@azure/arm-appservice"; -import * as vscode from 'vscode'; import { createAzureClient } from '@microsoft/vscode-azext-azureutils'; -import { localize } from "../localize"; import { AzExtTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; +import { AzureAccount } from "../azure/azureLogin/azureAccount"; +import { AzureSubscriptionHelper } from "../azure/azureLogin/subscriptions"; export namespace azureClientUtil { export function getClient(context: IActionContext, node: AzExtTreeItem): WebSiteManagementClient { @@ -15,18 +15,9 @@ export namespace azureClientUtil { // tslint:disable: no-unsafe-any export async function selectSubscription(context: IActionContext): Promise { - const azureAccountExtension = vscode.extensions.getExtension('ms-vscode.azure-account'); - // tslint:disable-next-line: no-non-null-assertion - const azureAccount = azureAccountExtension!.exports; - await azureAccount.waitForFilters(); - if (azureAccount.status !== 'LoggedIn') { - throw new Error(localize("", "Please Log in at first!")); - } - const subscriptions : {id: string, name: string}[] = azureAccount.filters.map(filter => {return {id: filter.subscription.subscriptionId, name: filter.subscription.displayName}; }); - const subscriptionId = await context.ui.showQuickPick(subscriptions.map((s) => { - const option = s.id.concat(' (', s.name, ')'); - return { label: option, subscriptionId: s.id}; - }), { canPickMany: false, placeHolder: localize("", "Please choose the Azure subscription")}); - return subscriptionId.subscriptionId; + context; + await AzureAccount.selectSubscriptions(); + let res = await AzureSubscriptionHelper.getFilteredSubscriptions(); + return res[0].subscriptionId; } } diff --git a/test/createService.test.ts b/test/createService.test.ts index 777ad48..99a2790 100644 --- a/test/createService.test.ts +++ b/test/createService.test.ts @@ -10,9 +10,9 @@ import { ResourceManagementClient } from '@azure/arm-resources'; import { Context, Suite } from 'mocha'; import * as vscode from 'vscode'; import { ISubscriptionContext, TestAzureAccount} from '@microsoft/vscode-azext-dev'; -import { AzExtTreeDataProvider, AzExtParentTreeItem } from '@microsoft/vscode-azext-utils'; +import { AzExtParentTreeItem } from '@microsoft/vscode-azext-utils'; //import { ApiManagementProvider, AzureTreeDataProvider, ext, getRandomHexString, TestAzureAccount, TestUserInput, treeUtils } from '../extension.bundle'; -import { AzureAccountTreeItem, ext, treeUtils } from '../extension.bundle'; +import { ext, treeUtils } from '../extension.bundle'; import { longRunningTestsEnabled } from './global.test'; import { HttpHeaders } from '@azure/ms-rest-js'; //import { runWithApimSetting } from './runWithSetting'; @@ -32,8 +32,8 @@ suite('Create Azure Resources', async function (this: Suite): Promise { this.timeout(120 * 1000); await testAccount.signIn(); - ext.azureAccountTreeItem = new AzureAccountTreeItem(testAccount); - ext.tree = new AzExtTreeDataProvider(ext.azureAccountTreeItem, 'azureApiManagement.LoadMore'); + // ext.azureAccountTreeItem = new AzureAccountTreeItem(testAccount); + // ext.tree = new AzExtTreeDataProvider(ext.azureAccountTreeItem, 'azureApiManagement.LoadMore'); const rootNode : AzExtParentTreeItem = await treeUtils.getRootNode(ext.tree); rootNode.subscription.userId = "vscodeapimtest@microsoft.com"; // userId doesnt exist for service principal. //apiManagementClient = getApiManagementClient(testAccount); From 52db70cebe508bb209cf0a9d0cd5c3c9786ab5a5 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Thu, 13 Feb 2025 12:43:12 +0800 Subject: [PATCH 03/10] test: update for the rule debugger --- src/debugger/apimDebug.ts | 35 ++++++++++++++++++---------------- src/debugger/policySource.ts | 8 ++++---- src/extension.ts | 2 +- src/utils/requestUtil.ts | 37 +++++++++++++++++++++++++++++++----- 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/src/debugger/apimDebug.ts b/src/debugger/apimDebug.ts index edaf834..51d372a 100644 --- a/src/debugger/apimDebug.ts +++ b/src/debugger/apimDebug.ts @@ -2,7 +2,7 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; +// import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; import * as request from 'request-promise-native'; import * as vscode from 'vscode'; import { Breakpoint, Handles, InitializedEvent, Logger, logger, LoggingDebugSession, OutputEvent, Scope, StackFrame, StoppedEvent, TerminatedEvent, Thread, ThreadEvent, Variable } from 'vscode-debugadapter'; @@ -17,7 +17,9 @@ import { DebuggerConnection, RequestContract } from './debuggerConnection'; import { PolicySource } from './policySource'; import { UiRequest } from './uiRequest'; import { UiThread } from './uiThread'; - +import { AzureAuth } from "../azure/azureLogin/azureAuth"; +import { GeneralUtils } from "../utils/generalUtils"; +import { TokenCredential } from "@azure/core-auth"; // tslint:disable: no-unsafe-any // tslint:disable: indent // tslint:disable: export-name @@ -377,23 +379,24 @@ export class ApimDebugSession extends LoggingDebugSession { } } - private async getAccountCredentials(subscriptionId: string): Promise { - const azureAccountExtension = vscode.extensions.getExtension('ms-vscode.azure-account'); - const azureAccount = azureAccountExtension!.exports; - await azureAccount.waitForFilters(); - if (azureAccount.status !== 'LoggedIn') { - throw new Error("ERROR!"); - } - const creds = azureAccount.filters.filter(filter => filter.subscription.subscriptionId === subscriptionId).map(filter => filter.session.credentials); - return creds[0]; - // const session = await AzureAuth.getReadySessionProvider(); - // if(!session.succeeded) { + private async getAccountCredentials(subscriptionId: string): Promise { + // const azureAccountExtension = vscode.extensions.getExtension('ms-vscode.azure-account'); + // const azureAccount = azureAccountExtension!.exports; + // await azureAccount.waitForFilters(); + // if (azureAccount.status !== 'LoggedIn') { // throw new Error("ERROR!"); // } - // return AzureAuth.getCredential(session.result); + // const creds = azureAccount.filters.filter(filter => filter.subscription.subscriptionId === subscriptionId).map(filter => filter.session.credentials); + // return creds[0]; + subscriptionId; + const session = await AzureAuth.getReadySessionProvider(); + if (GeneralUtils.failed(session)) { + throw new Error("ERROR!"); + } + return await AzureAuth.getCredential(session.result); } - private async getMasterSubscriptionKey(managementAddress: string, credential?: TokenCredentialsBase, managementAuth?: string) { + private async getMasterSubscriptionKey(managementAddress: string, credential?: TokenCredential, managementAuth?: string) { const resourceUrl = `${managementAddress}/subscriptions/master/listSecrets?api-version=${Constants.apimApiVersion}`; const authToken = managementAuth ? managementAuth : await getBearerToken(resourceUrl, "GET", credential!); const subscription: IMasterSubscriptionsSecrets = await request.post(resourceUrl, { @@ -414,7 +417,7 @@ export class ApimDebugSession extends LoggingDebugSession { return subscription.primaryKey; } - private async getAvailablePolicies(managementAddress: string, credential?: TokenCredentialsBase, managementAuth?: string) { + private async getAvailablePolicies(managementAddress: string, credential?: TokenCredential, managementAuth?: string) { const resourceUrl = `${managementAddress}/policyDescriptions?api-version=${Constants.apimApiVersion}`; const authToken = managementAuth ? managementAuth : await getBearerToken(resourceUrl, "GET", credential!); const policyDescriptions: IPaged = await request.get(resourceUrl, { diff --git a/src/debugger/policySource.ts b/src/debugger/policySource.ts index b5f291f..23f529e 100644 --- a/src/debugger/policySource.ts +++ b/src/debugger/policySource.ts @@ -1,8 +1,8 @@ /*--------------------------------------------------------- * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ - -import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; +import { TokenCredential } from "@azure/core-auth"; +// import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; import * as path from 'path'; import * as request from 'request-promise-native'; import { Source } from 'vscode-debugadapter'; @@ -25,11 +25,11 @@ export class PolicySource { private static NextSourceReference : number = 1; private managementAddress: string; - private credential: TokenCredentialsBase | undefined; + private credential: TokenCredential | undefined; private auth: string | undefined; private policies: { [key: string]: Policy } = {}; - constructor(managementAddress: string, credential?: TokenCredentialsBase, auth?: string) { + constructor(managementAddress: string, credential?: TokenCredential, auth?: string) { this.managementAddress = managementAddress; this.credential = credential; this.auth = auth; diff --git a/src/extension.ts b/src/extension.ts index 850f880..cc09af5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -122,7 +122,7 @@ export async function activateInternal(context: vscode.ExtensionContext) { function registerCommands(tree: AzExtTreeDataProvider): void { registerCommand('azureApiManagement.signInToAzure', async () => { await AzureAccount.signInToAzure(); }); registerCommand('azureApiManagement.openUrl', async(context: IActionContext, node?: AzExtTreeItem) => { await openUrlFromTreeNode(context, node); }); - registerCommand('azureApiManagement.Refresh', async (context: IActionContext, node?: AzExtTreeItem) => await tree.refresh(context, node)); // need to double check + registerCommand('azureApiManagement.Refresh', async (context: IActionContext) => await ext.azureAccountTreeItem.refresh(context)); // need to double check registerCommand('azureApiManagement.selectSubscriptions', async() => { await AzureAccount.selectSubscriptions();}); registerCommand('azureApiManagement.selectTenant', async() => { await AzureAccount.selectTenant();}); registerCommand('azureApiManagement.LoadMore', async (context: IActionContext, node: AzExtTreeItem) => await tree.loadMore(node, context)); // need to double check diff --git a/src/utils/requestUtil.ts b/src/utils/requestUtil.ts index 8137cc6..d2e4606 100644 --- a/src/utils/requestUtil.ts +++ b/src/utils/requestUtil.ts @@ -3,13 +3,15 @@ * Licensed under the MIT License. See License.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { HttpMethods, HttpOperationResponse, ParameterValue, ServiceClient, WebResource } from "@azure/ms-rest-js"; -import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; +import { HttpMethods, HttpOperationResponse, ParameterValue, ServiceClient, WebResource, Constants as MSRestConstants } from "@azure/ms-rest-js"; +// import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; +import { AccessToken, TokenCredential } from "@azure/core-auth"; import requestPromise from 'request-promise'; import { appendExtensionUserAgent } from '@microsoft/vscode-azext-utils'; import { AzExtServiceClientCredentials } from "@microsoft/vscode-azext-utils"; import { clientOptions } from "../azure/clientOptions"; - +import { Environment } from "@azure/ms-rest-azure-env"; +import { AzureAuth } from "../azure/azureLogin/azureAuth"; export type nRequest = WebResource & requestPromise.RequestPromiseOptions; // tslint:disable-next-line: no-any @@ -28,13 +30,13 @@ export async function sendRequest(httpReq: nRequest): Promise { } // tslint:disable: no-unsafe-any -export async function getBearerToken(url: string, method: HttpMethods, credentials: TokenCredentialsBase): Promise { +export async function getBearerToken(url: string, method: HttpMethods, credentials: TokenCredential): Promise { const requestOptions: WebResource = new WebResource(); requestOptions.headers.set("User-Agent", appendExtensionUserAgent()); requestOptions.url = url; requestOptions.method = method; try { - await credentials.signRequest(requestOptions); + await signRequest(credentials, requestOptions); } catch (err) { throw err; } @@ -47,3 +49,28 @@ export async function getBearerToken(url: string, method: HttpMethods, credentia return authToken; } } + +export function getDefaultMsalScopes(environment: Environment): string[] { + return [ + createMsalScope(environment.managementEndpointUrl) + ]; +} + +function createMsalScope(authority: string, scope: string = '.default'): string { + return authority.endsWith('/') ? + `${authority}${scope}` : + `${authority}/${scope}`; +} + +export async function signRequest(credential: TokenCredential, webResource: WebResource): Promise { + const tokenResponse: AccessToken| null = await credential.getToken(getDefaultMsalScopes(AzureAuth.getEnvironment())); + if(tokenResponse) { + // webResource.headers.set( + // , + // `${MSRestConstants.HeaderConstants.AUTHORIZATION_SCHEME} ${tokenResponse.token}` + // ); + webResource.headers[MSRestConstants.HeaderConstants.AUTHORIZATION]= `${MSRestConstants.HeaderConstants.AUTHORIZATION_SCHEME} ${tokenResponse.token}`; + return webResource; + } + return undefined; +} From ed3aacd5c92aee0ffc2413c81da6195b49a22dc5 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Thu, 13 Feb 2025 15:00:02 +0800 Subject: [PATCH 04/10] feat: update the code logic --- package-lock.json | 2 +- package.json | 4 ++-- src/extension.ts | 9 +++++---- src/extensionVariables.ts | 3 +-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1cf678..089364c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "vscode-apimanagement", - "version": "1.0.9", + "version": "1.0.10", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 5d23697..6f474ce 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "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", + "aiKey": "20f6d4af-3678-4602-ae98-f6724f057cd5", "engines": { "vscode": "^1.50.1" }, diff --git a/src/extension.ts b/src/extension.ts index cc09af5..7165372 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -101,10 +101,11 @@ export async function activateInternal(context: vscode.ExtensionContext) { // ext.azureAccountTreeItem = new AzureAccountTreeItem(); AzureSessionProviderHelper.activateAzureSessionProvider(context); const sessionProvider = AzureSessionProviderHelper.getSessionProvider(); - ext.azureAccountTreeItem = createAzureAccountTreeItem(sessionProvider); - context.subscriptions.push(ext.azureAccountTreeItem); - - ext.tree = new AzExtTreeDataProvider(ext.azureAccountTreeItem, 'azureApiManagement.LoadMore'); + const azureAccountTreeItem = createAzureAccountTreeItem(sessionProvider); + context.subscriptions.push(azureAccountTreeItem); + ext.azureAccountTreeItem = azureAccountTreeItem; + + ext.tree = new AzExtTreeDataProvider(azureAccountTreeItem, 'azureApiManagement.loadMore'); context.subscriptions.push(vscode.window.registerTreeDataProvider('azureApiManagementExplorer', ext.tree)); registerCommands(ext.tree); diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index 9c77d0d..fb29f8a 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ExtensionContext } from "vscode"; -import { AzExtTreeDataProvider, IAzExtOutputChannel } from "@microsoft/vscode-azext-utils"; -import { AzExtParentTreeItem } from "@microsoft/vscode-azext-utils"; +import { AzExtTreeDataProvider, IAzExtOutputChannel, AzExtParentTreeItem } from "@microsoft/vscode-azext-utils"; /** * Namespace for common variables used throughout the extension. They must be initialized in the activate() method of extension.ts From 5902ed1a4b8b32bc40a8c3db43110735982be2f0 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Thu, 13 Feb 2025 15:27:26 +0800 Subject: [PATCH 05/10] feat: update --- package.json | 10 ++++++++-- package.nls.json | 4 +++- src/explorer/AzureAccountTreeItem.ts | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 6f474ce..74b7e7a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/package.nls.json b/package.nls.json index 9edc71c..6b9c5b1 100644 --- a/package.nls.json +++ b/package.nls.json @@ -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..." } \ No newline at end of file diff --git a/src/explorer/AzureAccountTreeItem.ts b/src/explorer/AzureAccountTreeItem.ts index daf75ec..a047dbd 100644 --- a/src/explorer/AzureAccountTreeItem.ts +++ b/src/explorer/AzureAccountTreeItem.ts @@ -142,7 +142,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { ]; } - const subscriptions = await AzureSubscriptionHelper.getSubscriptions(this.sessionProvider, SelectionType.AllIfNoFilters); + const subscriptions = await AzureSubscriptionHelper.getSubscriptions(this.sessionProvider, SelectionType.Filtered); if (GeneralUtils.failed(subscriptions)) { return [ new GenericTreeItem(this, { From b374d185b1afb60b73b3131550eb5033505deaf4 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Thu, 13 Feb 2025 15:36:04 +0800 Subject: [PATCH 06/10] feat: update --- src/debugger/apimDebug.ts | 14 ++------------ src/debugger/policySource.ts | 1 - src/explorer/AzureAccountTreeItem.ts | 9 --------- src/extension.ts | 1 - src/utils/requestUtil.ts | 5 ----- 5 files changed, 2 insertions(+), 28 deletions(-) diff --git a/src/debugger/apimDebug.ts b/src/debugger/apimDebug.ts index 51d372a..7ed9c90 100644 --- a/src/debugger/apimDebug.ts +++ b/src/debugger/apimDebug.ts @@ -2,7 +2,6 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -// import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; import * as request from 'request-promise-native'; import * as vscode from 'vscode'; import { Breakpoint, Handles, InitializedEvent, Logger, logger, LoggingDebugSession, OutputEvent, Scope, StackFrame, StoppedEvent, TerminatedEvent, Thread, ThreadEvent, Variable } from 'vscode-debugadapter'; @@ -93,7 +92,7 @@ export class ApimDebugSession extends LoggingDebugSession { masterKey = await this.getMasterSubscriptionKey(args.managementAddress, undefined, args.managementAuth); this.availablePolicies = await this.getAvailablePolicies(args.managementAddress, undefined, args.managementAuth); } else { - const credential = await this.getAccountCredentials(args.subscriptionId); + const credential = await this.getAccountCredentials(); this.policySource = new PolicySource(args.managementAddress, credential); masterKey = await this.getMasterSubscriptionKey(args.managementAddress, credential); this.availablePolicies = await this.getAvailablePolicies(args.managementAddress, credential); @@ -379,16 +378,7 @@ export class ApimDebugSession extends LoggingDebugSession { } } - private async getAccountCredentials(subscriptionId: string): Promise { - // const azureAccountExtension = vscode.extensions.getExtension('ms-vscode.azure-account'); - // const azureAccount = azureAccountExtension!.exports; - // await azureAccount.waitForFilters(); - // if (azureAccount.status !== 'LoggedIn') { - // throw new Error("ERROR!"); - // } - // const creds = azureAccount.filters.filter(filter => filter.subscription.subscriptionId === subscriptionId).map(filter => filter.session.credentials); - // return creds[0]; - subscriptionId; + private async getAccountCredentials(): Promise { const session = await AzureAuth.getReadySessionProvider(); if (GeneralUtils.failed(session)) { throw new Error("ERROR!"); diff --git a/src/debugger/policySource.ts b/src/debugger/policySource.ts index 23f529e..17fd55b 100644 --- a/src/debugger/policySource.ts +++ b/src/debugger/policySource.ts @@ -2,7 +2,6 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ import { TokenCredential } from "@azure/core-auth"; -// import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; import * as path from 'path'; import * as request from 'request-promise-native'; import { Source } from 'vscode-debugadapter'; diff --git a/src/explorer/AzureAccountTreeItem.ts b/src/explorer/AzureAccountTreeItem.ts index a047dbd..e5c8278 100644 --- a/src/explorer/AzureAccountTreeItem.ts +++ b/src/explorer/AzureAccountTreeItem.ts @@ -12,15 +12,6 @@ import { AzureAuth } from "../azure/azureLogin/azureAuth"; import { APIMAccount } from "../constants"; import { Subscription } from "@azure/arm-resources-subscriptions"; import { createSubscriptionTreeItem } from "./ApiManagementProvider"; -// export class AzureAccountTreeItem extends AzureAccountTreeItemBase { -// public constructor(testAccount?: {}) { -// super(undefined, testAccount); -// } - -// public createSubscriptionTreeItem(root: ISubscriptionContext): ApiManagementProvider { -// return new ApiManagementProvider(this, root); -// } -// } export function createAzureAccountTreeItem( sessionProvider: AzureSessionProvider, diff --git a/src/extension.ts b/src/extension.ts index 7165372..5c0eebe 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -98,7 +98,6 @@ export async function activateInternal(context: vscode.ExtensionContext) { await callWithTelemetryAndErrorHandling('azureApiManagement.Activate', async (activateContext: IActionContext) => { activateContext.telemetry.properties.isActivationEvent = 'true'; - // ext.azureAccountTreeItem = new AzureAccountTreeItem(); AzureSessionProviderHelper.activateAzureSessionProvider(context); const sessionProvider = AzureSessionProviderHelper.getSessionProvider(); const azureAccountTreeItem = createAzureAccountTreeItem(sessionProvider); diff --git a/src/utils/requestUtil.ts b/src/utils/requestUtil.ts index d2e4606..9bdc959 100644 --- a/src/utils/requestUtil.ts +++ b/src/utils/requestUtil.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { HttpMethods, HttpOperationResponse, ParameterValue, ServiceClient, WebResource, Constants as MSRestConstants } from "@azure/ms-rest-js"; -// import { TokenCredentialsBase } from "@azure/ms-rest-nodeauth"; import { AccessToken, TokenCredential } from "@azure/core-auth"; import requestPromise from 'request-promise'; import { appendExtensionUserAgent } from '@microsoft/vscode-azext-utils'; @@ -65,10 +64,6 @@ function createMsalScope(authority: string, scope: string = '.default'): string export async function signRequest(credential: TokenCredential, webResource: WebResource): Promise { const tokenResponse: AccessToken| null = await credential.getToken(getDefaultMsalScopes(AzureAuth.getEnvironment())); if(tokenResponse) { - // webResource.headers.set( - // , - // `${MSRestConstants.HeaderConstants.AUTHORIZATION_SCHEME} ${tokenResponse.token}` - // ); webResource.headers[MSRestConstants.HeaderConstants.AUTHORIZATION]= `${MSRestConstants.HeaderConstants.AUTHORIZATION_SCHEME} ${tokenResponse.token}`; return webResource; } From b7c90f8b2519e8a6d36eb69371137d2eae571b28 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Thu, 13 Feb 2025 15:39:28 +0800 Subject: [PATCH 07/10] feat: update --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 74b7e7a..c97cd12 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "version": "1.0.10", "publisher": "ms-azuretools", "icon": "resources/apim-icon-newone.png", - "aiKey": "20f6d4af-3678-4602-ae98-f6724f057cd5", + "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "engines": { "vscode": "^1.50.1" }, From 6f4a1d261b61d1b2250b40fd7083c3b04423f69b Mon Sep 17 00:00:00 2001 From: wenyutang Date: Thu, 13 Feb 2025 16:43:43 +0800 Subject: [PATCH 08/10] feat: update --- package.json | 14 +++ src/azure/azureLogin/azureAccount.ts | 15 ++- src/azure/azureLogin/azureAuth.ts | 11 ++- src/azure/azureLogin/azureSessionProvider.ts | 12 ++- src/azure/azureLogin/constants.ts | 15 +++ src/azure/azureLogin/subscriptions.ts | 8 +- src/commands/openUrl.ts | 10 +- src/constants.ts | 12 ++- src/explorer/AzureAccountTreeItem.ts | 22 ++--- src/uiStrings.ts | 97 +++----------------- 10 files changed, 100 insertions(+), 116 deletions(-) create mode 100644 src/azure/azureLogin/constants.ts diff --git a/package.json b/package.json index c97cd12..572700b 100644 --- a/package.json +++ b/package.json @@ -795,6 +795,20 @@ "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" + } + } } } } diff --git a/src/azure/azureLogin/azureAccount.ts b/src/azure/azureLogin/azureAccount.ts index 97fefb2..dcb0caf 100644 --- a/src/azure/azureLogin/azureAccount.ts +++ b/src/azure/azureLogin/azureAccount.ts @@ -2,17 +2,27 @@ // 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 } from "vscode"; +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 { await AzureSessionProviderHelper.getSessionProvider().signIn(); } + + export function getSelectedTenant(): Tenant | undefined { + return workspace.getConfiguration(extensionPrefix).get(AzureLoginConstantString.selectedTenant); + } + + export async function updateSelectedTenant(value?: Tenant): Promise { + await workspace.getConfiguration(extensionPrefix).update(AzureLoginConstantString.selectedTenant, value, ConfigurationTarget.Global, true); + } export async function selectTenant(): Promise { const sessionProvider = AzureSessionProviderHelper.getSessionProvider(); @@ -42,6 +52,7 @@ export namespace AzureAccount { } sessionProvider.selectedTenant = selectedTenant; + await updateSelectedTenant(selectedTenant); } type SubscriptionQuickPickItem = QuickPickItem & { subscription: SubscriptionFilter }; @@ -64,7 +75,7 @@ export namespace AzureAccount { const setupAccount = UiStrings.SetUpAzureAccount; const response = await window.showInformationMessage(noSubscriptionsFound, setupAccount); if (response === setupAccount) { - env.openExternal(Uri.parse("https://azure.microsoft.com/")); + env.openExternal(Uri.parse(AzureAccountUrl.azureMicrosoftLink)); } return; diff --git a/src/azure/azureLogin/azureAuth.ts b/src/azure/azureLogin/azureAuth.ts index 7807634..3c2301b 100644 --- a/src/azure/azureLogin/azureAuth.ts +++ b/src/azure/azureLogin/azureAuth.ts @@ -8,6 +8,7 @@ import { UiStrings } from "../../uiStrings"; import { GeneralUtils } from "../../utils/generalUtils"; import { AzureSessionProvider, GetAuthSessionOptions, ReadyAzureSessionProvider, SignInStatus, Tenant } from "./authTypes"; import { AzureSessionProviderHelper } from "./azureSessionProvider"; +import { AzureEnvType, AzureLoginConstantString } from "./constants"; export namespace AzureAuth { export function getEnvironment(): Environment { return getConfiguredAzureEnv(); @@ -98,16 +99,16 @@ export namespace AzureAuth { // See: // https://github.com/microsoft/vscode/blob/eac16e9b63a11885b538db3e0b533a02a2fb8143/extensions/microsoft-authentication/package.json#L40-L99 const section = "microsoft-sovereign-cloud"; - const settingName = "environment"; + const settingName = AzureLoginConstantString.environment; const authProviderConfig = vscode.workspace.getConfiguration(section); const environmentSettingValue = authProviderConfig.get(settingName); - if (environmentSettingValue === "ChinaCloud") { + if (environmentSettingValue === AzureEnvType.ChinaCloud) { return Environment.ChinaCloud; - } else if (environmentSettingValue === "USGovernment") { + } else if (environmentSettingValue === AzureEnvType.USGovernment) { return Environment.USGovernment; - } else if (environmentSettingValue === "custom") { - const customCloud = authProviderConfig.get("customEnvironment"); + } else if (environmentSettingValue === AzureEnvType.custom) { + const customCloud = authProviderConfig.get(AzureEnvType.customEnvironment); if (customCloud) { return new Environment(customCloud); } diff --git a/src/azure/azureLogin/azureSessionProvider.ts b/src/azure/azureLogin/azureSessionProvider.ts index a37df6e..c501c4f 100644 --- a/src/azure/azureLogin/azureSessionProvider.ts +++ b/src/azure/azureLogin/azureSessionProvider.ts @@ -16,6 +16,7 @@ import { GeneralUtils } from "../../utils/generalUtils"; import { AzureAuthenticationSession, AzureSessionProvider, GetAuthSessionOptions, SignInStatus, Tenant } from "./authTypes"; import { AzureAccount } from "./azureAccount"; import { AzureAuth } from "./azureAuth"; +import { AzureLoginConstantString } from "./constants"; enum AuthScenario { Initialization, @@ -120,7 +121,7 @@ export namespace AzureSessionProviderHelper { // This allows the user to sign in to the Microsoft provider and list tenants, // but the resulting session will not allow tenant-level operations. For that, // we need to get a session for a specific tenant. - const orgTenantId = "organizations"; + const orgTenantId = AzureLoginConstantString.organizations; const scopes = AzureAuth.getScopes(orgTenantId, {}); const getSessionResult = await this.getArmSession(orgTenantId, scopes, authScenario); @@ -143,6 +144,9 @@ export namespace AzureSessionProviderHelper { this.tenants = newTenants; this.signInStatusValue = newSignInStatus; if (signInStatusChanged || tenantsChanged || selectedTenantChanged) { + if (newSignInStatus === SignInStatus.SignedOut) { + await AzureAccount.updateSelectedTenant(); + } this.onSignInStatusChangeEmitter.fire(this.signInStatusValue); } } @@ -220,7 +224,11 @@ export namespace AzureSessionProviderHelper { ); const results = await Promise.all(getSessionPromises); const accessibleTenants = results.filter(GeneralUtils.succeeded).map((r) => r.result); - return accessibleTenants.length === 1 ? AzureAccount.findTenant(tenants, accessibleTenants[0].tenantId) : null; + if (accessibleTenants.length === 1) { + return AzureAccount.findTenant(tenants, accessibleTenants[0].tenantId); + } + const lastTenant = AzureAccount.getSelectedTenant(); + return lastTenant && accessibleTenants.some(item => item.tenantId === lastTenant.id) ? lastTenant : null; } private async getArmSession( diff --git a/src/azure/azureLogin/constants.ts b/src/azure/azureLogin/constants.ts new file mode 100644 index 0000000..d0ff4d6 --- /dev/null +++ b/src/azure/azureLogin/constants.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +export const AzureLoginConstantString = { + organizations: "organizations", + environment: "environment", + selectedSubscriptions: "selectedSubscriptions", + selectedTenant: "selectedTenant", +} + +export const AzureEnvType = { + ChinaCloud: "ChinaCloud", + USGovernment: "USGovernment", + custom: "custom", + customEnvironment: "customEnvironment", +} \ No newline at end of file diff --git a/src/azure/azureLogin/subscriptions.ts b/src/azure/azureLogin/subscriptions.ts index 1b17481..561f0c3 100644 --- a/src/azure/azureLogin/subscriptions.ts +++ b/src/azure/azureLogin/subscriptions.ts @@ -5,6 +5,8 @@ import * as vscode from "vscode"; import { GeneralUtils } from "../../utils/generalUtils"; import { ReadyAzureSessionProvider, SelectionType, SubscriptionFilter } from "./authTypes"; import { AzureAuth } from "./azureAuth"; +import { extensionPrefix } from "../../constants"; +import { AzureLoginConstantString } from "./constants"; export namespace AzureSubscriptionHelper { const onFilteredSubscriptionsChangeEmitter = new vscode.EventEmitter(); @@ -15,7 +17,7 @@ export namespace AzureSubscriptionHelper { export function getFilteredSubscriptions(): SubscriptionFilter[] { try { - let values = vscode.workspace.getConfiguration("azureApiManagement").get("selectedSubscriptions", []); + let values = vscode.workspace.getConfiguration(extensionPrefix).get(AzureLoginConstantString.selectedSubscriptions, []); return values.map(asSubscriptionFilter).filter((v) => v !== null) as SubscriptionFilter[]; } catch (e) { return []; @@ -73,8 +75,8 @@ export namespace AzureSubscriptionHelper { if (filtersChanged) { await vscode.workspace - .getConfiguration("azureApiManagement") - .update("selectedSubscriptions", values, vscode.ConfigurationTarget.Global, true); + .getConfiguration(extensionPrefix) + .update(AzureLoginConstantString.selectedSubscriptions, values, vscode.ConfigurationTarget.Global, true); onFilteredSubscriptionsChangeEmitter.fire(); } } diff --git a/src/commands/openUrl.ts b/src/commands/openUrl.ts index f62648e..583cbdd 100644 --- a/src/commands/openUrl.ts +++ b/src/commands/openUrl.ts @@ -1,17 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { AzExtTreeItem, IActionContext, openUrl } from '@microsoft/vscode-azext-utils'; -import { APIMAccount, AzureAccountCreateUrl } from "../constants"; +import { APIMAccountCommandId, AzureAccountUrl } from "../constants"; export async function openUrlFromTreeNode(context: IActionContext, node?: AzExtTreeItem) { context; switch (node?.id) { - case APIMAccount.createAzureAccount: { - await openUrl(AzureAccountCreateUrl.createAzureAccountUrl); + case APIMAccountCommandId.createAzureAccount: { + await openUrl(AzureAccountUrl.createAzureAccountUrl); break; } - case APIMAccount.createAzureStudentAccount: { - await openUrl(AzureAccountCreateUrl.createAzureStudentUrl); + case APIMAccountCommandId.createAzureStudentAccount: { + await openUrl(AzureAccountUrl.createAzureStudentUrl); break; } } diff --git a/src/constants.ts b/src/constants.ts index 1123d1a..53bf912 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -39,14 +39,20 @@ export enum GatewayKeyType { secondary = "secondary" } -export const APIMAccount = { +export const APIMAccountCommandId = { createAzureAccount: "Create an Azure Account...", createAzureStudentAccount: "Create an Azure for Students Account...", + accountLoading: "azureManagementAccountLoading", + accountSignIn: "azureManagementAccountSignIn", + accountSigningIn: "azureManagementAccountSigningIn", + accountSelectTenant: "azureManagementAccountSelectTenant", + accountError: "AzureAccountError", } -export const AzureAccountCreateUrl = { +export const AzureAccountUrl = { createAzureAccountUrl: "https://aka.ms/VSCodeCreateAzureAccount", - createAzureStudentUrl: "https://aka.ms/student-account" + createAzureStudentUrl: "https://aka.ms/student-account", + azureMicrosoftLink: "https://azure.microsoft.com/", }; // constants for extractor diff --git a/src/explorer/AzureAccountTreeItem.ts b/src/explorer/AzureAccountTreeItem.ts index e5c8278..8e74c90 100644 --- a/src/explorer/AzureAccountTreeItem.ts +++ b/src/explorer/AzureAccountTreeItem.ts @@ -9,7 +9,7 @@ import { AzureSubscriptionHelper } from "../azure/azureLogin/subscriptions"; import { AzureSessionProvider, ReadyAzureSessionProvider, SelectionType, SignInStatus } from "../azure/azureLogin/authTypes"; import { GeneralUtils } from "../utils/generalUtils"; import { AzureAuth } from "../azure/azureLogin/azureAuth"; -import { APIMAccount } from "../constants"; +import { APIMAccountCommandId } from "../constants"; import { Subscription } from "@azure/arm-resources-subscriptions"; import { createSubscriptionTreeItem } from "./ApiManagementProvider"; @@ -21,7 +21,7 @@ export function createAzureAccountTreeItem( export class AzureAccountTreeItem extends AzExtParentTreeItem { private subscriptionTreeItems: AzExtTreeItem[] | undefined; - public static contextValue: string = "azureApiCenterAzureAccount"; + public static contextValue: string = "azureApiManagementAzureAccount"; public readonly contextValue: string = AzureAccountTreeItem.contextValue; constructor(private readonly sessionProvider: AzureSessionProvider) { super(undefined); @@ -62,7 +62,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { new GenericTreeItem(this, { label: UiStrings.Loading, contextValue: "azureCommand", - id: "azureapicenterAccountLoading", + id: APIMAccountCommandId.accountLoading, iconPath: new vscode.ThemeIcon("loading~spin"), }), ]; @@ -72,7 +72,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { label: UiStrings.SignIntoAzure, commandId: "azureApiManagement.signInToAzure", contextValue: "azureCommand", - id: "azureapicenterAccountSignIn", + id: APIMAccountCommandId.accountSignIn, iconPath: new vscode.ThemeIcon("sign-in"), includeInTreeItemPicker: true, }), @@ -80,7 +80,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { label: UiStrings.CreateAzureAccount, commandId: "azureApiManagement.openUrl", contextValue: "azureCommand", - id: APIMAccount.createAzureAccount, + id: APIMAccountCommandId.createAzureAccount, iconPath: new vscode.ThemeIcon("add"), includeInTreeItemPicker: true, }), @@ -88,7 +88,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { label: UiStrings.CreateAzureStudentAccount, commandId: "azureApiManagement.openUrl", contextValue: "azureCommand", - id: APIMAccount.createAzureStudentAccount, + id: APIMAccountCommandId.createAzureStudentAccount, iconPath: new vscode.ThemeIcon("mortar-board"), includeInTreeItemPicker: true, }) @@ -98,7 +98,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { new GenericTreeItem(this, { label: UiStrings.WaitForAzureSignIn, contextValue: "azureCommand", - id: "azureapicenterAccountSigningIn", + id: APIMAccountCommandId.accountSigningIn, iconPath: new vscode.ThemeIcon("loading~spin"), }), ]; @@ -111,7 +111,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { label: UiStrings.SelectTenant, commandId: "azureApiManagement.selectTenant", contextValue: "azureCommand", - id: "azureapicenterAccountSelectTenant", + id: APIMAccountCommandId.accountSelectTenant, iconPath: new vscode.ThemeIcon("account"), includeInTreeItemPicker: true, }), @@ -127,7 +127,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { new GenericTreeItem(this, { label: UiStrings.ErrorAuthenticating, contextValue: "azureCommand", - id: "AzureAccountError", + id: APIMAccountCommandId.accountError, iconPath: new vscode.ThemeIcon("error"), }), ]; @@ -139,7 +139,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { new GenericTreeItem(this, { label: UiStrings.ErrorLoadingSubscriptions, contextValue: "azureCommand", - id: "AzureAccountError", + id: APIMAccountCommandId.accountError, iconPath: new vscode.ThemeIcon("error"), description: subscriptions.error, }), @@ -151,7 +151,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { new GenericTreeItem(this, { label: UiStrings.NoSubscriptionsFound, contextValue: "azureCommand", - id: "AzureAccountError", + id: APIMAccountCommandId.accountError, iconPath: new vscode.ThemeIcon("info"), }), ]; diff --git a/src/uiStrings.ts b/src/uiStrings.ts index dfcaef2..046a9c7 100644 --- a/src/uiStrings.ts +++ b/src/uiStrings.ts @@ -3,97 +3,24 @@ import * as vscode from "vscode"; export class UiStrings { - static readonly ApiTitle = vscode.l10n.t("API Title"); - static readonly ApiType = vscode.l10n.t("API Type"); - static readonly ApiVersionTitle = vscode.l10n.t("API Version Title"); - static readonly ApiVersionLifecycle = vscode.l10n.t("API Version Lifecycle"); - static readonly ApiDefinitionTitle = vscode.l10n.t("API Definition Title"); - static readonly ApiSpecificationName = vscode.l10n.t("API Specification Name"); - static readonly SelectFile = vscode.l10n.t("Select File"); - static readonly SelectApiDefinitionFile = vscode.l10n.t("Select API Definition File To Import"); - static readonly Import = vscode.l10n.t("Import"); - static readonly RegisterApi = vscode.l10n.t("Register API"); - static readonly CreatingApi = vscode.l10n.t("Creating API..."); - static readonly CreatingApiVersion = vscode.l10n.t("Creating API Version..."); - static readonly CreatingApiVersionDefinition = vscode.l10n.t("Creating API Version Definition..."); - static readonly ImportingApiDefinition = vscode.l10n.t("Importing API Definition..."); - static readonly ApiIsRegistered = vscode.l10n.t("API is registered."); - static readonly FailedToRegisterApi = vscode.l10n.t("Failed to register API."); - static readonly ValueNotBeEmpty = vscode.l10n.t("The value should not be empty."); - static readonly ValueAtLeast2Char = vscode.l10n.t("The value should have at least 2 characters of numbers or letters."); - static readonly ValueStartWithAlphanumeric = vscode.l10n.t("The value should start with letter or number."); - static readonly OpenWorkspace = vscode.l10n.t("Open a workspace in Visual Studio Code to generate a CI/CD pipeline."); - static readonly SelectCiCdProvider = vscode.l10n.t("Select CI/CD Provider"); - static readonly NoKiotaExtension = vscode.l10n.t("Please install the Microsoft Kiota extension to generate an API library."); - static readonly NoRestClientExtension = vscode.l10n.t("Please install the REST Client extension to test APIs with a HTTP file."); - static readonly NoSpectralExtension = vscode.l10n.t("Please install the Spectral extension to lint APIs."); - static readonly SetApiStyleGuide = vscode.l10n.t("Set API Style Guide"); - static readonly SelectRulesetFile = vscode.l10n.t("Select Ruleset File"); - static readonly Ruleset = vscode.l10n.t("Ruleset"); - static readonly RemoteUrlRuleset = vscode.l10n.t('Remote URL of Ruleset File'); - static readonly ValidUrlStart = vscode.l10n.t('Please enter a valid URL.'); - static readonly ValidUrlType = vscode.l10n.t('Please enter a valid URL to a JSON, YAML, or JavaScript file.'); - static readonly RulesetFileSet = vscode.l10n.t("API Style Guide is set to '{0}'."); - static readonly CopilotNoCmd = vscode.l10n.t("Hi! What can I help you with? Please use `/list` or `/find` to chat with me!"); - static readonly CopilotQueryData = vscode.l10n.t("Querying data from Azure API Center..."); - static readonly CopilotNoMoreApiSpec = vscode.l10n.t("⚠️ There are no more API specification documents."); - static readonly CopilotParseApiSpec = vscode.l10n.t("Parsing API Specifications..."); - static readonly CopilotParseApiSpecFor = vscode.l10n.t("Parsing API Specifications for '{0}'..."); - static readonly CopilotExceedsTokenLimit = vscode.l10n.t("The contents of the current file are too large for GitHub Copilot. Please try again with a smaller file."); - static readonly RegisterApiOptionStepByStep = vscode.l10n.t("Manual"); - static readonly RegisterApiOptionCicd = vscode.l10n.t("CI/CD"); - static readonly ApiRulesetOptionDefault = vscode.l10n.t("Default"); - static readonly ApiRulesetOptionAzureApiGuideline = vscode.l10n.t("Microsoft Azure REST API"); - static readonly ApiRulesetOptionSpectralOwasp = vscode.l10n.t("OWASP API Security Top 10"); - static readonly ApiRulesetOptionSelectFile = vscode.l10n.t("Select Local File"); - static readonly ApiRulesetOptionInputUrl = vscode.l10n.t("Input Remote URL"); - static readonly CICDTypeGitHub = vscode.l10n.t("GitHub"); - static readonly CICDTypeAzure = vscode.l10n.t("Azure DevOps"); - static readonly ApiSpecificationOptionApiCenter = vscode.l10n.t("Azure API Center"); - static readonly ApiSpecificationOptionLocalFile = vscode.l10n.t("Local File"); - static readonly ApiSpecificationOptionActiveEditor = vscode.l10n.t("Active Editor"); - static readonly TreeitemLabelApis = vscode.l10n.t("APIs"); - static readonly TreeitemLabelDefinitions = vscode.l10n.t("Definitions"); - static readonly TreeitemLabelVersions = vscode.l10n.t("Versions"); - static readonly TreeitemLabelEnvironments = vscode.l10n.t("Environments"); - static readonly SubscriptionTreeItemChildTypeLabel = vscode.l10n.t("API Center Service"); - static readonly ApiCenterTreeItemTreeItemChildTypeLabel = vscode.l10n.t("APIs or Environments"); - static readonly ApisTreeItemChildTypeLabel = vscode.l10n.t("API"); - static readonly ApiTreeItemChildTypeLabel = vscode.l10n.t("Deployments or Versions"); - static readonly ApiVersionsChildTypeLabel = vscode.l10n.t("API Version"); - static readonly ApiVersionChildTypeLabel = vscode.l10n.t("Definitions"); - static readonly ApiVersionDefinitionsTreeItemChildTypeLabel = vscode.l10n.t("API Definition"); - static readonly NoFolderOpened = vscode.l10n.t("No folder is opened. Please open a folder to use this feature."); - static readonly NoNodeInstalled = vscode.l10n.t("Node.js is not installed. Please install Node.js to use this feature."); - static readonly SelectFirstApiSpecification = vscode.l10n.t("Select first API specification document"); - static readonly SelectSecondApiSpecification = vscode.l10n.t("Select second API specification document"); - static readonly SelectApiSpecification = vscode.l10n.t("Select API specification document"); - static readonly OpticTaskName = vscode.l10n.t("Breaking Change Detection"); - static readonly OpticTaskSource = vscode.l10n.t("Azure API Center"); - static readonly SearchAPI = vscode.l10n.t('Search API'); - static readonly SearchAPIsResult = vscode.l10n.t("Search Result for '{0}'"); - static readonly SearchContentHint = vscode.l10n.t("Search for API name, kind, lifecycle stage"); - static readonly AIContentIncorrect = vscode.l10n.t("AI-generated content may be incorrect"); - static readonly NoActiveFileOpen = vscode.l10n.t("'No active file is open."); - static readonly GeneratingOpenAPI = vscode.l10n.t("Generating OpenAPI Specification from Current File..."); - static readonly SelectTenantBeforeSignIn = vscode.l10n.t("You must sign in before selecting a tenant."); - static readonly NoTenantSelected = vscode.l10n.t("No tenant selected."); - static readonly NoSubscriptionsFoundAndSetup = vscode.l10n.t("No subscriptions were found. Set up your account if you have yet to do so."); - static readonly SetUpAzureAccount = vscode.l10n.t("Set up Account"); - static readonly SelectSubscription = vscode.l10n.t("Select Subscriptions"); + static readonly AzureAccount = vscode.l10n.t("Azure"); static readonly Loading = vscode.l10n.t("Loading..."); - static readonly SignIntoAzure = vscode.l10n.t("Sign in to Azure..."); + static readonly CreateAzureStudentAccount = vscode.l10n.t("Create an Azure for Students Account..."); + static readonly CreateAzureAccount = vscode.l10n.t("Create an Azure Account..."); static readonly WaitForAzureSignIn = vscode.l10n.t("Waiting for Azure sign-in..."); + static readonly SignIntoAzure = vscode.l10n.t("Sign in to Azure..."); static readonly SelectTenant = vscode.l10n.t("Select tenant..."); static readonly ErrorAuthenticating = vscode.l10n.t("Error authenticating"); - static readonly ErrorLoadingSubscriptions = vscode.l10n.t("Error loading subscriptions"); + static readonly SelectTenantBeforeSignIn = vscode.l10n.t("You must sign in before selecting a tenant."); + static readonly NoTenantSelected = vscode.l10n.t("No tenant selected."); + static readonly NoSubscriptionsFoundAndSetup = vscode.l10n.t("No subscriptions were found. Set up your account if you have yet to do so."); + static readonly SetUpAzureAccount = vscode.l10n.t("Set up Account"); static readonly NoSubscriptionsFound = vscode.l10n.t("No subscriptions found"); - static readonly AzureAccount = vscode.l10n.t("Azure"); - static readonly CreateAzureAccount = vscode.l10n.t("Create an Azure Account..."); - static readonly CreateAzureStudentAccount = vscode.l10n.t("Create an Azure for Students Account..."); - static readonly WaitForSignIn = vscode.l10n.t("Waiting for sign-in"); - static readonly SelectATenant = vscode.l10n.t("Select a tenant"); + static readonly ErrorLoadingSubscriptions = vscode.l10n.t("Error loading subscriptions"); + static readonly SelectSubscription = vscode.l10n.t("Select Subscriptions"); static readonly NoMSAuthSessionFound = vscode.l10n.t("No Microsoft authentication session found: {0}"); + static readonly SelectATenant = vscode.l10n.t("Select a tenant"); + static readonly WaitForSignIn = vscode.l10n.t("Waiting for sign-in"); static readonly CustomCloudChoiseNotConfigured = vscode.l10n.t("The custom cloud choice is not configured. Please configure the setting {0}.{1}."); static readonly FailedToListGroup = vscode.l10n.t("Failed to list resources: {0}"); static readonly NotSignInStatus = vscode.l10n.t("Not signed in {0}"); From 53592309b8f5e178e0c7bcf850baa84d2c74a719 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Fri, 14 Feb 2025 10:02:24 +0800 Subject: [PATCH 09/10] perf: update the code according to comments --- package.json | 8 ++++---- package.nls.json | 4 ++++ src/commands/openUrl.ts | 2 ++ src/debugger/apimDebug.ts | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 572700b..fa307b3 100644 --- a/package.json +++ b/package.json @@ -791,22 +791,22 @@ }, "azureApiManagement.selectedSubscriptions": { "type": "array", - "description": "Selected Azure subscriptions", + "description": "%azureApiManagement.selectAzureSubscriptions%", "items": { "type": "string" } }, "azureApiManagement.selectedTenant": { "type": "object", - "description": "A specific tenant to sign in to", + "description": "%azureApiManagement.selectAzureTenant%", "properties": { "name": { "type": "string", - "description": "tenant name" + "description": "%azureApiManagement.selectAzureTenant.tenantName%" }, "id": { "type": "string", - "description": "tenant id" + "description": "%azureApiManagement.selectAzureTenant.tenantId%" } } } diff --git a/package.nls.json b/package.nls.json index 6b9c5b1..fae3e79 100644 --- a/package.nls.json +++ b/package.nls.json @@ -27,6 +27,10 @@ "azureApiManagement.openExtensionWorkspaceFolder": "Open extension workspace folder", "azureApiManagement.initializeExtensionWorkspaceFolder": "Initialize extension workspace folder", "azureApiManagement.advancedPolicyAuthoringExperience": "(Experimental Feature) Enables advanced policy authoring experience.", + "azureApiManagement.selectAzureSubscriptions": "Selected Azure subscriptions", + "azureApiManagement.selectAzureTenant": "Selected Azure Tenant", + "azureApiManagement.selectAzureTenant.tenantName": "tenant name", + "azureApiManagement.selectAzureTenant.tennatId": "tenant id", "azureApiManagement.extractService": "Extract Service", "azureApiManagement.extractApi": "Extract API", "azureApiManagement.importFunctionApp": "Import from Azure Functions", diff --git a/src/commands/openUrl.ts b/src/commands/openUrl.ts index 583cbdd..756db93 100644 --- a/src/commands/openUrl.ts +++ b/src/commands/openUrl.ts @@ -14,5 +14,7 @@ export async function openUrlFromTreeNode(context: IActionContext, node?: AzExtT await openUrl(AzureAccountUrl.createAzureStudentUrl); break; } + default: + break; } } diff --git a/src/debugger/apimDebug.ts b/src/debugger/apimDebug.ts index 7ed9c90..bacbcf3 100644 --- a/src/debugger/apimDebug.ts +++ b/src/debugger/apimDebug.ts @@ -381,7 +381,7 @@ export class ApimDebugSession extends LoggingDebugSession { private async getAccountCredentials(): Promise { const session = await AzureAuth.getReadySessionProvider(); if (GeneralUtils.failed(session)) { - throw new Error("ERROR!"); + throw new Error("Failed to access the Azure Account Session."); } return await AzureAuth.getCredential(session.result); } From c6ce4a2d6e155e8896cd2ff900a5c9682dfbb279 Mon Sep 17 00:00:00 2001 From: wenyutang Date: Fri, 14 Feb 2025 14:20:33 +0800 Subject: [PATCH 10/10] perf: update the code according to the comments --- src/constants.ts | 14 +++++++------- src/explorer/ApiManagementProvider.ts | 7 ------- src/explorer/AzureAccountTreeItem.ts | 14 +++----------- src/extension.ts | 6 +++--- src/extensionVariables.ts | 4 ++-- src/utils/azureClientUtil.ts | 3 +-- test/createService.test.ts | 2 -- 7 files changed, 16 insertions(+), 34 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 53bf912..744aeef 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -40,13 +40,13 @@ export enum GatewayKeyType { } export const APIMAccountCommandId = { - createAzureAccount: "Create an Azure Account...", - createAzureStudentAccount: "Create an Azure for Students Account...", - accountLoading: "azureManagementAccountLoading", - accountSignIn: "azureManagementAccountSignIn", - accountSigningIn: "azureManagementAccountSigningIn", - accountSelectTenant: "azureManagementAccountSelectTenant", - accountError: "AzureAccountError", + createAzureAccount: "azureApiManagementCreateAzureAccount", + createAzureStudentAccount: "azureApiManagementCreateAzureStudentAccount", + accountLoading: "azureApiManagementAccountLoading", + accountSignIn: "azureApiManagementAccountSignIn", + accountSigningIn: "azureApiManagementAccountSigningIn", + accountSelectTenant: "azureApiManagementAccountSelectTenant", + accountError: "azureApiManagementAccountError", } export const AzureAccountUrl = { diff --git a/src/explorer/ApiManagementProvider.ts b/src/explorer/ApiManagementProvider.ts index 02c960a..850a1f3 100644 --- a/src/explorer/ApiManagementProvider.ts +++ b/src/explorer/ApiManagementProvider.ts @@ -18,13 +18,6 @@ import { getWorkspaceSetting, updateGlobalSetting } from '../vsCodeConfig/settin import { ServiceTreeItem } from './ServiceTreeItem'; import { treeUtils } from '../utils/treeUtils'; -export function createSubscriptionTreeItem( - parent: AzExtParentTreeItem, - subscription: ISubscriptionContext, -): AzExtTreeItem { - return new ApiManagementProvider(parent, subscription); -} - export class ApiManagementProvider extends SubscriptionTreeItemBase { public readonly childTypeLabel: string = localize('azureApiManagement.ApimService', 'API Management Service'); diff --git a/src/explorer/AzureAccountTreeItem.ts b/src/explorer/AzureAccountTreeItem.ts index 8e74c90..983bf39 100644 --- a/src/explorer/AzureAccountTreeItem.ts +++ b/src/explorer/AzureAccountTreeItem.ts @@ -11,13 +11,7 @@ import { GeneralUtils } from "../utils/generalUtils"; import { AzureAuth } from "../azure/azureLogin/azureAuth"; import { APIMAccountCommandId } from "../constants"; import { Subscription } from "@azure/arm-resources-subscriptions"; -import { createSubscriptionTreeItem } from "./ApiManagementProvider"; - -export function createAzureAccountTreeItem( - sessionProvider: AzureSessionProvider, -): AzExtParentTreeItem & { dispose(): unknown } { - return new AzureAccountTreeItem(sessionProvider); -} +import { ApiManagementProvider } from "./ApiManagementProvider"; export class AzureAccountTreeItem extends AzExtParentTreeItem { private subscriptionTreeItems: AzExtTreeItem[] | undefined; @@ -46,9 +40,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { } // no need to sort the array - public compareChildrenImpl(item1: AzExtTreeItem, item2: AzExtTreeItem): number { - item1; - item2; + public compareChildrenImpl(_item1: AzExtTreeItem, _item2: AzExtTreeItem): number { return 0; } @@ -173,7 +165,7 @@ export class AzureAccountTreeItem extends AzExtParentTreeItem { session.result, subscription, ); - return await createSubscriptionTreeItem(this, subscriptionContext); + return new ApiManagementProvider(this, subscriptionContext); } }), ); diff --git a/src/extension.ts b/src/extension.ts index 5c0eebe..3e10e6d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -51,7 +51,7 @@ import { AuthorizationProvidersTreeItem } from './explorer/AuthorizationProvider import { AuthorizationProviderTreeItem } from './explorer/AuthorizationProviderTreeItem'; import { AuthorizationsTreeItem } from './explorer/AuthorizationsTreeItem'; import { AuthorizationTreeItem } from './explorer/AuthorizationTreeItem'; -import { createAzureAccountTreeItem } from './explorer/AzureAccountTreeItem'; +import { AzureAccountTreeItem } from './explorer/AzureAccountTreeItem'; import { ApiResourceEditor } from './explorer/editors/arm/ApiResourceEditor'; import { AuthorizationAccessPolicyResourceEditor } from './explorer/editors/arm/AuthorizationAccessPolicyResourceEditor'; import { AuthorizationProviderResourceEditor } from './explorer/editors/arm/AuthorizationProviderResourceEditor'; @@ -100,11 +100,11 @@ export async function activateInternal(context: vscode.ExtensionContext) { activateContext.telemetry.properties.isActivationEvent = 'true'; AzureSessionProviderHelper.activateAzureSessionProvider(context); const sessionProvider = AzureSessionProviderHelper.getSessionProvider(); - const azureAccountTreeItem = createAzureAccountTreeItem(sessionProvider); + const azureAccountTreeItem = new AzureAccountTreeItem(sessionProvider); context.subscriptions.push(azureAccountTreeItem); ext.azureAccountTreeItem = azureAccountTreeItem; - ext.tree = new AzExtTreeDataProvider(azureAccountTreeItem, 'azureApiManagement.loadMore'); + ext.tree = new AzExtTreeDataProvider(azureAccountTreeItem, 'azureApiManagement.LoadMore'); context.subscriptions.push(vscode.window.registerTreeDataProvider('azureApiManagementExplorer', ext.tree)); registerCommands(ext.tree); diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index fb29f8a..db7c7a3 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext } from "vscode"; +import { ExtensionContext, Disposable } from "vscode"; import { AzExtTreeDataProvider, IAzExtOutputChannel, AzExtParentTreeItem } from "@microsoft/vscode-azext-utils"; /** @@ -12,7 +12,7 @@ export namespace ext { export let context: ExtensionContext; export let tree: AzExtTreeDataProvider; export let outputChannel: IAzExtOutputChannel; - export let azureAccountTreeItem: AzExtParentTreeItem & { dispose(): unknown }; + export let azureAccountTreeItem: AzExtParentTreeItem & Disposable; export const prefix: string = 'azureAPIM'; //export let reporter: ITelemetryContext; } diff --git a/src/utils/azureClientUtil.ts b/src/utils/azureClientUtil.ts index 3b54a3b..0b7b231 100644 --- a/src/utils/azureClientUtil.ts +++ b/src/utils/azureClientUtil.ts @@ -14,8 +14,7 @@ export namespace azureClientUtil { } // tslint:disable: no-unsafe-any - export async function selectSubscription(context: IActionContext): Promise { - context; + export async function selectSubscription(_context: IActionContext): Promise { await AzureAccount.selectSubscriptions(); let res = await AzureSubscriptionHelper.getFilteredSubscriptions(); return res[0].subscriptionId; diff --git a/test/createService.test.ts b/test/createService.test.ts index 99a2790..a6707c7 100644 --- a/test/createService.test.ts +++ b/test/createService.test.ts @@ -32,8 +32,6 @@ suite('Create Azure Resources', async function (this: Suite): Promise { this.timeout(120 * 1000); await testAccount.signIn(); - // ext.azureAccountTreeItem = new AzureAccountTreeItem(testAccount); - // ext.tree = new AzExtTreeDataProvider(ext.azureAccountTreeItem, 'azureApiManagement.LoadMore'); const rootNode : AzExtParentTreeItem = await treeUtils.getRootNode(ext.tree); rootNode.subscription.userId = "vscodeapimtest@microsoft.com"; // userId doesnt exist for service principal. //apiManagementClient = getApiManagementClient(testAccount);