@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.
- package/CHANGELOG.md +4 -0
- package/README.md +9 -79
- package/dist/cjs/src/contracts/AzureAccount.js +7 -0
- package/dist/cjs/src/contracts/AzureSubscriptionProviderRequestOptions.js +48 -0
- package/dist/cjs/src/index.js +13 -10
- package/dist/cjs/src/providers/AzureDevOpsSubscriptionProvider.js +178 -0
- package/dist/cjs/src/providers/AzureSubscriptionProviderBase.js +393 -0
- package/dist/cjs/src/providers/VSCodeAzureSubscriptionProvider.js +269 -0
- package/dist/cjs/src/utils/Limiter.js +41 -0
- package/dist/cjs/src/{NotSignedInError.js → utils/NotSignedInError.js} +3 -2
- package/dist/cjs/src/utils/configuredAzureEnv.js +11 -16
- package/dist/cjs/src/utils/dedupeSubscriptions.js +27 -0
- package/dist/cjs/src/utils/getMetricsForTelemetry.js +47 -0
- package/dist/cjs/src/{getSessionFromVSCode.js → utils/getSessionFromVSCode.js} +5 -2
- package/dist/cjs/src/utils/getSignalForToken.js +29 -0
- package/dist/cjs/src/utils/map/CaselessMap.js +71 -0
- package/dist/cjs/src/utils/map/TwoKeyCaselessMap.js +194 -0
- package/dist/cjs/src/utils/screen.js +62 -0
- package/dist/cjs/src/{signInToTenant.js → utils/signInToTenant.js} +15 -13
- package/dist/cjs/src/utils/tryGetTokenExpiration.js +25 -0
- package/dist/esm/src/contracts/AzureAccount.d.ts +5 -0
- package/dist/esm/src/contracts/AzureAccount.js +6 -0
- package/dist/esm/src/{AzureAuthentication.d.ts → contracts/AzureAuthentication.d.ts} +1 -1
- package/dist/esm/src/{AzureSubscription.d.ts → contracts/AzureSubscription.d.ts} +4 -4
- package/dist/esm/src/contracts/AzureSubscriptionProvider.d.ts +112 -0
- package/dist/esm/src/contracts/AzureSubscriptionProviderRequestOptions.d.ts +103 -0
- package/dist/esm/src/contracts/AzureSubscriptionProviderRequestOptions.js +44 -0
- package/dist/esm/src/contracts/AzureTenant.d.ts +15 -0
- package/dist/esm/src/index.d.ts +13 -10
- package/dist/esm/src/index.js +13 -10
- package/dist/esm/src/providers/AzureDevOpsSubscriptionProvider.d.ts +68 -0
- package/dist/esm/src/providers/AzureDevOpsSubscriptionProvider.js +140 -0
- package/dist/esm/src/providers/AzureSubscriptionProviderBase.d.ts +74 -0
- package/dist/esm/src/providers/AzureSubscriptionProviderBase.js +356 -0
- package/dist/esm/src/providers/VSCodeAzureSubscriptionProvider.d.ts +70 -0
- package/dist/esm/src/providers/VSCodeAzureSubscriptionProvider.js +232 -0
- package/dist/esm/src/utils/Limiter.d.ts +9 -0
- package/dist/esm/src/utils/Limiter.js +37 -0
- package/dist/esm/src/{NotSignedInError.d.ts → utils/NotSignedInError.d.ts} +2 -2
- package/dist/esm/src/{NotSignedInError.js → utils/NotSignedInError.js} +3 -2
- package/dist/esm/src/utils/configuredAzureEnv.d.ts +7 -4
- package/dist/esm/src/utils/configuredAzureEnv.js +11 -16
- package/dist/esm/src/utils/dedupeSubscriptions.d.ts +14 -0
- package/dist/esm/src/utils/dedupeSubscriptions.js +24 -0
- package/dist/esm/src/utils/getMetricsForTelemetry.d.ts +32 -0
- package/dist/esm/src/utils/getMetricsForTelemetry.js +44 -0
- package/dist/esm/src/{getSessionFromVSCode.js → utils/getSessionFromVSCode.js} +5 -2
- package/dist/esm/src/utils/getSignalForToken.d.ts +7 -0
- package/dist/esm/src/utils/getSignalForToken.js +26 -0
- package/dist/esm/src/utils/map/CaselessMap.d.ts +28 -0
- package/dist/esm/src/utils/map/CaselessMap.js +67 -0
- package/dist/esm/src/utils/map/TwoKeyCaselessMap.d.ts +49 -0
- package/dist/esm/src/utils/map/TwoKeyCaselessMap.js +190 -0
- package/dist/esm/src/utils/screen.d.ts +9 -0
- package/dist/esm/src/utils/screen.js +59 -0
- package/dist/esm/src/utils/signInToTenant.d.ts +7 -0
- package/dist/esm/src/{signInToTenant.js → utils/signInToTenant.js} +16 -14
- package/dist/esm/src/utils/tryGetTokenExpiration.d.ts +2 -0
- package/dist/esm/src/utils/tryGetTokenExpiration.js +22 -0
- package/package.json +33 -23
- package/AzureFederatedCredentialsGuide.md +0 -174
- package/dist/cjs/src/AzureDevOpsSubscriptionProvider.js +0 -215
- package/dist/cjs/src/VSCodeAzureSubscriptionProvider.js +0 -395
- package/dist/cjs/src/utils/getUnauthenticatedTenants.js +0 -23
- package/dist/cjs/src/utils/isGetSubscriptionsFilter.js +0 -27
- package/dist/esm/src/AzureDevOpsSubscriptionProvider.d.ts +0 -68
- package/dist/esm/src/AzureDevOpsSubscriptionProvider.js +0 -210
- package/dist/esm/src/AzureSubscriptionProvider.d.ts +0 -82
- package/dist/esm/src/AzureTenant.d.ts +0 -5
- package/dist/esm/src/VSCodeAzureSubscriptionProvider.d.ts +0 -116
- package/dist/esm/src/VSCodeAzureSubscriptionProvider.js +0 -358
- package/dist/esm/src/signInToTenant.d.ts +0 -6
- package/dist/esm/src/utils/getUnauthenticatedTenants.d.ts +0 -9
- package/dist/esm/src/utils/getUnauthenticatedTenants.js +0 -20
- package/dist/esm/src/utils/isGetSubscriptionsFilter.d.ts +0 -14
- package/dist/esm/src/utils/isGetSubscriptionsFilter.js +0 -23
- /package/dist/cjs/src/{AzureAuthentication.js → contracts/AzureAuthentication.js} +0 -0
- /package/dist/cjs/src/{AzureSubscription.js → contracts/AzureSubscription.js} +0 -0
- /package/dist/cjs/src/{AzureSubscriptionProvider.js → contracts/AzureSubscriptionProvider.js} +0 -0
- /package/dist/cjs/src/{AzureTenant.js → contracts/AzureTenant.js} +0 -0
- /package/dist/esm/src/{AzureAuthentication.js → contracts/AzureAuthentication.js} +0 -0
- /package/dist/esm/src/{AzureSubscription.js → contracts/AzureSubscription.js} +0 -0
- /package/dist/esm/src/{AzureSubscriptionProvider.js → contracts/AzureSubscriptionProvider.js} +0 -0
- /package/dist/esm/src/{AzureTenant.js → contracts/AzureTenant.js} +0 -0
- /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
|