Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auth: Support multi-account scenarios #1827

Merged
merged 17 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion auth/src/AzureSubscriptionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ export interface AzureSubscriptionProvider {
* Asks the user to sign in or pick an account to use.
*
* @param tenantId (Optional) Provide to sign in to a specific tenant.
* @param account (Optional) Provide to sign in to a specific account.
*
* @returns True if the user is signed in, false otherwise.
*/
signIn(tenantId?: string): Promise<boolean>;
signIn(tenantId?: string, account?: vscode.AuthenticationSessionAccountInformation): Promise<boolean>;
alexweininger marked this conversation as resolved.
Show resolved Hide resolved

/**
* An event that is fired when the user signs in. Debounced to fire at most once every 5 seconds.
Expand Down
65 changes: 51 additions & 14 deletions auth/src/VSCodeAzureSubscriptionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ export class VSCodeAzureSubscriptionProvider extends vscode.Disposable implement
* @param filter - Whether to filter the list returned, according to the list returned
* by `getTenantFilters()` and `getSubscriptionFilters()`. Optional, default true.
*
* @returns A list of Azure subscriptions.
* @returns A list of Azure subscriptions. The list is sorted by subscription name.
* The list can contain duplicate subscriptions if they come from different accounts.
*
* @throws A {@link NotSignedInError} If the user is not signed in to Azure.
* Use {@link isSignedIn} and/or {@link signIn} before this method to ensure
Expand All @@ -94,7 +95,7 @@ export class VSCodeAzureSubscriptionProvider extends vscode.Disposable implement
const tenantIds = await this.getTenantFilters();
const shouldFilterTenants = filter && !!tenantIds.length; // If the list is empty it is treated as "no filter"

const results: AzureSubscription[] = [];
const allSubscriptions: AzureSubscription[] = [];

try {
this.suppressSignInEvents = true;
Expand All @@ -111,40 +112,63 @@ export class VSCodeAzureSubscriptionProvider extends vscode.Disposable implement
continue;
}

// If the user is not signed in to this tenant, then skip it
if (!(await this.isSignedIn(tenantId, account))) {
continue;
}

// For each tenant, get the list of subscriptions
results.push(...await this.getSubscriptionsForTenant(tenantId, account));
allSubscriptions.push(...await this.getSubscriptionsForTenant(account, tenantId));
}

// list subscriptions for the home tenant
allSubscriptions.push(...await this.getSubscriptionsForTenant(account))
}
} finally {
this.suppressSignInEvents = false;
}

// It's possible that by listing subscriptions in all tenants and the "home" tenant there could be duplicate subscriptions
// Thus, we remove duplicate subscriptions. However, if multiple accounts have the same subscription, we keep them.
const subscriptionMap = new Map<string, AzureSubscription>();
allSubscriptions.forEach(sub => subscriptionMap.set(`${sub.account.id}/${sub.subscriptionId}`, sub));
const uniqueSubscriptions = Array.from(subscriptionMap.values());

const sortSubscriptions = (subscriptions: AzureSubscription[]): AzureSubscription[] =>
subscriptions.sort((a, b) => a.name.localeCompare(b.name));

const subscriptionIds = await this.getSubscriptionFilters();
if (filter && !!subscriptionIds.length) { // If the list is empty it is treated as "no filter"
return sortSubscriptions(
results.filter(sub => subscriptionIds.includes(sub.subscriptionId))
uniqueSubscriptions.filter(sub => subscriptionIds.includes(sub.subscriptionId))
);
}

return sortSubscriptions(results);
return sortSubscriptions(uniqueSubscriptions);
}

/**
* Checks to see if a user is signed in.
*
* @param tenantId (Optional) Provide to check if a user is signed in to a specific tenant.
* @param account (Optional) Provide to check if a user is signed in to a specific account.
*
* @returns True if the user is signed in, false otherwise.
*
* If no tenant or account is provided, then
* checks all accounts for a session.
*/
public async isSignedIn(tenantId?: string, account?: vscode.AuthenticationSessionAccountInformation): Promise<boolean> {

// If no tenant or account is provided, then check all accounts for a session
if (!account && !tenantId) {
const accounts = await vscode.authentication.getAccounts(getConfiguredAuthProviderId());
if (accounts.length === 0) {
return false;
}

for (const account of accounts) {
if (await this.isSignedIn(undefined, account)) {
return true;
}
}
}

const session = await getSessionFromVSCode([], tenantId, { createIfNone: false, silent: true, account });
return !!session;
}
Expand All @@ -153,11 +177,18 @@ export class VSCodeAzureSubscriptionProvider extends vscode.Disposable implement
* Asks the user to sign in or pick an account to use.
*
* @param tenantId (Optional) Provide to sign in to a specific tenant.
* @param account (Optional) Provide to sign in to a specific account.
*
* @returns True if the user is signed in, false otherwise.
*/
public async signIn(tenantId?: string): Promise<boolean> {
const session = await getSessionFromVSCode([], tenantId, { createIfNone: true, clearSessionPreference: true });
public async signIn(tenantId?: string, account?: vscode.AuthenticationSessionAccountInformation): Promise<boolean> {

const session = await getSessionFromVSCode([], tenantId, {
createIfNone: true,
// If no account is provided, then clear the session preference which tells VS Code to show the account picker
clearSessionPreference: !account,
account,
});
return !!session;
}

Expand Down Expand Up @@ -219,7 +250,13 @@ export class VSCodeAzureSubscriptionProvider extends vscode.Disposable implement
*
* @returns The list of subscriptions for the tenant.
*/
private async getSubscriptionsForTenant(tenantId: string, account: vscode.AuthenticationSessionAccountInformation): Promise<AzureSubscription[]> {
private async getSubscriptionsForTenant(account: vscode.AuthenticationSessionAccountInformation, tenantId?: string): Promise<AzureSubscription[]> {
// If the user is not signed in to this tenant or account, then return an empty list
// This is to prevent the NotSignedInError from being thrown in getSubscriptionClient
if (!await this.isSignedIn(tenantId, account)) {
return [];
}

const { client, credential, authentication } = await this.getSubscriptionClient(account, tenantId, undefined);
const environment = getConfiguredAzureEnv();

Expand All @@ -234,8 +271,8 @@ export class VSCodeAzureSubscriptionProvider extends vscode.Disposable implement
/* eslint-disable @typescript-eslint/no-non-null-assertion */
name: subscription.displayName!,
subscriptionId: subscription.subscriptionId!,
tenantId: tenantId ?? subscription.tenantId!,
/* eslint-enable @typescript-eslint/no-non-null-assertion */
alexweininger marked this conversation as resolved.
Show resolved Hide resolved
tenantId: tenantId,
account: account
});
}
Expand Down