Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/azure/MssqlVSCodeAzureSubscriptionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ export class MssqlVSCodeAzureSubscriptionProvider extends VSCodeAzureSubscriptio
}

protected override async getTenantFilters(): Promise<TenantId[]> {
return this.getSelectedSubscriptions().map((id) => id.split("/")[0]);
// Format is now "account/tenantId/subscriptionId", extract tenantId (index 1)
return this.getSelectedSubscriptions().map((id) => id.split("/")[1]);
}

protected override async getSubscriptionFilters(): Promise<SubscriptionId[]> {
return this.getSelectedSubscriptions().map((id) => id.split("/")[1]);
// Format is now "account/tenantId/subscriptionId", extract subscriptionId (index 2)
return this.getSelectedSubscriptions().map((id) => id.split("/")[2]);
}
}
24 changes: 16 additions & 8 deletions src/connectionconfig/azureHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,18 @@ export async function promptForAzureSubscriptionFilter(
return false;
}

await vscode.workspace.getConfiguration().update(
configSelectedAzureSubscriptions,
selectedSubs.map((s) => `${s.tenantId}/${s.subscriptionId}`),
vscode.ConfigurationTarget.Global,
const filterConfig = selectedSubs.map(
(s) => `${s.group}/${s.tenantId}/${s.subscriptionId}`,
);

await vscode.workspace
.getConfiguration()
.update(
configSelectedAzureSubscriptions,
filterConfig,
vscode.ConfigurationTarget.Global,
);

return true;
} catch (error) {
state.formMessage = { message: l10n.t("Error loading Azure subscriptions.") };
Expand All @@ -287,19 +293,21 @@ export async function getSubscriptionQuickPickItems(
false /* don't use the current filter, 'cause we're gonna set it */,
);

// Get previously selected subscriptions as "account/tenantId/subscriptionId" strings
const prevSelectedSubs = vscode.workspace
.getConfiguration()
.get<string[] | undefined>(configSelectedAzureSubscriptions)
?.map((entry) => entry.split("/")[1]);
.get<string[] | undefined>(configSelectedAzureSubscriptions);

const quickPickItems: SubscriptionPickItem[] = allSubs
.map((sub) => {
const compositeKey = `${sub.account.label}/${sub.tenantId}/${sub.subscriptionId}`;
const isPicked = prevSelectedSubs ? prevSelectedSubs.includes(compositeKey) : true;
return {
label: sub.name,
description: sub.subscriptionId,
description: `${sub.subscriptionId} (${sub.account.label})`,
tenantId: sub.tenantId,
subscriptionId: sub.subscriptionId,
picked: prevSelectedSubs ? prevSelectedSubs.includes(sub.subscriptionId) : true,
picked: isPicked,
group: sub.account.label,
};
})
Expand Down
93 changes: 89 additions & 4 deletions src/connectionconfig/connectionDialogWebviewController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,17 @@ export class ConnectionDialogWebviewController extends FormWebviewController<
});

this.registerReducer("loadAzureServers", async (state, payload) => {
await this.loadAzureServersForSubscription(state, payload.subscriptionId);
// Find the subscription in state to get its tenantId
const subscription = state.azureSubscriptions.find(
(s) => s.id === payload.subscriptionId,
);
if (subscription) {
await this.loadAzureServersForSubscription(
state,
subscription.tenantId,
subscription.id,
);
}

return state;
});
Expand Down Expand Up @@ -1486,6 +1496,11 @@ export class ConnectionDialogWebviewController extends FormWebviewController<
state.loadingAzureSubscriptionsStatus = ApiStatus.Loading;
this.updateState();

// Step 3: Ensure we have valid tokens for all tenants in all accounts
// This is critical for discovering new subscriptions that were added after initial sign-in
this.logger.verbose("Refreshing authentication tokens for all accounts and tenants...");
await this.refreshAzureTokensForAllAccounts(auth, state.azureAccounts);

// getSubscriptions() below checks this config setting if filtering is specified. If the user has this set, then we use it; if not, we get all subscriptions.
// The specific vscode config setting it uses is hardcoded into the VS Code Azure SDK, so we need to use the same value here.
const shouldUseFilter =
Expand All @@ -1498,8 +1513,13 @@ export class ConnectionDialogWebviewController extends FormWebviewController<
TelemetryActions.LoadAzureSubscriptions,
);

