@microsoft/vscode-azext-azureauth 5.1.1 → 6.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +9 -79
  3. package/dist/cjs/src/contracts/AzureAccount.js +7 -0
  4. package/dist/cjs/src/contracts/AzureSubscriptionProviderRequestOptions.js +48 -0
  5. package/dist/cjs/src/index.js +13 -10
  6. package/dist/cjs/src/providers/AzureDevOpsSubscriptionProvider.js +178 -0
  7. package/dist/cjs/src/providers/AzureSubscriptionProviderBase.js +393 -0
  8. package/dist/cjs/src/providers/VSCodeAzureSubscriptionProvider.js +269 -0
  9. package/dist/cjs/src/utils/Limiter.js +41 -0
  10. package/dist/cjs/src/{NotSignedInError.js → utils/NotSignedInError.js} +3 -2
  11. package/dist/cjs/src/utils/configuredAzureEnv.js +11 -16
  12. package/dist/cjs/src/utils/dedupeSubscriptions.js +27 -0
  13. package/dist/cjs/src/utils/getMetricsForTelemetry.js +47 -0
  14. package/dist/cjs/src/{getSessionFromVSCode.js → utils/getSessionFromVSCode.js} +5 -2
  15. package/dist/cjs/src/utils/getSignalForToken.js +29 -0
  16. package/dist/cjs/src/utils/map/CaselessMap.js +71 -0
  17. package/dist/cjs/src/utils/map/TwoKeyCaselessMap.js +194 -0
  18. package/dist/cjs/src/utils/screen.js +62 -0
  19. package/dist/cjs/src/{signInToTenant.js → utils/signInToTenant.js} +15 -13
  20. package/dist/cjs/src/utils/tryGetTokenExpiration.js +25 -0
  21. package/dist/esm/src/contracts/AzureAccount.d.ts +5 -0
  22. package/dist/esm/src/contracts/AzureAccount.js +6 -0
  23. package/dist/esm/src/{AzureAuthentication.d.ts → contracts/AzureAuthentication.d.ts} +1 -1
  24. package/dist/esm/src/{AzureSubscription.d.ts → contracts/AzureSubscription.d.ts} +4 -4
  25. package/dist/esm/src/contracts/AzureSubscriptionProvider.d.ts +112 -0
  26. package/dist/esm/src/contracts/AzureSubscriptionProviderRequestOptions.d.ts +103 -0
  27. package/dist/esm/src/contracts/AzureSubscriptionProviderRequestOptions.js +44 -0
  28. package/dist/esm/src/contracts/AzureTenant.d.ts +15 -0
  29. package/dist/esm/src/index.d.ts +13 -10
  30. package/dist/esm/src/index.js +13 -10
  31. package/dist/esm/src/providers/AzureDevOpsSubscriptionProvider.d.ts +68 -0
  32. package/dist/esm/src/providers/AzureDevOpsSubscriptionProvider.js +140 -0
  33. package/dist/esm/src/providers/AzureSubscriptionProviderBase.d.ts +74 -0
  34. package/dist/esm/src/providers/AzureSubscriptionProviderBase.js +356 -0
  35. package/dist/esm/src/providers/VSCodeAzureSubscriptionProvider.d.ts +70 -0
  36. package/dist/esm/src/providers/VSCodeAzureSubscriptionProvider.js +232 -0
  37. package/dist/esm/src/utils/Limiter.d.ts +9 -0
  38. package/dist/esm/src/utils/Limiter.js +37 -0
  39. package/dist/esm/src/{NotSignedInError.d.ts → utils/NotSignedInError.d.ts} +2 -2
  40. package/dist/esm/src/{NotSignedInError.js → utils/NotSignedInError.js} +3 -2
  41. package/dist/esm/src/utils/configuredAzureEnv.d.ts +7 -4
  42. package/dist/esm/src/utils/configuredAzureEnv.js +11 -16
  43. package/dist/esm/src/utils/dedupeSubscriptions.d.ts +14 -0
  44. package/dist/esm/src/utils/dedupeSubscriptions.js +24 -0
  45. package/dist/esm/src/utils/getMetricsForTelemetry.d.ts +32 -0
  46. package/dist/esm/src/utils/getMetricsForTelemetry.js +44 -0
  47. package/dist/esm/src/{getSessionFromVSCode.js → utils/getSessionFromVSCode.js} +5 -2
  48. package/dist/esm/src/utils/getSignalForToken.d.ts +7 -0
  49. package/dist/esm/src/utils/getSignalForToken.js +26 -0
  50. package/dist/esm/src/utils/map/CaselessMap.d.ts +28 -0
  51. package/dist/esm/src/utils/map/CaselessMap.js +67 -0
  52. package/dist/esm/src/utils/map/TwoKeyCaselessMap.d.ts +49 -0
  53. package/dist/esm/src/utils/map/TwoKeyCaselessMap.js +190 -0
  54. package/dist/esm/src/utils/screen.d.ts +9 -0
  55. package/dist/esm/src/utils/screen.js +59 -0
  56. package/dist/esm/src/utils/signInToTenant.d.ts +7 -0
  57. package/dist/esm/src/{signInToTenant.js → utils/signInToTenant.js} +16 -14
  58. package/dist/esm/src/utils/tryGetTokenExpiration.d.ts +2 -0
  59. package/dist/esm/src/utils/tryGetTokenExpiration.js +22 -0
  60. package/package.json +33 -23
  61. package/AzureFederatedCredentialsGuide.md +0 -174
  62. package/dist/cjs/src/AzureDevOpsSubscriptionProvider.js +0 -215
  63. package/dist/cjs/src/VSCodeAzureSubscriptionProvider.js +0 -395
  64. package/dist/cjs/src/utils/getUnauthenticatedTenants.js +0 -23
  65. package/dist/cjs/src/utils/isGetSubscriptionsFilter.js +0 -27
  66. package/dist/esm/src/AzureDevOpsSubscriptionProvider.d.ts +0 -68
  67. package/dist/esm/src/AzureDevOpsSubscriptionProvider.js +0 -210
  68. package/dist/esm/src/AzureSubscriptionProvider.d.ts +0 -82
  69. package/dist/esm/src/AzureTenant.d.ts +0 -5
  70. package/dist/esm/src/VSCodeAzureSubscriptionProvider.d.ts +0 -116
  71. package/dist/esm/src/VSCodeAzureSubscriptionProvider.js +0 -358
  72. package/dist/esm/src/signInToTenant.d.ts +0 -6
  73. package/dist/esm/src/utils/getUnauthenticatedTenants.d.ts +0 -9
  74. package/dist/esm/src/utils/getUnauthenticatedTenants.js +0 -20
  75. package/dist/esm/src/utils/isGetSubscriptionsFilter.d.ts +0 -14
  76. package/dist/esm/src/utils/isGetSubscriptionsFilter.js +0 -23
  77. /package/dist/cjs/src/{AzureAuthentication.js → contracts/AzureAuthentication.js} +0 -0
  78. /package/dist/cjs/src/{AzureSubscription.js → contracts/AzureSubscription.js} +0 -0
  79. /package/dist/cjs/src/{AzureSubscriptionProvider.js → contracts/AzureSubscriptionProvider.js} +0 -0
  80. /package/dist/cjs/src/{AzureTenant.js → contracts/AzureTenant.js} +0 -0
  81. /package/dist/esm/src/{AzureAuthentication.js → contracts/AzureAuthentication.js} +0 -0
  82. /package/dist/esm/src/{AzureSubscription.js → contracts/AzureSubscription.js} +0 -0
  83. /package/dist/esm/src/{AzureSubscriptionProvider.js → contracts/AzureSubscriptionProvider.js} +0 -0
  84. /package/dist/esm/src/{AzureTenant.js → contracts/AzureTenant.js} +0 -0
  85. /package/dist/esm/src/{getSessionFromVSCode.d.ts → utils/getSessionFromVSCode.d.ts} +0 -0
