Skip to content
Open
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,12 @@
{
"title": "Azure",
"properties": {
"azure.tenant": {
"type": "string",
"description": "%azure.tenant%",
"default": "",
"scope": "machine"
},
"azureResourceGroups.enableOutputTimestamps": {
"type": "boolean",
"description": "%azureResourceGroups.enableOutputTimestamps%",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"azureResourceGroups.selectSubscriptions": "Select Subscriptions...",
"azureResourceGroups.createResourceGroup": "Create Resource Group...",
"azureResourceGroups.createResourceGroupDetail": "The logical grouping for your resources.",
"azure.tenant": "The Microsoft Entra tenant ID or domain to use when signing in to Azure.",
"azureResourceGroups.deleteResourceGroup": "Delete Resource Group...",
"azureResourceGroups.editTags": "Edit Tags...",
"azureResourceGroups.description": "An extension for viewing and managing Azure resources.",
Expand Down
5 changes: 4 additions & 1 deletion src/commands/accounts/logIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { IActionContext } from '@microsoft/vscode-azext-utils';
import type { SignInOptions } from '@microsoft/vscode-azext-azureauth';
import { ext } from '../../extensionVariables';
import { getConfiguredAzureTenant } from '../../utils/azureTenantSetting';

// eslint-disable-next-line @typescript-eslint/naming-convention
let _isLoggingIn: boolean = false;
Expand All @@ -16,7 +17,9 @@ export async function logIn(_context: IActionContext, options?: SignInOptions):
_isLoggingIn = true;
ext.actions.refreshAzureTree(); // Refresh to cause the "logging in" spinner to show
ext.actions.refreshTenantTree(); // Refresh to cause the "logging in" spinner to show
await provider.signIn(undefined, options);
const tenantId = getConfiguredAzureTenant();
const tenant = tenantId ? { tenantId } as Parameters<typeof provider.signIn>[0] : undefined;
await provider.signIn(tenant, options);
} finally {
_isLoggingIn = false;
// Clear cache to ensure fresh data is fetched after sign-in
Expand Down
18 changes: 4 additions & 14 deletions src/exportAuthRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as path from 'path';
import type { ExtensionContext } from 'vscode';
import * as vscode from 'vscode';
import { ext } from './extensionVariables';
import { addConfiguredTenantScope, getConfiguredAzureTenant } from './utils/azureTenantSetting';
import { inCloudShell } from './utils/inCloudShell';

/**
Expand Down Expand Up @@ -174,7 +175,7 @@ export function getAuthAccountStateManager(): AuthAccountStateManager {
*/
export async function exportAuthRecord(context: IActionContext, evt?: vscode.AuthenticationSessionsChangeEvent): Promise<void> {
const AUTH_PROVIDER_ID = 'microsoft'; // VS Code Azure auth provider
const SCOPES = ['https://management.azure.com/.default']; // Default ARM scope
const SCOPES = addConfiguredTenantScope(['https://management.azure.com/.default']); // Default ARM scope, optionally tenant-scoped

context.errorHandling.suppressDisplay = true;
context.telemetry.suppressIfSuccessful = true;
Expand Down Expand Up @@ -263,19 +264,8 @@ export async function exportAuthRecord(context: IActionContext, evt?: vscode.Aut
}

// Helper to get tenantId from session or config override
function getTenantId(session: unknown, context?: IActionContext): string | undefined {
let tenantFromArg: string | undefined = undefined;
try {
// This handles the case if an error is thrown, if the configuration is not registered by any extension
tenantFromArg = vscode.workspace.getConfiguration().get<string>('@azure.argTenant');
} catch {
// If the configuration is not found, ignore and proceed
ext.outputChannel.appendLine('No @azure.argTenant configuration found. Proceeding without tenant override.');
}
if (tenantFromArg) {
return tenantFromArg;
}
return extractTenantIdFromIdToken(session, context);
export function getTenantId(session: unknown, context?: IActionContext): string | undefined {
return extractTenantIdFromIdToken(session, context) ?? getConfiguredAzureTenant();
}

// Helper to extract tenantId (tid) from the idToken of a VS Code authentication session (For MS Auth, this will be present).
Expand Down
40 changes: 29 additions & 11 deletions src/services/VSCodeAzureSubscriptionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,30 @@
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { AzureAccount, AzureTenant, GetTenantsForAccountOptions, VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth';
import { getSelectedTenantAndSubscriptionIds } from '../commands/accounts/selectSubscriptions';
import { ext } from '../extensionVariables';
import { isTenantFilteredOut } from '../utils/tenantSelection';
import { AzureAccount, AzureTenant, GetTenantsForAccountOptions, SignInOptions, TenantIdAndAccount, VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth';
import { getSelectedTenantAndSubscriptionIds } from '../commands/accounts/selectSubscriptions';
import { ext } from '../extensionVariables';
import { getTenantIdForAuthentication } from '../utils/azureTenantSetting';
import { isTenantFilteredOut } from '../utils/tenantSelection';

/**
* Extends {@link VSCodeAzureSubscriptionProvider} to additionally filter tenants based on the
* Accounts & Tenants view checkbox state. Without this, unselected tenants still count toward
* the maximum tenants limit, preventing selected tenants from being processed.
*/
class ResourceGroupsSubscriptionProvider extends VSCodeAzureSubscriptionProvider {
public override async getTenantsForAccount(account: AzureAccount, options?: GetTenantsForAccountOptions): Promise<AzureTenant[]> {
const tenants = await super.getTenantsForAccount(account, options);
class ResourceGroupsSubscriptionProvider extends VSCodeAzureSubscriptionProvider {
public override async signIn(tenant?: TenantIdAndAccount, options?: SignInOptions): Promise<boolean> {
if (tenant?.tenantId) {
return super.signIn(tenant, options);
}

const tenantId = getTenantIdForAuthentication(undefined);
const tenantForSignIn = tenantId ? { ...tenant, tenantId } as TenantIdAndAccount : tenant;
return super.signIn(tenantForSignIn, options);
}

public override async getTenantsForAccount(account: AzureAccount, options?: GetTenantsForAccountOptions): Promise<AzureTenant[]> {
const tenants = await super.getTenantsForAccount(account, options);

// When filtering is enabled, also exclude tenants unchecked in the Accounts & Tenants view.
// The base class only filters by selectedSubscriptions config; the Tenants view stores its
Expand All @@ -28,10 +39,17 @@ class ResourceGroupsSubscriptionProvider extends VSCodeAzureSubscriptionProvider
}
return filtered;
}

return tenants;
}
}

return tenants;
}

protected override getSubscriptionContext(tenant: Partial<TenantIdAndAccount>) {
return super.getSubscriptionContext({
...tenant,
tenantId: getTenantIdForAuthentication(tenant.tenantId),
});
}
}

let vscodeAzureSubscriptionProvider: VSCodeAzureSubscriptionProvider | undefined;

Expand Down
30 changes: 30 additions & 0 deletions src/utils/azureTenantSetting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';

export const azureTenantSettingKey = 'azure.tenant';

export function normalizeConfiguredTenant(tenant: string | undefined): string | undefined {
const trimmedTenant = tenant?.trim();
return trimmedTenant ? trimmedTenant : undefined;
}

export function getConfiguredAzureTenant(): string | undefined {
return normalizeConfiguredTenant(vscode.workspace.getConfiguration().get<string>(azureTenantSettingKey));
}

export function addConfiguredTenantScope(scopes: readonly string[], tenant: string | undefined = getConfiguredAzureTenant()): string[] {
const normalizedTenant = normalizeConfiguredTenant(tenant);
if (!normalizedTenant) {
return [...scopes];
}

return Array.from(new Set([...scopes, `VSCODE_TENANT:${normalizedTenant}`]));
}

export function getTenantIdForAuthentication(tenantId: string | undefined, configuredTenant: string | undefined = getConfiguredAzureTenant()): string | undefined {
return normalizeConfiguredTenant(tenantId) ?? normalizeConfiguredTenant(configuredTenant);
}
104 changes: 104 additions & 0 deletions test/azureTenantSetting.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import assert from 'assert';
import type { AzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth';
import * as vscode from 'vscode';
import { logIn } from '../src/commands/accounts/logIn';
import { ext } from '../src/extensionVariables';
import { getTenantId } from '../src/exportAuthRecord';
import { addConfiguredTenantScope, getTenantIdForAuthentication, normalizeConfiguredTenant } from '../src/utils/azureTenantSetting';

suite('azureTenantSetting', () => {
test('normalizeConfiguredTenant returns undefined for blank tenants', () => {
assert.strictEqual(normalizeConfiguredTenant(undefined), undefined);
assert.strictEqual(normalizeConfiguredTenant(''), undefined);
assert.strictEqual(normalizeConfiguredTenant(' '), undefined);
});

test('normalizeConfiguredTenant trims configured tenants', () => {
assert.strictEqual(normalizeConfiguredTenant(' contoso.onmicrosoft.com '), 'contoso.onmicrosoft.com');
});

test('addConfiguredTenantScope appends VSCODE_TENANT scope when tenant is configured', () => {
assert.deepStrictEqual(
addConfiguredTenantScope(['https://management.azure.com/.default'], 'contoso.onmicrosoft.com'),
['https://management.azure.com/.default', 'VSCODE_TENANT:contoso.onmicrosoft.com']
);
});

test('addConfiguredTenantScope leaves scopes unchanged when tenant is not configured', () => {
assert.deepStrictEqual(
addConfiguredTenantScope(['https://management.azure.com/.default'], ' '),
['https://management.azure.com/.default']
);
});

test('addConfiguredTenantScope does not duplicate tenant scope', () => {
assert.deepStrictEqual(
addConfiguredTenantScope(
['https://management.azure.com/.default', 'VSCODE_TENANT:contoso.onmicrosoft.com'],
'contoso.onmicrosoft.com'
),
['https://management.azure.com/.default', 'VSCODE_TENANT:contoso.onmicrosoft.com']
);
});

test('getTenantIdForAuthentication prefers explicit tenant over configured tenant', () => {
assert.strictEqual(getTenantIdForAuthentication('subscription-tenant', 'configured-tenant'), 'subscription-tenant');
});

test('getTenantIdForAuthentication falls back to configured tenant', () => {
assert.strictEqual(getTenantIdForAuthentication(undefined, 'configured-tenant'), 'configured-tenant');
});

test('getTenantId prefers token tid over configured tenant domain', async () => {
const tenantId = '11111111-2222-3333-4444-555555555555';
const tokenPayload = Buffer.from(JSON.stringify({ tid: tenantId })).toString('base64');
const session = { idToken: `header.${tokenPayload}.signature` };

try {
await vscode.workspace.getConfiguration().update('azure.tenant', 'contoso.onmicrosoft.com', vscode.ConfigurationTarget.Global);

assert.strictEqual(getTenantId(session), tenantId);
} finally {
await vscode.workspace.getConfiguration().update('azure.tenant', undefined, vscode.ConfigurationTarget.Global);
}
});

test('logIn passes configured azure.tenant to subscription provider signIn', async () => {
const originalSubscriptionProviderFactory = ext.subscriptionProviderFactory;
const originalRefreshAzureTree = ext.actions.refreshAzureTree;
const originalRefreshTenantTree = ext.actions.refreshTenantTree;
const originalRefreshFocusTree = ext.actions.refreshFocusTree;

let signedInTenantId: string | undefined;
const provider = {
signIn: async (tenant?: { tenantId: string }): Promise<boolean> => {
signedInTenantId = tenant?.tenantId;
return true;
},
} as AzureSubscriptionProvider;

try {
ext.subscriptionProviderFactory = async () => provider;
ext.actions.refreshAzureTree = () => { /* no-op */ };
ext.actions.refreshTenantTree = () => { /* no-op */ };
ext.actions.refreshFocusTree = () => { /* no-op */ };

await vscode.workspace.getConfiguration().update('azure.tenant', 'contoso.onmicrosoft.com', vscode.ConfigurationTarget.Global);

await logIn({} as never);

assert.strictEqual(signedInTenantId, 'contoso.onmicrosoft.com');
} finally {
await vscode.workspace.getConfiguration().update('azure.tenant', undefined, vscode.ConfigurationTarget.Global);
ext.subscriptionProviderFactory = originalSubscriptionProviderFactory;
ext.actions.refreshAzureTree = originalRefreshAzureTree;
ext.actions.refreshTenantTree = originalRefreshTenantTree;
ext.actions.refreshFocusTree = originalRefreshFocusTree;
}
});
});