Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/accounts/common/defaultAccount.ts
3296 views
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
6
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
7
import { Emitter, Event } from '../../../../base/common/event.js';
8
import { Disposable } from '../../../../base/common/lifecycle.js';
9
import { IProductService } from '../../../../platform/product/common/productService.js';
10
import { IAuthenticationService } from '../../authentication/common/authentication.js';
11
import { asJson, IRequestService } from '../../../../platform/request/common/request.js';
12
import { CancellationToken } from '../../../../base/common/cancellation.js';
13
import { IExtensionService } from '../../extensions/common/extensions.js';
14
import { ILogService } from '../../../../platform/log/common/log.js';
15
import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
16
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
17
import { localize } from '../../../../nls.js';
18
import { IWorkbenchContribution } from '../../../common/contributions.js';
19
import { Barrier } from '../../../../base/common/async.js';
20
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
21
import { getErrorMessage } from '../../../../base/common/errors.js';
22
import { IDefaultAccount } from '../../../../base/common/defaultAccount.js';
23
24
export const DEFAULT_ACCOUNT_SIGN_IN_COMMAND = 'workbench.actions.accounts.signIn';
25
26
const enum DefaultAccountStatus {
27
Uninitialized = 'uninitialized',
28
Unavailable = 'unavailable',
29
Available = 'available',
30
}
31
32
const CONTEXT_DEFAULT_ACCOUNT_STATE = new RawContextKey<string>('defaultAccountStatus', DefaultAccountStatus.Uninitialized);
33
34
interface IChatEntitlementsResponse {
35
readonly access_type_sku: string;
36
readonly assigned_date: string;
37
readonly can_signup_for_limited: boolean;
38
readonly chat_enabled: boolean;
39
readonly analytics_tracking_id: string;
40
readonly limited_user_quotas?: {
41
readonly chat: number;
42
readonly completions: number;
43
};
44
readonly monthly_quotas?: {
45
readonly chat: number;
46
readonly completions: number;
47
};
48
readonly limited_user_reset_date: string;
49
}
50
51
interface ITokenEntitlementsResponse {
52
token: string;
53
}
54
55
interface IMcpRegistryProvider {
56
readonly url: string;
57
readonly registry_access: 'allow_all' | 'registry_only';
58
readonly owner: {
59
readonly login: string;
60
readonly id: number;
61
readonly type: string;
62
readonly parent_login: string | null;
63
readonly priority: number;
64
};
65
}
66
67
interface IMcpRegistryResponse {
68
readonly mcp_registries: ReadonlyArray<IMcpRegistryProvider>;
69
}
70
71
export const IDefaultAccountService = createDecorator<IDefaultAccountService>('defaultAccountService');
72
73
export interface IDefaultAccountService {
74
75
readonly _serviceBrand: undefined;
76
77
readonly onDidChangeDefaultAccount: Event<IDefaultAccount | null>;
78
79
getDefaultAccount(): Promise<IDefaultAccount | null>;
80
setDefaultAccount(account: IDefaultAccount | null): void;
81
}
82
83
export class DefaultAccountService extends Disposable implements IDefaultAccountService {
84
declare _serviceBrand: undefined;
85
86
private _defaultAccount: IDefaultAccount | null | undefined = undefined;
87
get defaultAccount(): IDefaultAccount | null { return this._defaultAccount ?? null; }
88
89
private readonly initBarrier = new Barrier();
90
91
private readonly _onDidChangeDefaultAccount = this._register(new Emitter<IDefaultAccount | null>());
92
readonly onDidChangeDefaultAccount = this._onDidChangeDefaultAccount.event;
93
94
async getDefaultAccount(): Promise<IDefaultAccount | null> {
95
await this.initBarrier.wait();
96
return this.defaultAccount;
97
}
98
99
setDefaultAccount(account: IDefaultAccount | null): void {
100
const oldAccount = this._defaultAccount;
101
this._defaultAccount = account;
102
103
if (oldAccount !== this._defaultAccount) {
104
this._onDidChangeDefaultAccount.fire(this._defaultAccount);
105
}
106
107
this.initBarrier.open();
108
}
109
110
}
111
112
export class NullDefaultAccountService extends Disposable implements IDefaultAccountService {
113
114
declare _serviceBrand: undefined;
115
116
readonly onDidChangeDefaultAccount = Event.None;
117
118
async getDefaultAccount(): Promise<IDefaultAccount | null> {
119
return null;
120
}
121
122
setDefaultAccount(account: IDefaultAccount | null): void {
123
// noop
124
}
125
126
}
127
128
export class DefaultAccountManagementContribution extends Disposable implements IWorkbenchContribution {
129
130
static ID = 'workbench.contributions.defaultAccountManagement';
131
132
private defaultAccount: IDefaultAccount | null = null;
133
private readonly accountStatusContext: IContextKey<string>;
134
135
constructor(
136
@IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService,
137
@IConfigurationService private readonly configurationService: IConfigurationService,
138
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
139
@IExtensionService private readonly extensionService: IExtensionService,
140
@IProductService private readonly productService: IProductService,
141
@IRequestService private readonly requestService: IRequestService,
142
@ILogService private readonly logService: ILogService,
143
@IContextKeyService contextKeyService: IContextKeyService,
144
) {
145
super();
146
this.accountStatusContext = CONTEXT_DEFAULT_ACCOUNT_STATE.bindTo(contextKeyService);
147
this.initialize();
148
}
149
150
private async initialize(): Promise<void> {
151
if (!this.productService.defaultAccount) {
152
return;
153
}
154
155
const { authenticationProvider, tokenEntitlementUrl, chatEntitlementUrl, mcpRegistryDataUrl } = this.productService.defaultAccount;
156
await this.extensionService.whenInstalledExtensionsRegistered();
157
158
const declaredProvider = this.authenticationService.declaredProviders.find(provider => provider.id === authenticationProvider.id);
159
if (!declaredProvider) {
160
this.logService.info(`Default account authentication provider ${authenticationProvider} is not declared.`);
161
return;
162
}
163
164
this.registerSignInAction(authenticationProvider.id, declaredProvider.label, authenticationProvider.enterpriseProviderId, authenticationProvider.enterpriseProviderConfig, authenticationProvider.scopes);
165
this.setDefaultAccount(await this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider.id, authenticationProvider.enterpriseProviderId, authenticationProvider.enterpriseProviderConfig, authenticationProvider.scopes, tokenEntitlementUrl, chatEntitlementUrl, mcpRegistryDataUrl));
166
167
this._register(this.authenticationService.onDidChangeSessions(async e => {
168
if (e.providerId !== authenticationProvider.id && e.providerId !== authenticationProvider.enterpriseProviderId) {
169
return;
170
}
171
172
if (this.defaultAccount && e.event.removed?.some(session => session.id === this.defaultAccount?.sessionId)) {
173
this.setDefaultAccount(null);
174
return;
175
}
176
this.setDefaultAccount(await this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider.id, authenticationProvider.enterpriseProviderId, authenticationProvider.enterpriseProviderConfig, authenticationProvider.scopes, tokenEntitlementUrl, chatEntitlementUrl, mcpRegistryDataUrl));
177
}));
178
179
}
180
181
private setDefaultAccount(account: IDefaultAccount | null): void {
182
this.defaultAccount = account;
183
this.defaultAccountService.setDefaultAccount(this.defaultAccount);
184
if (this.defaultAccount) {
185
this.accountStatusContext.set(DefaultAccountStatus.Available);
186
} else {
187
this.accountStatusContext.set(DefaultAccountStatus.Unavailable);
188
}
189
}
190
191
private extractFromToken(token: string): Map<string, string> {
192
const result = new Map<string, string>();
193
const firstPart = token?.split(':')[0];
194
const fields = firstPart?.split(';');
195
for (const field of fields) {
196
const [key, value] = field.split('=');
197
result.set(key, value);
198
}
199
this.logService.trace(`DefaultAccount#extractFromToken: ${JSON.stringify(Object.fromEntries(result))}`);
200
return result;
201
}
202
203
private async getDefaultAccountFromAuthenticatedSessions(authProviderId: string, enterpriseAuthProviderId: string, enterpriseAuthProviderConfig: string, scopes: string[], tokenEntitlementUrl: string, chatEntitlementUrl: string, mcpRegistryDataUrl: string): Promise<IDefaultAccount | null> {
204
const id = this.configurationService.getValue(enterpriseAuthProviderConfig) === enterpriseAuthProviderId ? enterpriseAuthProviderId : authProviderId;
205
const sessions = await this.authenticationService.getSessions(id, undefined, undefined, true);
206
const session = sessions.find(s => this.scopesMatch(s.scopes, scopes));
207
208
if (!session) {
209
return null;
210
}
211
212
const [chatEntitlements, tokenEntitlements] = await Promise.all([
213
this.getChatEntitlements(session.accessToken, chatEntitlementUrl),
214
this.getTokenEntitlements(session.accessToken, tokenEntitlementUrl),
215
]);
216
217
const mcpRegistryProvider = this.productService.quality !== 'stable' && tokenEntitlements.mcp && this.configurationService.getValue<boolean>('chat.mcp.enterprise.registry.enabled') === true ? await this.getMcpRegistryProvider(session.accessToken, mcpRegistryDataUrl) : undefined;
218
219
return {
220
sessionId: session.id,
221
enterprise: id === enterpriseAuthProviderId || session.account.label.includes('_'),
222
...chatEntitlements,
223
...tokenEntitlements,
224
mcpRegistryUrl: mcpRegistryProvider?.url,
225
mcpAccess: mcpRegistryProvider?.registry_access,
226
};
227
}
228
229
private scopesMatch(scopes: ReadonlyArray<string>, expectedScopes: string[]): boolean {
230
return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope));
231
}
232
233
private async getTokenEntitlements(accessToken: string, tokenEntitlementsUrl: string): Promise<Partial<IDefaultAccount>> {
234
if (!tokenEntitlementsUrl) {
235
return {};
236
}
237
238
try {
239
const chatContext = await this.requestService.request({
240
type: 'GET',
241
url: tokenEntitlementsUrl,
242
disableCache: true,
243
headers: {
244
'Authorization': `Bearer ${accessToken}`
245
}
246
}, CancellationToken.None);
247
248
const chatData = await asJson<ITokenEntitlementsResponse>(chatContext);
249
if (chatData) {
250
const tokenMap = this.extractFromToken(chatData.token);
251
return {
252
// Editor preview features are disabled if the flag is present and set to 0
253
chat_preview_features_enabled: tokenMap.get('editor_preview_features') !== '0',
254
chat_agent_enabled: tokenMap.get('agent_mode') !== '0',
255
// MCP is disabled if the flag is present and set to 0
256
mcp: tokenMap.get('mcp') !== '0',
257
};
258
}
259
this.logService.error('Failed to fetch token entitlements', 'No data returned');
260
} catch (error) {
261
this.logService.error('Failed to fetch token entitlements', getErrorMessage(error));
262
}
263
264
return {};
265
}
266
267
private async getChatEntitlements(accessToken: string, chatEntitlementsUrl: string): Promise<Partial<IChatEntitlementsResponse>> {
268
if (!chatEntitlementsUrl) {
269
return {};
270
}
271
272
try {
273
const context = await this.requestService.request({
274
type: 'GET',
275
url: chatEntitlementsUrl,
276
disableCache: true,
277
headers: {
278
'Authorization': `Bearer ${accessToken}`
279
}
280
}, CancellationToken.None);
281
282
const data = await asJson<IChatEntitlementsResponse>(context);
283
if (data) {
284
return data;
285
}
286
this.logService.error('Failed to fetch entitlements', 'No data returned');
287
} catch (error) {
288
this.logService.error('Failed to fetch entitlements', getErrorMessage(error));
289
}
290
return {};
291
}
292
293
private async getMcpRegistryProvider(accessToken: string, mcpRegistryDataUrl: string): Promise<IMcpRegistryProvider | undefined> {
294
if (!mcpRegistryDataUrl) {
295
return undefined;
296
}
297
298
try {
299
const context = await this.requestService.request({
300
type: 'GET',
301
url: mcpRegistryDataUrl,
302
disableCache: true,
303
headers: {
304
'Authorization': `Bearer ${accessToken}`
305
}
306
}, CancellationToken.None);
307
308
const data = await asJson<IMcpRegistryResponse>(context);
309
if (data) {
310
this.logService.debug('Fetched MCP registry providers', data.mcp_registries);
311
return data.mcp_registries[0];
312
}
313
this.logService.error('Failed to fetch MCP registry providers', 'No data returned');
314
} catch (error) {
315
this.logService.error('Failed to fetch MCP registry providers', getErrorMessage(error));
316
}
317
return undefined;
318
}
319
320
private registerSignInAction(authProviderId: string, authProviderLabel: string, enterpriseAuthProviderId: string, enterpriseAuthProviderConfig: string, scopes: string[]): void {
321
const that = this;
322
this._register(registerAction2(class extends Action2 {
323
constructor() {
324
super({
325
id: DEFAULT_ACCOUNT_SIGN_IN_COMMAND,
326
title: localize('sign in', "Sign in to {0}", authProviderLabel),
327
});
328
}
329
run(): Promise<any> {
330
const id = that.configurationService.getValue(enterpriseAuthProviderConfig) === enterpriseAuthProviderId ? enterpriseAuthProviderId : authProviderId;
331
return that.authenticationService.createSession(id, scopes);
332
}
333
}));
334
}
335
336
}
337
338