@@ -0,0 +1,356 @@
1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License. See License.txt in the project root for license information.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ import * as vscode from 'vscode';
6
+ import { DefaultOptions, DefaultSignInOptions } from '../contracts/AzureSubscriptionProviderRequestOptions';
7
+ import { getConfiguredAuthProviderId, getConfiguredAzureEnv } from '../utils/configuredAzureEnv';
8
+ import { dedupeSubscriptions } from '../utils/dedupeSubscriptions';
9
+ import { getSessionFromVSCode } from '../utils/getSessionFromVSCode';
10
+ import { getSignalForToken } from '../utils/getSignalForToken';
11
+ import { isAuthenticationWwwAuthenticateRequest } from '../utils/isAuthenticationWwwAuthenticateRequest';
12
+ import { Limiter } from '../utils/Limiter';
13
+ import { isNotSignedInError, NotSignedInError } from '../utils/NotSignedInError';
14
+ import { screen } from '../utils/screen';
15
+ import { tryGetTokenExpiration } from '../utils/tryGetTokenExpiration';
16
+ const EventDebounce = 5 * 1000; // 5 seconds minimum between `onRefreshSuggested` events
17
+ const EventSilenceTime = 5 * 1000; // 5 seconds after sign-in to silence `onRefreshSuggested` events
18
+ const TenantListConcurrency = 3; // We will try to list tenants for at most 3 accounts in parallel
19
+ const SubscriptionListConcurrency = 5; // We will try to list subscriptions for at most 5 account+tenants in parallel
20
+ let armSubs;
21
+ /**
22
+ * Base class for Azure subscription providers that use VS Code authentication.
23
+ * Handles actual communication with Azure via the Azure SDK, as well as
24
+ * controlling the firing of `onRefreshSuggested` events.
25
+ */
26
+ export class AzureSubscriptionProviderBase {
27
+ logger;
28
+ sessionChangeListener;
29
+ refreshSuggestedEmitter = new vscode.EventEmitter();
30
+ lastRefreshSuggestedTime = 0;
31
+ suppressRefreshSuggestedEvents = false;
32
+ /**
33
+ * Constructs a new {@link AzureSubscriptionProviderBase}.
34
+ * @param logger (Optional) A logger to record information to
35
+ */
36
+ constructor(logger) {
37
+ this.logger = logger;
38
+ }
39
+ dispose() {
40
+ if (this.timeout) {
41
+ clearTimeout(this.timeout);
42
+ this.timeout = undefined;
43
+ }
44
+ this.sessionChangeListener?.dispose();
45
+ this.refreshSuggestedEmitter.dispose();
46
+ }
47
+ /**
48
+ * @inheritdoc
49
+ */
50
+ onRefreshSuggested(callback, thisArg, disposables) {
51
+ this.sessionChangeListener ??= vscode.authentication.onDidChangeSessions(evt => {
52
+ if (evt.provider.id === getConfiguredAuthProviderId()) {
53
+ this.fireRefreshSuggestedIfNeeded({ reason: 'sessionChange' });
54
+ }
55
+ });
56
+ return this.refreshSuggestedEmitter.event(callback, thisArg, disposables);
57
+ }
58
+ fireRefreshSuggestedIfNeeded(evtArgs) {
59
+ if (this.suppressRefreshSuggestedEvents || Date.now() < this.lastRefreshSuggestedTime + EventDebounce) {
60
+ // Suppress and/or debounce events to avoid flooding
61
+ return false;
62
+ }
63
+ this.log(`Firing onRefreshSuggested event due to reason: ${evtArgs.reason}`);
64
+ this.lastRefreshSuggestedTime = Date.now();
65
+ this.refreshSuggestedEmitter.fire(evtArgs);
66
+ return true;
67
+ }
68
+ /**
69
+ * @inheritdoc
70
+ */
71
+ async signIn(tenant, options = DefaultSignInOptions) {
72
+ const prompt = options.promptIfNeeded ?? DefaultSignInOptions.promptIfNeeded;
73
+ if (prompt) {
74
+ // If interactive, suppress without timeout until sign in is done (it can take a while when done interactively)
75
+ this.suppressRefreshSuggestedEvents = true;
76
+ }
77
+ else {
78
+ // If silent, suppress with normal timeout
79
+ this.silenceRefreshEvents();
80
+ }
81
+ const session = await getSessionFromVSCode(undefined, tenant?.tenantId, {
82
+ account: tenant?.account,
83
+ clearSessionPreference: options.clearSessionPreference ?? DefaultSignInOptions.clearSessionPreference,
84
+ createIfNone: prompt,
85
+ silent: !prompt,
86
+ });
87
+ if (prompt) {
88
+ // Interactive sign in can take a while, so silence events for a bit longer
89
+ this.silenceRefreshEvents();
90
+ }
91
+ return !!session;
92
+ }
93
+ /**
94
+ * @inheritdoc
95
+ */
96
+ async getAvailableSubscriptions(options = DefaultOptions) {
97
+ try {
98
+ const availableSubscriptions = [];
99
+ const tenantListLimiter = new Limiter(TenantListConcurrency);
100
+ const tenantListPromises = [];
101
+ const subscriptionListLimiter = new Limiter(SubscriptionListConcurrency);
102
+ const subscriptionListPromisesFlat = [];
103
+ let tenantsProcessed = 0;
104
+ const maximumTenants = options.maximumTenants ?? DefaultOptions.maximumTenants;
105
+ const accounts = await this.getAccounts(options);
106
+ for (const account of accounts) {
107
+ this.throwIfCancelled(options.token);
108
+ tenantListPromises.push(tenantListLimiter.queue(async () => {
109
+ try {
110
+ if (tenantsProcessed >= maximumTenants) {
111
+ this.logForAccount(account, `Skipping account because maximum tenants of ${maximumTenants} has been reached`);
112
+ return;
113
+ }
114
+ const tenants = await this.getTenantsForAccount(account, options);
115
+ for (const tenant of tenants) {
116
+ this.throwIfCancelled(options.token);
117
+ if (tenantsProcessed >= maximumTenants) {
118
+ this.logForAccount(account, `Skipping remaining tenants because maximum tenants of ${maximumTenants} has been reached`);
119
+ break;
120
+ }
121
+ tenantsProcessed++;
122
+ subscriptionListPromisesFlat.push(subscriptionListLimiter.queue(async () => {
123
+ try {
124
+ const subscriptions = await this.getSubscriptionsForTenant(tenant, options);
125
+ availableSubscriptions.push(...subscriptions);
126
+ }
127
+ catch (err) {
128
+ if (isNotSignedInError(err)) {
129
+ this.logForTenant(tenant, 'Skipping account+tenant because it is not signed in');
130
+ return;
131
+ }
132
+ throw err;
133
+ }
134
+ }));
135
+ }
136
+ }
137
+ catch (err) {
138
+ if (isNotSignedInError(err)) {
139
+ this.logForAccount(account, 'Skipping account because it is not signed in');
140
+ return;
141
+ }
142
+ }
143
+ }));
144
+ }
145
+ await Promise.all(tenantListPromises);
146
+ await Promise.all(subscriptionListPromisesFlat);
147
+ return dedupeSubscriptions(availableSubscriptions);
148
+ }
149
+ catch (err) {
150
+ // Intentionally not eating NotSignedInError here, if it is thrown by getAccounts()
151
+ this.remapLogRethrow(err, options.token);
152
+ }
153
+ finally {
154
+ this.throwIfCancelled(options.token);
155
+ }
156
+ }
157
+ /**
158
+ * @inheritdoc
159
+ */
160
+ async getAccounts(options = DefaultOptions) {
161
+ try {
162
+ const startTime = Date.now();
163
+ this.log('Fetching accounts...');
164
+ this.silenceRefreshEvents();
165
+ const results = await vscode.authentication.getAccounts(getConfiguredAuthProviderId());
166
+ if (results.length === 0) {
167
+ this.log('No accounts found');
168
+ throw new NotSignedInError();
169
+ }
170
+ this.log(`Fetched ${results.length} accounts (before filter) in ${Date.now() - startTime}ms`);
171
+ return Array.from(results);
172
+ }
173
+ catch (err) {
174
+ // Cancellation is not actually supported by vscode.authentication.getAccounts, but just in case it is added in the future...
175
+ this.remapLogRethrow(err, options.token);
176
+ }
177
+ finally {
178
+ this.throwIfCancelled(options.token);
179
+ }
180
+ }
181
+ /**
182
+ * @inheritdoc
183
+ */
184
+ async getUnauthenticatedTenantsForAccount(account, options = DefaultOptions) {
185
+ try {
186
+ const startTime = Date.now();
187
+ const tenantListLimiter = new Limiter(TenantListConcurrency);
188
+ const tenantListPromises = [];
189
+ const allTenants = await this.getTenantsForAccount(account, { ...options, filter: false });
190
+ const unauthenticatedTenants = [];
191
+ for (const tenant of allTenants) {
192
+ tenantListPromises.push(tenantListLimiter.queue(async () => {
193
+ this.throwIfCancelled(options.token);
194
+ this.silenceRefreshEvents();
195
+ const session = await getSessionFromVSCode(undefined, tenant.tenantId, {
196
+ account: account,
197
+ createIfNone: false,
198
+ silent: true,
199
+ });
200
+ if (!session) {
201
+ unauthenticatedTenants.push(tenant);
202
+ }
203
+ }));
204
+ }
205
+ await Promise.all(tenantListPromises);
206
+ this.logForAccount(account, `Found ${unauthenticatedTenants.length} unauthenticated tenants in ${Date.now() - startTime}ms`);
207
+ return unauthenticatedTenants;
208
+ }
209
+ finally {
210
+ this.throwIfCancelled(options.token);
211
+ }
212
+ }
213
+ /**
214
+ * @inheritdoc
215
+ */
216
+ async getTenantsForAccount(account, options = DefaultOptions) {
217
+ try {
218
+ const startTime = Date.now();
219
+ this.logForAccount(account, 'Fetching tenants for account...');
220
+ const { client } = await this.getSubscriptionClient({ account: account, tenantId: undefined });
221
+ const allTenants = [];
222
+ for await (const tenant of client.tenants.list({ abortSignal: getSignalForToken(options.token) })) {
223
+ allTenants.push({
224
+ ...tenant,
225
+ tenantId: tenant.tenantId, // eslint-disable-line @typescript-eslint/no-non-null-assertion -- This is never null in practice
226
+ account: account,
227
+ });
228
+ }
229
+ this.logForAccount(account, `Fetched ${allTenants.length} tenants (before filter) in ${Date.now() - startTime}ms`);
230
+ return allTenants;
231
+ }
232
+ catch (err) {
233
+ this.remapLogRethrow(err, options.token);
234
+ }
235
+ finally {
236
+ this.throwIfCancelled(options.token);
237
+ }
238
+ }
239
+ /**
240
+ * @inheritdoc
241
+ */
242
+ async getSubscriptionsForTenant(tenant, options = DefaultOptions) {
243
+ try {
244
+ const startTime = Date.now();
245
+ this.logForTenant(tenant, 'Fetching subscriptions for account+tenant...');
246
+ const { client, credential, authentication } = await this.getSubscriptionClient(tenant);
247
+ const environment = getConfiguredAzureEnv();
248
+ const allSubs = [];
249
+ for await (const subscription of client.subscriptions.list({ abortSignal: getSignalForToken(options.token) })) {
250
+ allSubs.push({
251
+ authentication: authentication,
252
+ environment: environment,
253
+ credential: credential,
254
+ isCustomCloud: environment.isCustomCloud,
255
+ /* eslint-disable @typescript-eslint/no-non-null-assertion */
256
+ name: subscription.displayName,
257
+ subscriptionId: subscription.subscriptionId,
258
+ /* eslint-enable @typescript-eslint/no-non-null-assertion */
259
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
260
+ tenantId: subscription.tenantId || tenant.tenantId, // In rare cases, a subscription may be listed but come from a different tenant
261
+ account: tenant.account,
262
+ });
263
+ }
264
+ this.logForTenant(tenant, `Fetched ${allSubs.length} subscriptions (before filter) in ${Date.now() - startTime}ms`);
265
+ return allSubs;
266
+ }
267
+ catch (err) {
268
+ this.remapLogRethrow(err, options.token);
269
+ }
270
+ finally {
271
+ this.throwIfCancelled(options.token);
272
+ }
273
+ }
274
+ /**
275
+ * Gets a {@link SubscriptionClient} plus extras for the given account+tenant.
276
+ * @param tenant (Optional) The account+tenant to get a subscription client for. If not specified, the default account and home tenant
277
+ * will be used.
278
+ * @returns A {@link SubscriptionClient}, {@link TokenCredential}, and {@link AzureAuthentication} for the given account+tenant.
279
+ */
280
+ async getSubscriptionClient(tenant) {
281
+ const credential = {
282
+ getToken: async (scopes, options) => {
283
+ this.silenceRefreshEvents();
284
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
285
+ const session = await getSessionFromVSCode(scopes, options?.tenantId || tenant.tenantId, { createIfNone: false, silent: true, account: tenant.account });
286
+ if (!session) {
287
+ throw new NotSignedInError();
288
+ }
289
+ return {
290
+ token: session.accessToken,
291
+ expiresOnTimestamp: tryGetTokenExpiration(session),
292
+ };
293
+ }
294
+ };
295
+ armSubs ??= await import('@azure/arm-resources-subscriptions');
296
+ return {
297
+ client: new armSubs.SubscriptionClient(credential, { endpoint: getConfiguredAzureEnv().resourceManagerEndpointUrl }),
298
+ credential: credential,
299
+ authentication: {
300
+ getSession: async () => {
301
+ this.silenceRefreshEvents();
302
+ const session = await getSessionFromVSCode(undefined, tenant.tenantId, { createIfNone: false, silent: true, account: tenant.account });
303
+ if (!session) {
304
+ throw new NotSignedInError();
305
+ }
306
+ return session;
307
+ },
308
+ getSessionWithScopes: async (scopeListOrRequest) => {
309
+ this.silenceRefreshEvents();
310
+ // in order to handle a challenge, we must enable createIfNone so
311
+ // that we can prompt the user to step-up their session with MFA
312
+ // otherwise, never prompt the user
313
+ const session = await getSessionFromVSCode(scopeListOrRequest, tenant.tenantId, { ...(isAuthenticationWwwAuthenticateRequest(scopeListOrRequest) ? { createIfNone: true } : { silent: true }), account: tenant.account });
314
+ if (!session) {
315
+ throw new NotSignedInError();
316
+ }
317
+ return session;
318
+ },
319
+ }
320
+ };
321
+ }
322
+ log(message) {
323
+ this.logger?.debug(`[auth] ${message}`);
324
+ }
325
+ logForAccount(account, message) {
326
+ this.logger?.debug(`[auth] [account: ${screen(account)}] ${message}`);
327
+ }
328
+ logForTenant(tenant, message) {
329
+ this.logger?.debug(`[auth] [account: ${screen(tenant.account)}] [tenant: ${screen(tenant)}] ${message}`);
330
+ }
331
+ throwIfCancelled(token) {
332
+ if (token?.isCancellationRequested) {
333
+ throw new vscode.CancellationError();
334
+ }
335
+ }
336
+ timeout;
337
+ silenceRefreshEvents() {
338
+ this.suppressRefreshSuggestedEvents = true;
339
+ if (this.timeout) {
340
+ clearTimeout(this.timeout);
341
+ this.timeout = undefined;
342
+ }
343
+ this.timeout = setTimeout(() => {
344
+ clearTimeout(this.timeout);
345
+ this.timeout = undefined;
346
+ this.suppressRefreshSuggestedEvents = false;
347
+ }, EventSilenceTime);
348
+ }
349
+ remapLogRethrow(err, token) {
350
+ this.throwIfCancelled(token);
351
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
352
+ this.logger?.error(`[auth] Error occurred: ${err}`);
353
+ throw err;
354
+ }
355
+ }
356
+ //# sourceMappingURL=AzureSubscriptionProviderBase.js.map
@@ -0,0 +1,70 @@
1
+ import * as vscode from 'vscode';
2
+ import type { AzureAccount } from '../contracts/AzureAccount';
3
+ import type { AzureSubscription, SubscriptionId, TenantId } from '../contracts/AzureSubscription';
4
+ import type { RefreshSuggestedEvent, TenantIdAndAccount } from '../contracts/AzureSubscriptionProvider';
5
+ import { type GetAccountsOptions, type GetAvailableSubscriptionsOptions, type GetSubscriptionsForTenantOptions, type GetTenantsForAccountOptions } from '../contracts/AzureSubscriptionProviderRequestOptions';
6
+ import type { AzureTenant } from '../contracts/AzureTenant';
7
+ import { AzureSubscriptionProviderBase } from './AzureSubscriptionProviderBase';
8
+ /**
9
+ * Extension of {@link AzureSubscriptionProviderBase} that adds caching of accounts, tenants, and subscriptions,
10
+ * as well as filtering and deduplication according to configured settings. Additionally, promise
11
+ * coalescence is added for {@link getAvailableSubscriptions}.
12
+ *
13
+ * @note See important notes about caching on {@link BaseOptions.noCache}
14
+ */
15
+ export declare class VSCodeAzureSubscriptionProvider extends AzureSubscriptionProviderBase {
16
+ private readonly accountCache;
17
+ private readonly tenantCache;
18
+ private readonly subscriptionCache;
19
+ private readonly availableSubscriptionsPromises;
20
+ private configChangeListener;
21
+ dispose(): void;
22
+ /**
23
+ * @inheritdoc
24
+ */
25
+ onRefreshSuggested(callback: (reason: RefreshSuggestedEvent) => unknown, thisArg?: unknown, disposables?: vscode.Disposable[]): vscode.Disposable;
26
+ protected fireRefreshSuggestedIfNeeded(evtArgs: RefreshSuggestedEvent): boolean;
27
+ /**
28
+ * @inheritdoc
29
+ */
30
+ getAvailableSubscriptions(options?: GetAvailableSubscriptionsOptions): Promise<AzureSubscription[]>;
31
+ /**
32
+ * @inheritdoc
33
+ */
34
+ getAccounts(options?: GetAccountsOptions): Promise<AzureAccount[]>;
35
+ /**
36
+ * @inheritdoc
37
+ */
38
+ getTenantsForAccount(account: AzureAccount, options?: GetTenantsForAccountOptions): Promise<AzureTenant[]>;
39
+ /**
40
+ * @inheritdoc
41
+ */
42
+ getSubscriptionsForTenant(tenant: TenantIdAndAccount, options?: GetSubscriptionsForTenantOptions): Promise<AzureSubscription[]>;
43
+ /**
44
+ * Gets the account filters that are configured in `azureResourceGroups.selectedSubscriptions`. To
45
+ * override the settings with a custom filter, implement a child class with filter methods overridden.
46
+ *
47
+ * If no values are returned by `getAccountFilters()`, then all accounts will be scanned for subscriptions.
48
+ *
49
+ * @returns A list of account IDs that are configured in `azureResourceGroups.selectedSubscriptions`.
50
+ */
51
+ protected getAccountFilters(): Promise<string[]>;
52
+ /**
53
+ * Gets the tenant filters that are configured in `azureResourceGroups.selectedSubscriptions`. To
54
+ * override the settings with a custom filter, implement a child class with filter methods overridden.
55
+ *
56
+ * If no values are returned by `getTenantFilters()`, then all tenants will be scanned for subscriptions.
57
+ *
58
+ * @returns A list of unique tenant IDs that are configured in `azureResourceGroups.selectedSubscriptions`.
59
+ */
60
+ protected getTenantFilters(): Promise<TenantId[]>;
61
+ /**
62
+ * Gets the subscription filters that are configured in `azureResourceGroups.selectedSubscriptions`. To
63
+ * override the settings with a custom filter, implement a child class with filter methods overridden.
64
+ *
65
+ * If no values are returned by `getSubscriptionFilters()`, then all subscriptions will be scanned.
66
+ *
67
+ * @returns A list of unique subscription IDs that are configured in `azureResourceGroups.selectedSubscriptions`.
68
+ */
69
+ protected getSubscriptionFilters(): Promise<SubscriptionId[]>;
70
+ }
@@ -0,0 +1,232 @@
1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Microsoft Corporation. All rights reserved.
3
+ * Licensed under the MIT License. See License.txt in the project root for license information.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ import * as vscode from 'vscode';
6
+ import { DefaultOptions, getCoalescenceKey } from '../contracts/AzureSubscriptionProviderRequestOptions'; // eslint-disable-line @typescript-eslint/no-unused-vars -- It is used in the doc comments
7
+ import { dedupeSubscriptions } from '../utils/dedupeSubscriptions';
8
+ import { CaselessMap } from '../utils/map/CaselessMap';
9
+ import { TwoKeyCaselessMap } from '../utils/map/TwoKeyCaselessMap';
10
+ import { AzureSubscriptionProviderBase } from './AzureSubscriptionProviderBase';
11
+ const ConfigPrefix = 'azureResourceGroups';
12
+ const SelectedSubscriptionsConfigKey = 'selectedSubscriptions';
13
+ /**
14
+ * Extension of {@link AzureSubscriptionProviderBase} that adds caching of accounts, tenants, and subscriptions,
15
+ * as well as filtering and deduplication according to configured settings. Additionally, promise
16
+ * coalescence is added for {@link getAvailableSubscriptions}.
17
+ *
18
+ * @note See important notes about caching on {@link BaseOptions.noCache}
19
+ */
20
+ export class VSCodeAzureSubscriptionProvider extends AzureSubscriptionProviderBase {
21
+ accountCache = new CaselessMap(); // Key is the account ID
22
+ tenantCache = new CaselessMap(); // Key is the account ID
23
+ subscriptionCache = new TwoKeyCaselessMap(); // Keys are account ID and tenant ID
24
+ availableSubscriptionsPromises = new Map(); // Key is from getOptionsCoalescenceKey
25
+ configChangeListener;
26
+ dispose() {
27
+ this.configChangeListener?.dispose();
28
+ super.dispose();
29
+ }
30
+ /**
31
+ * @inheritdoc
32
+ */
33
+ onRefreshSuggested(callback, thisArg, disposables) {
34
+ this.configChangeListener ??= vscode.workspace.onDidChangeConfiguration(e => {
35
+ if (e.affectsConfiguration(`${ConfigPrefix}.${SelectedSubscriptionsConfigKey}`)) {
36
+ this.fireRefreshSuggestedIfNeeded({ reason: 'subscriptionFilterChange' });
37
+ }
38
+ });
39
+ return super.onRefreshSuggested(callback, thisArg, disposables);
40
+ }
41
+ fireRefreshSuggestedIfNeeded(evtArgs) {
42
+ const actuallyFired = super.fireRefreshSuggestedIfNeeded(evtArgs);
43
+ if (actuallyFired && evtArgs.reason === 'sessionChange') {
44
+ // Clear just the account cache--tenants and subscriptions are probably still valid,
45
+ // but the session change event may been have fired due to account sign out
46
+ this.accountCache.clear();
47
+ }
48
+ return actuallyFired;
49
+ }
50
+ /**
51
+ * @inheritdoc
52
+ */
53
+ async getAvailableSubscriptions(options = DefaultOptions) {
54
+ try {
55
+ const key = getCoalescenceKey(options);
56
+ if (key && this.availableSubscriptionsPromises.has(key)) {
57
+ return await this.availableSubscriptionsPromises.get(key); // eslint-disable-line @typescript-eslint/no-non-null-assertion -- We just checked it has the key
58
+ }
59
+ else {
60
+ try {
61
+ const promise = super.getAvailableSubscriptions(options);
62
+ if (key) {
63
+ this.availableSubscriptionsPromises.set(key, promise);
64
+ }
65
+ return await promise;
66
+ }
67
+ finally {
68
+ if (key) {
69
+ this.availableSubscriptionsPromises.delete(key);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ finally {
75
+ this.throwIfCancelled(options.token);
76
+ }
77
+ }
78
+ /**
79
+ * @inheritdoc
80
+ */
81
+ async getAccounts(options = DefaultOptions) {
82
+ try {
83
+ if (options.noCache ?? DefaultOptions.noCache) {
84
+ this.accountCache.clear();
85
+ }
86
+ // If needed, refill the cache
87
+ if (this.accountCache.size === 0) {
88
+ const accounts = await super.getAccounts(options);
89
+ accounts.forEach(account => this.accountCache.set(account.id, account));
90
+ this.log(`Cached ${accounts.length} accounts`);
91
+ }
92
+ else {
93
+ this.log('Using cached accounts');
94
+ }
95
+ let results = Array.from(this.accountCache.values());
96
+ // If needed, filter according to configured filters
97
+ if (options.filter ?? DefaultOptions.filter) {
98
+ const accountFilters = await this.getAccountFilters();
99
+ if (accountFilters.length > 0) {
100
+ this.log(`Filtering accounts to ${accountFilters.length} configured accounts`);
101
+ results = results.filter(account => accountFilters.includes(account.id.toLowerCase()));
102
+ }
103
+ }
104
+ this.log(`Returning ${results.length} accounts.`);
105
+ return results.sort((a, b) => a.label.localeCompare(b.label));
106
+ }
107
+ finally {
108
+ this.throwIfCancelled(options.token);
109
+ }
110
+ }
111
+ /**
112
+ * @inheritdoc
113
+ */
114
+ async getTenantsForAccount(account, options = DefaultOptions) {
115
+ try {
116
+ // If needed, delete the cache for this account
117
+ if (options.noCache ?? DefaultOptions.noCache) {
118
+ this.tenantCache.delete(account.id);
119
+ }
120
+ // If needed, refill the cache
121
+ if (!this.tenantCache.has(account.id)) {
122
+ const tenants = await super.getTenantsForAccount(account, options);
123
+ this.tenantCache.set(account.id, tenants);
124
+ this.logForAccount(account, `Cached ${tenants.length} tenants for account`);
125
+ }
126
+ else {
127
+ this.logForAccount(account, 'Using cached tenants for account');
128
+ }
129
+ let results = this.tenantCache.get(account.id); // eslint-disable-line @typescript-eslint/no-non-null-assertion -- We just filled it
130
+ // If needed, filter according to configured filters
131
+ if (options.filter ?? DefaultOptions.filter) {
132
+ const tenantFilters = await this.getTenantFilters();
133
+ if (tenantFilters.length > 0) {
134
+ this.logForAccount(account, `Filtering tenants for account to ${tenantFilters.length} configured tenants`);
135
+ results = results.filter(tenant => tenantFilters.includes(tenant.tenantId.toLowerCase()));
136
+ }
137
+ }
138
+ this.logForAccount(account, `Returning ${results.length} tenants for account`);
139
+ // Finally, sort
140
+ return results.sort((a, b) => {
141
+ if (a.displayName && b.displayName) {
142
+ return a.displayName.localeCompare(b.displayName);
143
+ }
144
+ return a.tenantId.localeCompare(b.tenantId);
145
+ });
146
+ }
147
+ finally {
148
+ this.throwIfCancelled(options.token);
149
+ }
150
+ }
151
+ /**
152
+ * @inheritdoc
153
+ */
154
+ async getSubscriptionsForTenant(tenant, options = DefaultOptions) {
155
+ try {
156
+ // If needed, delete the cache for this account+tenant
157
+ if (options.noCache ?? DefaultOptions.noCache) {
158
+ this.subscriptionCache.delete(tenant.account.id, tenant.tenantId);
159
+ }
160
+ // If needed, refill the cache
161
+ if (!this.subscriptionCache.has(tenant.account.id, tenant.tenantId)) {
162
+ const subscriptions = await super.getSubscriptionsForTenant(tenant, options);
163
+ this.subscriptionCache.set(tenant.account.id, tenant.tenantId, subscriptions);
164
+ this.logForTenant(tenant, `Cached ${subscriptions.length} subscriptions for account+tenant`);
165
+ }
166
+ else {
167
+ this.logForTenant(tenant, 'Using cached subscriptions for account+tenant');
168
+ }
169
+ let results = this.subscriptionCache.get(tenant.account.id, tenant.tenantId); // eslint-disable-line @typescript-eslint/no-non-null-assertion -- We just filled it
170
+ // If needed, filter according to configured filters
171
+ if (options.filter ?? DefaultOptions.filter) {
172
+ const subscriptionFilters = await this.getSubscriptionFilters();
173
+ if (subscriptionFilters.length > 0) {
174
+ this.logForTenant(tenant, `Filtering subscriptions for account+tenant to ${subscriptionFilters.length} configured subscriptions`);
175
+ results = results.filter(sub => subscriptionFilters.includes(sub.subscriptionId.toLowerCase()));
176
+ }
177
+ }
178
+ // If needed, dedupe according to options
179
+ if (options.dedupe ?? DefaultOptions.dedupe) {
180
+ this.logForTenant(tenant, 'Deduping subscriptions for account+tenant');
181
+ results = dedupeSubscriptions(results);
182
+ }
183
+ this.logForTenant(tenant, `Returning ${results.length} subscriptions for account+tenant`);
184
+ // Finally, sort
185
+ return results.sort((a, b) => a.name.localeCompare(b.name));
186
+ }
187
+ finally {
188
+ this.throwIfCancelled(options.token);
189
+ }
190
+ }
191
+ /**
192
+ * Gets the account filters that are configured in `azureResourceGroups.selectedSubscriptions`. To
193
+ * override the settings with a custom filter, implement a child class with filter methods overridden.
194
+ *
195
+ * If no values are returned by `getAccountFilters()`, then all accounts will be scanned for subscriptions.
196
+ *
197
+ * @returns A list of account IDs that are configured in `azureResourceGroups.selectedSubscriptions`.
198
+ */
199
+ getAccountFilters() {
200
+ // TODO: implement account filtering based on configuration if needed
201
+ return Promise.resolve([]);
202
+ }
203
+ /**
204
+ * Gets the tenant filters that are configured in `azureResourceGroups.selectedSubscriptions`. To
205
+ * override the settings with a custom filter, implement a child class with filter methods overridden.
206
+ *
207
+ * If no values are returned by `getTenantFilters()`, then all tenants will be scanned for subscriptions.
208
+ *
209
+ * @returns A list of unique tenant IDs that are configured in `azureResourceGroups.selectedSubscriptions`.
210
+ */
211
+ getTenantFilters() {
212
+ const config = vscode.workspace.getConfiguration(ConfigPrefix);
213
+ const fullSubscriptionIds = config.get(SelectedSubscriptionsConfigKey, []);
214
+ const tenantIds = fullSubscriptionIds.map(id => id.split('/')[0].toLowerCase());
215
+ return Promise.resolve(Array.from(new Set(tenantIds)));
216
+ }
217
+ /**
218
+ * Gets the subscription filters that are configured in `azureResourceGroups.selectedSubscriptions`. To
219
+ * override the settings with a custom filter, implement a child class with filter methods overridden.
220
+ *
221
+ * If no values are returned by `getSubscriptionFilters()`, then all subscriptions will be scanned.
222
+ *
223
+ * @returns A list of unique subscription IDs that are configured in `azureResourceGroups.selectedSubscriptions`.
224
+ */
225
+ getSubscriptionFilters() {
226
+ const config = vscode.workspace.getConfiguration(ConfigPrefix);
227
+ const fullSubscriptionIds = config.get(SelectedSubscriptionsConfigKey, []);
228
+ const subscriptionIds = fullSubscriptionIds.map(id => id.split('/')[1].toLowerCase());
229
+ return Promise.resolve(Array.from(new Set(subscriptionIds)));
230
+ }
231
+ }
232
+ //# sourceMappingURL=VSCodeAzureSubscriptionProvider.js.map
@@ -0,0 +1,9 @@
1
+ export declare class Limiter<T> {
2
+ private runningPromises;
3
+ private maxDegreeOfParalellism;
4
+ private outstandingPromises;
5
+ constructor(maxDegreeOfParalellism: number);
6
+ queue(factory: () => Promise<T>): Promise<T>;
7
+ private consume;
8
+ private consumed;
9
+ }