Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
df0e2f8
Moving toward auth pkg v6
bwateratmsft Oct 27, 2025
5e5315d
Merge branch 'main' into bmw/authV6
bwateratmsft Oct 27, 2025
f347d1f
Engine 106
bwateratmsft Oct 27, 2025
113e82d
Fix mock subscription provider
bwateratmsft Oct 27, 2025
b0a6354
Fix selectSubscriptions
bwateratmsft Oct 27, 2025
8bf992b
Few more fixes
bwateratmsft Oct 27, 2025
3b8e457
Fix focus view TDP
bwateratmsft Oct 27, 2025
b6b606b
Fix one more thing
bwateratmsft Oct 27, 2025
e86152c
Minor changes
bwateratmsft Oct 27, 2025
bc99d99
Commit what I have
bwateratmsft Oct 27, 2025
a314e97
Fixes and refactors
bwateratmsft Oct 28, 2025
4359423
Minimally fix cloud console
bwateratmsft Oct 28, 2025
4ace0db
Build err
bwateratmsft Oct 28, 2025
1c4120d
Delay sending telemetry so we can use cached data
bwateratmsft Oct 28, 2025
66e8086
Dependency updates
bwateratmsft Oct 30, 2025
298fa2f
Ahyup
bwateratmsft Oct 30, 2025
9e3baef
Oh boy...
bwateratmsft Oct 30, 2025
f65146a
Merge branch 'main' into bmw/authV6
bwateratmsft Nov 17, 2025
fe75e7b
Go back to main
bwateratmsft Nov 17, 2025
634048b
Redo after merges
bwateratmsft Nov 17, 2025
f46258c
Should be nodenext
bwateratmsft Nov 17, 2025
10b2b76
We need node 22
bwateratmsft Nov 17, 2025
632635e
Use event
bwateratmsft Nov 17, 2025
e4621f0
Tiny updates
bwateratmsft Nov 17, 2025
e01696e
Fixes
bwateratmsft Nov 17, 2025
6f56d3e
Merge branch 'main' into bmw/authV6
bwateratmsft Nov 20, 2025
0e76bad
Fixes
bwateratmsft Nov 20, 2025
d841bce
Fix super busted lockfile
bwateratmsft Nov 20, 2025
18f6608
Merge branch 'main' into bmw/authV6
bwateratmsft Nov 20, 2025
3c99c9a
Merge branch 'main' into bmw/authV6
bwateratmsft Dec 2, 2025
73f1933
Merge branch 'main' into bmw/authV6
bwateratmsft Dec 4, 2025
1339576
Deps update
bwateratmsft Dec 4, 2025
b1dab72
Impl manual refresh clearing cache
bwateratmsft Dec 4, 2025
60e15bc
Fix lockfile conflict
bwateratmsft Dec 4, 2025
1c82366
wip
alexweininger Jan 29, 2026
43df097
Merge remote-tracking branch 'origin/main' into alex/authv6
alexweininger Jan 29, 2026
009bc1e
Fix
alexweininger Jan 29, 2026
ee1602c
Refactor cache management: replace boolean flag with dedicated functi…
alexweininger Jan 29, 2026
378e484
Remove temp md file
alexweininger Jan 29, 2026
83e0dc8
Only load AzDO provider when testing
alexweininger Jan 29, 2026
1413885
Fixup
alexweininger Jan 29, 2026
e851624
Fixup
alexweininger Jan 29, 2026
b63a4fb
Merge remote-tracking branch 'origin/main' into alex/authv6
alexweininger Feb 3, 2026
f05db86
Optimize tenant authentication check by using a Set for unauthenticat…
alexweininger Feb 3, 2026
6ba9aa5
Copilot PR feedback
alexweininger Feb 3, 2026
a19973e
Merge remote-tracking branch 'origin/main' into alex/authv6
alexweininger Feb 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
377 changes: 235 additions & 142 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"icon": "resources/resourceGroup.png",
"aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255",
"engines": {
"vscode": "^1.105.0"
"vscode": "^1.106.0"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -926,7 +926,7 @@
"dependencies": {
"@azure/arm-resources": "^5.2.0",
"@azure/arm-resources-profile-2020-09-01-hybrid": "^2.1.0",
"@microsoft/vscode-azext-azureauth": "^5.1.1",
"@microsoft/vscode-azext-azureauth": "^6.0.0-alpha.1",
"@microsoft/vscode-azext-azureutils": "^4.0.0",
"@microsoft/vscode-azext-utils": "^4.0.4",
"form-data": "^4.0.4",
Expand Down
33 changes: 17 additions & 16 deletions src/cloudConsole/cloudConsole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { TenantIdDescription } from '@azure/arm-resources-subscriptions';
import { AzureSubscriptionProvider, getConfiguredAzureEnv } from '@microsoft/vscode-azext-azureauth';
import { AzureSubscriptionProvider, AzureTenant, getConfiguredAzureEnv } from '@microsoft/vscode-azext-azureauth';
import { IActionContext, IAzureQuickPickItem, IParsedError, callWithTelemetryAndErrorHandlingSync, nonNullProp, parseError } from '@microsoft/vscode-azext-utils';
import * as cp from 'child_process';
import { default as FormData } from 'form-data';
Expand Down Expand Up @@ -262,20 +262,18 @@ export function createCloudConsole(subscriptionProvider: AzureSubscriptionProvid
res.end();
});

if (!await subscriptionProvider.isSignedIn()) {
serverQueue.push({ type: 'log', args: [localize('loggingIn', "Signing in...")] });
try {
if (await subscriptionProvider.signIn()) {
serverQueue.push({ type: 'log', args: [localize('loggingIn', "Signed in successful.")] });
}
} catch (e) {
serverQueue.push({ type: 'log', args: [localize('loggingIn', parseError(e).message)] });
// We used to delay for a second then exit here, but then the user can't read or copy the error message
// await delay(1000);
// serverQueue.push({ type: 'exit' });
updateStatus('Disconnected');
return;
serverQueue.push({ type: 'log', args: [localize('loggingIn', "Signing in...")] });
try {
if (await subscriptionProvider.signIn()) {
serverQueue.push({ type: 'log', args: [localize('loggingIn', "Signed in successful.")] });
Comment thread
bwateratmsft marked this conversation as resolved.
}
} catch (e) {
serverQueue.push({ type: 'log', args: [localize('loggingIn', parseError(e).message)] });
// We used to delay for a second then exit here, but then the user can't read or copy the error message
// await delay(1000);
// serverQueue.push({ type: 'exit' });
updateStatus('Disconnected');
return;
}

const env: TerminalOptions['env'] = {
Expand Down Expand Up @@ -336,10 +334,13 @@ export function createCloudConsole(subscriptionProvider: AzureSubscriptionProvid

liveServerQueue = serverQueue;

const tenants = await subscriptionProvider.getTenants();
const tenants: AzureTenant[] = [];
for (const account of await subscriptionProvider.getAccounts({ filter: false })) {
tenants.push(...await subscriptionProvider.getTenantsForAccount(account, { filter: false }));
}
let selectedTenant: TenantIdDescription | undefined = undefined;

const subscriptions = await subscriptionProvider.getSubscriptions(false);
const subscriptions = await subscriptionProvider.getAvailableSubscriptions({ filter: false });
if (tenants.length <= 1) {
serverQueue.push({ type: 'log', args: [localize('foundOneTenant', `Found 1 tenant.`)] });
// if they have only one tenant, use it
Expand Down
2 changes: 2 additions & 0 deletions src/commands/accounts/logIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export async function logIn(_context: IActionContext): Promise<void> {
await provider.signIn();
} finally {
_isLoggingIn = false;
// Clear cache to ensure fresh data is fetched after sign-in
ext.setClearCacheOnNextLoad();
ext.actions.refreshAzureTree(); // Refresh now that sign in is complete
ext.actions.refreshTenantTree(); // Refresh now that sign in is complete
}
Expand Down
28 changes: 16 additions & 12 deletions src/commands/accounts/selectSubscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureSubscription } from "@microsoft/vscode-azext-azureauth";
import { AzureSubscription, isNotSignedInError } from "@microsoft/vscode-azext-azureauth";
import { IActionContext, IAzureQuickPickItem, nonNullValue } from "@microsoft/vscode-azext-utils";
import * as vscode from "vscode";
import { ext } from "../../extensionVariables";
Expand All @@ -28,16 +28,16 @@ export interface SelectSubscriptionOptions {

export async function selectSubscriptions(context: IActionContext, options?: SelectSubscriptionOptions): Promise<void> {
const provider = await ext.subscriptionProviderFactory();
if (await provider.isSignedIn()) {

try {
const selectedSubscriptionsWithFullId = await getSelectedTenantAndSubscriptionIds();
const selectedSubscriptionIds = selectedSubscriptionsWithFullId.map(id => id.split('/')[1]);

let subscriptionsShownInPicker: string[] = [];

const subscriptionQuickPickItems: () => Promise<IAzureQuickPickItem<AzureSubscription>[]> = async () => {
// If there are no tenants selected by default all subscriptions will be shown.
let subscriptions = await provider.getSubscriptions(false);
// Use cached subscriptions if available - the cache is cleared on sign-in/sign-out
// This avoids unnecessary network requests when the user opens the subscription picker
let subscriptions = await provider.getAvailableSubscriptions({ filter: false });

if (options?.account || options?.tenantId) {

Expand Down Expand Up @@ -89,13 +89,17 @@ export async function selectSubscriptions(context: IActionContext, options?: Sel
}

ext.actions.refreshAzureTree();
} else {
const signIn: vscode.MessageItem = { title: localize('signIn', 'Sign In') };
void vscode.window.showInformationMessage(localize('notSignedIn', 'You are not signed in. Sign in to continue.'), signIn).then((input) => {
if (input === signIn) {
void provider.signIn();
}
});
} catch (error) {
if (isNotSignedInError(error)) {
const signIn: vscode.MessageItem = { title: localize('signIn', 'Sign In') };
void vscode.window.showInformationMessage(localize('notSignedIn', 'You are not signed in. Sign in to continue.'), signIn).then((input) => {
if (input === signIn) {
void provider.signIn();
}
});
} else {
throw error;
}
}
}

Expand Down
28 changes: 22 additions & 6 deletions src/commands/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,19 @@ export function registerCommands(): void {
registerCommand('azureResourceGroups.uploadFileCloudConsole', uploadFileToCloudShell);

// Special-case refresh that ignores the selected/focused node and always refreshes the entire tree. Used by the refresh button in the tree title.
registerCommand('azureResourceGroups.refreshTree', () => ext.actions.refreshAzureTree());
registerCommand('azureResourceGroups.refreshTree', () => {
ext.setClearCacheOnNextLoad();
ext.actions.refreshAzureTree();
});
registerCommand('azureWorkspace.refreshTree', () => ext.actions.refreshWorkspaceTree());
registerCommand('azureFocusView.refreshTree', () => ext.actions.refreshFocusTree());
registerCommand('azureTenantsView.refreshTree', () => ext.actions.refreshTenantTree());
registerCommand('azureFocusView.refreshTree', () => {
ext.setClearCacheOnNextLoad();
ext.actions.refreshFocusTree();
});
registerCommand('azureTenantsView.refreshTree', () => {
ext.setClearCacheOnNextLoad();
ext.actions.refreshTenantTree();
});

// v1.5 client extensions attach these commands to tree item context menus for refreshing their tree items
registerCommand('azureResourceGroups.refresh', async (context, node?: ResourceGroupsItem) => {
Expand Down Expand Up @@ -78,8 +87,10 @@ export function registerCommands(): void {
});

registerCommand('azureTenantsView.signInToTenant', async (_context, node: TenantTreeItem) => {
await (await ext.subscriptionProviderFactory()).signIn(node.tenantId, node.account);
ext.actions.refreshTenantTree(node);
await (await ext.subscriptionProviderFactory()).signIn(node);
ext.setClearCacheOnNextLoad();
ext.actions.refreshTenantTree();
ext.actions.refreshAzureTree();
});

registerCommand('azureResourceGroups.focusGroup', focusGroup);
Expand All @@ -88,7 +99,12 @@ export function registerCommands(): void {
registerCommand('azureResourceGroups.logIn', (context: IActionContext) => logIn(context));
registerCommand('azureTenantsView.addAccount', (context: IActionContext) => logIn(context));
registerCommand('azureResourceGroups.selectSubscriptions', (context: IActionContext, options: SelectSubscriptionOptions) => selectSubscriptions(context, options));
registerCommand('azureResourceGroups.signInToTenant', async () => signInToTenant(await ext.subscriptionProviderFactory()));
registerCommand('azureResourceGroups.signInToTenant', async () => {
await signInToTenant(await ext.subscriptionProviderFactory());
ext.setClearCacheOnNextLoad();
ext.actions.refreshTenantTree();
ext.actions.refreshAzureTree();
});

registerCommand('azureResourceGroups.createResourceGroup', createResourceGroup);
registerCommand('azureResourceGroups.deleteResourceGroupV2', deleteResourceGroupV2);
Expand Down
4 changes: 3 additions & 1 deletion src/commands/sovereignCloud/configureSovereignCloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export async function configureSovereignCloud(context: ConfigureSovereignCloudCo
await wizard.prompt();
await wizard.execute();

// refresh resources and tenant view to accurrately reflect information for the selected sovereign cloud
// Clear cache and refresh views to reflect the selected sovereign cloud
// This ensures accounts from the previous environment are not shown
ext.setClearCacheOnNextLoad();
ext.actions.refreshAzureTree();
ext.actions.refreshTenantTree();
}
4 changes: 2 additions & 2 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export async function activate(context: vscode.ExtensionContext, perfStats: { lo
activateContext.telemetry.measurements.mainFileLoad = (perfStats.loadEndTime - perfStats.loadStartTime) / 1000;


ext.subscriptionProviderFactory = getSubscriptionProviderFactory(activateContext);
ext.subscriptionProviderFactory = getSubscriptionProviderFactory();

ext.tagFS = new TagFileSystem(ext.appResourceTree);
context.subscriptions.push(vscode.workspace.registerFileSystemProvider(TagFileSystem.scheme, ext.tagFS));
Expand Down Expand Up @@ -213,7 +213,7 @@ export async function activate(context: vscode.ExtensionContext, perfStats: { lo
ext.workspaceTree = new CompatibleAzExtTreeDataProvider(workspaceResourceTreeDataProvider);

const getSubscriptions: (filter: boolean) => Promise<AzureSubscription[]> =
async (filter: boolean) => { return await (await ext.subscriptionProviderFactory()).getSubscriptions(filter); };
async (filter: boolean) => { return await (await ext.subscriptionProviderFactory()).getAvailableSubscriptions({ filter }); };

const coreApiFactories: AzureExtensionApiFactory[] = [
{
Expand Down
24 changes: 24 additions & 0 deletions src/extensionVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ export namespace ext {
export let subscriptionProviderFactory: () => Promise<AzureSubscriptionProvider>;
export let managedIdentityBranchDataProvider: ManagedIdentityBranchDataProvider;

/**
* Cache invalidation flag. When set to true, the next call to `consumeClearCacheFlag()`
* will return true and atomically reset the flag to false. This prevents race conditions
* where multiple trees might read and reset the flag independently.
*/
let clearCacheOnNextLoadFlag: boolean = false;

/**
* Sets the flag to clear auth caches on the next load.
*/
export function setClearCacheOnNextLoad(): void {
clearCacheOnNextLoadFlag = true;
}

/**
* Atomically consumes the clear cache flag. Returns true if caches should be cleared,
* and resets the flag to false. This ensures only the first consumer gets `true`.
*/
export function consumeClearCacheFlag(): boolean {
const shouldClear = clearCacheOnNextLoadFlag;
clearCacheOnNextLoadFlag = false;
return shouldClear;
}

// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace v2 {
export let api: AzureResourcesApiInternal;
Expand Down
2 changes: 1 addition & 1 deletion src/managedIdentity/TargetServiceRoleAssignmentItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class TargetServiceRoleAssignmentItem implements TreeElementBase {

if (this._loadedAllSubscriptions) {
// filter out this sub since it's already loaded
const subscriptions = (await (await ext.subscriptionProviderFactory()).getSubscriptions(false)).filter(s => s.subscriptionId !== this.subscription.subscriptionId);
const subscriptions = (await (await ext.subscriptionProviderFactory()).getAvailableSubscriptions({ filter: false })).filter(s => s.subscriptionId !== this.subscription.subscriptionId);
await Promise.allSettled(subscriptions.map(async (subscription) => {
children.push(...await createRoleDefinitionsItems(context, subscription, this.msi, this.subscription.subscriptionId));
}));
Expand Down
42 changes: 6 additions & 36 deletions src/services/getSubscriptionProviderFactory.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureDevOpsSubscriptionProviderInitializer, AzureSubscriptionProvider, createAzureDevOpsSubscriptionProviderFactory } from "@microsoft/vscode-azext-azureauth";
import { IActionContext } from "@microsoft/vscode-azext-utils";
import type { AzureSubscriptionProvider } from "@microsoft/vscode-azext-azureauth";
import { createVSCodeAzureSubscriptionProviderFactory } from "./VSCodeAzureSubscriptionProvider";

/**
* Returns a factory function that creates a subscription provider, satisfying the `AzureSubscriptionProvider` interface.
*
* If the `useAzureSubscriptionProvider` is set to `true`, an `AzureDevOpsSubscriptionProviderFactory` is returned.
* Otherwise, a `VSCodeSubscriptionProviderFactory` is returned.
*
* For nightly tests that require Azure DevOps federated credentials, use the test API to set
* `ext.testing.overrideAzureSubscriptionProvider` with an AzDO provider factory instead.
*/
export function getSubscriptionProviderFactory(activateContext?: IActionContext): () => Promise<AzureSubscriptionProvider> {
// if this for a nightly test, we want to use the test subscription provider
const useAzureFederatedCredentials: boolean = !/^(false|0)?$/i.test(process.env['AzCode_UseAzureFederatedCredentials'] || '');
if (useAzureFederatedCredentials) {
// when running tests, ensure we throw the errors and they aren't silently swallowed
if (activateContext) {
activateContext.errorHandling.rethrow = useAzureFederatedCredentials;
}

const serviceConnectionId: string | undefined = process.env['AzCode_ServiceConnectionID'];
const domain: string | undefined = process.env['AzCode_ServiceConnectionDomain'];
const clientId: string | undefined = process.env['AzCode_ServiceConnectionClientID'];

if (!serviceConnectionId || !domain || !clientId) {
throw new Error(`Using Azure DevOps federated credentials, but federated service connection is not configured\n
process.env.AzCode_ServiceConnectionID: ${serviceConnectionId ? "✅" : "❌"}\n
process.env.AzCode_ServiceConnectionDomain: ${domain ? "✅" : "❌"}\n
process.env.AzCode_ServiceConnectionClientID: ${clientId ? "✅" : "❌"}\n
`);
}

const initializer: AzureDevOpsSubscriptionProviderInitializer = {
serviceConnectionId,
domain,
clientId,
};
return createAzureDevOpsSubscriptionProviderFactory(initializer);
} else {
return createVSCodeAzureSubscriptionProviderFactory();
}
export function getSubscriptionProviderFactory(): () => Promise<AzureSubscriptionProvider> {
return createVSCodeAzureSubscriptionProviderFactory();
}
Loading