// Store subscriptions with composite key "tenantId/subscriptionId" to handle cases where
// the same subscription is accessible from multiple tenants/accounts
this._azureSubscriptions = new Map(
(await auth.getSubscriptions(shouldUseFilter)).map((s) => [s.subscriptionId, s]),
(await auth.getSubscriptions(shouldUseFilter)).map((s) => [
`${s.tenantId}/${s.subscriptionId}`,
s,
]),
);
const tenantSubMap = Map.groupBy<string, AzureSubscription>(
Array.from(this._azureSubscriptions.values()),
Expand All @@ -1513,6 +1533,7 @@ export class ConnectionDialogWebviewController extends FormWebviewController<
subs.push({
id: s.subscriptionId,
name: s.name,
tenantId: s.tenantId,
loaded: false,
});
}
Expand All @@ -1526,6 +1547,7 @@ export class ConnectionDialogWebviewController extends FormWebviewController<
undefined, // additionalProperties
{
subscriptionCount: subs.length,
tenantCount: tenantSubMap.size,
},
);
this.updateState();
Expand All @@ -1540,6 +1562,63 @@ export class ConnectionDialogWebviewController extends FormWebviewController<
}
}

/**
* Refreshes authentication tokens for all accounts and their tenants.
* This ensures VS Code has valid tokens to discover subscriptions from all tenants,
* including newly added ones.
*/
private async refreshAzureTokensForAllAccounts(
auth: MssqlVSCodeAzureSubscriptionProvider,
accounts: IAzureAccount[],
): Promise<void> {
for (const account of accounts) {
try {
// Get the full account info
const accountInfo = await VsCodeAzureHelper.getAccountById(account.id);
if (!accountInfo) {
this.logger.verbose(`Could not find account info for ${account.name}`);
continue;
}

// Get all tenants for this account
const tenants = await auth.getTenants(accountInfo);
this.logger.verbose(
`Found ${tenants.length} tenant(s) for account ${account.name}`,
);

// Attempt to get a token for each tenant
// This ensures VS Code has authentication sessions for all tenants
for (const tenant of tenants) {
try {
// Check if already signed in to this tenant
const isSignedIn = await auth.isSignedIn(tenant.tenantId, accountInfo);

if (!isSignedIn) {
this.logger.verbose(
`Signing in to tenant ${tenant.displayName} (${tenant.tenantId}) for account ${account.name}`,
);
// This will prompt user if needed, but only for tenants without tokens
await auth.signIn(tenant.tenantId, accountInfo);
} else {
this.logger.verbose(
`Already signed in to tenant ${tenant.displayName} (${tenant.tenantId})`,
);
}
} catch (error) {
// Log but don't fail - some tenants might not be accessible or user might cancel
this.logger.verbose(
`Could not sign in to tenant ${tenant.displayName} (${tenant.tenantId}): ${getErrorMessage(error)}`,
);
}
}
} catch (error) {
this.logger.error(
`Error refreshing tokens for account ${account.name}: ${getErrorMessage(error)}`,
);
}
}
}

private async loadAllAzureServers(state: ConnectionDialogWebviewState): Promise<void> {
const endActivity = startActivity(
TelemetryViews.ConnectionDialog,
Expand All @@ -1566,7 +1645,11 @@ export class ConnectionDialogWebviewController extends FormWebviewController<
for (const t of tenantSubMap.keys()) {
for (const s of tenantSubMap.get(t)) {
promiseArray.push(
this.loadAzureServersForSubscription(state, s.subscriptionId),
this.loadAzureServersForSubscription(
state,
s.tenantId,
s.subscriptionId,
),
);
}
}
Expand Down Expand Up @@ -1597,9 +1680,11 @@ export class ConnectionDialogWebviewController extends FormWebviewController<

private async loadAzureServersForSubscription(
state: ConnectionDialogWebviewState,
tenantId: string,
subscriptionId: string,
) {
const azSub = this._azureSubscriptions.get(subscriptionId);
const compositeKey = `${tenantId}/${subscriptionId}`;
const azSub = this._azureSubscriptions.get(compositeKey);
const stateSub = state.azureSubscriptions.find((s) => s.id === subscriptionId);

try {
Expand Down
8 changes: 7 additions & 1 deletion src/reactviews/pages/ConnectionDialog/azureBrowsePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,17 @@ export const AzureBrowsePage = () => {
setSelectedServerWithFormState,
setServerValue,
srvs,
DefaultSelectionMode.SelectFirstIfAny,
DefaultSelectionMode.AlwaysSelectNone,
);
}
}, [locations, selectedLocation, context.state.azureServers]);

// databases
useEffect(() => {
if (!selectedServer) {
setDatabases([]);
setSelectedDatabase(undefined);
setDatabaseValue("");
return; // should not be visible if no server is selected
}

Expand All @@ -209,6 +212,9 @@ export const AzureBrowsePage = () => {
);

if (!server) {
setDatabases([]);
setSelectedDatabase(undefined);
setDatabaseValue("");
return;
}

Expand Down
1 change: 1 addition & 0 deletions src/sharedInterfaces/connectionDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export interface CreateConnectionGroupDialogProps extends IDialogProps {
export interface AzureSubscriptionInfo {
name: string;
id: string;
tenantId: string;
loaded: boolean;
}

Expand Down
Loading