Path: blob/main/src/vs/workbench/services/accounts/common/defaultAccount.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { Disposable } from '../../../../base/common/lifecycle.js';8import { IProductService } from '../../../../platform/product/common/productService.js';9import { IAuthenticationService } from '../../authentication/common/authentication.js';10import { asJson, IRequestService } from '../../../../platform/request/common/request.js';11import { CancellationToken } from '../../../../base/common/cancellation.js';12import { IExtensionService } from '../../extensions/common/extensions.js';13import { ILogService } from '../../../../platform/log/common/log.js';14import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';15import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';16import { localize } from '../../../../nls.js';17import { IWorkbenchContribution } from '../../../common/contributions.js';18import { Barrier } from '../../../../base/common/async.js';19import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';20import { getErrorMessage } from '../../../../base/common/errors.js';21import { IDefaultAccount } from '../../../../base/common/defaultAccount.js';2223export const DEFAULT_ACCOUNT_SIGN_IN_COMMAND = 'workbench.actions.accounts.signIn';2425const enum DefaultAccountStatus {26Uninitialized = 'uninitialized',27Unavailable = 'unavailable',28Available = 'available',29}3031const CONTEXT_DEFAULT_ACCOUNT_STATE = new RawContextKey<string>('defaultAccountStatus', DefaultAccountStatus.Uninitialized);3233interface IChatEntitlementsResponse {34readonly access_type_sku: string;35readonly assigned_date: string;36readonly can_signup_for_limited: boolean;37readonly chat_enabled: boolean;38readonly analytics_tracking_id: string;39readonly limited_user_quotas?: {40readonly chat: number;41readonly completions: number;42};43readonly monthly_quotas?: {44readonly chat: number;45readonly completions: number;46};47readonly limited_user_reset_date: string;48}4950interface ITokenEntitlementsResponse {51token: string;52}5354interface IMcpRegistryProvider {55readonly url: string;56readonly registry_access: 'allow_all' | 'registry_only';57readonly owner: {58readonly login: string;59readonly id: number;60readonly type: string;61readonly parent_login: string | null;62readonly priority: number;63};64}6566interface IMcpRegistryResponse {67readonly mcp_registries: ReadonlyArray<IMcpRegistryProvider>;68}6970export const IDefaultAccountService = createDecorator<IDefaultAccountService>('defaultAccountService');7172export interface IDefaultAccountService {7374readonly _serviceBrand: undefined;7576readonly onDidChangeDefaultAccount: Event<IDefaultAccount | null>;7778getDefaultAccount(): Promise<IDefaultAccount | null>;79setDefaultAccount(account: IDefaultAccount | null): void;80}8182export class DefaultAccountService extends Disposable implements IDefaultAccountService {83declare _serviceBrand: undefined;8485private _defaultAccount: IDefaultAccount | null | undefined = undefined;86get defaultAccount(): IDefaultAccount | null { return this._defaultAccount ?? null; }8788private readonly initBarrier = new Barrier();8990private readonly _onDidChangeDefaultAccount = this._register(new Emitter<IDefaultAccount | null>());91readonly onDidChangeDefaultAccount = this._onDidChangeDefaultAccount.event;9293async getDefaultAccount(): Promise<IDefaultAccount | null> {94await this.initBarrier.wait();95return this.defaultAccount;96}9798setDefaultAccount(account: IDefaultAccount | null): void {99const oldAccount = this._defaultAccount;100this._defaultAccount = account;101102if (oldAccount !== this._defaultAccount) {103this._onDidChangeDefaultAccount.fire(this._defaultAccount);104}105106this.initBarrier.open();107}108109}110111export class NullDefaultAccountService extends Disposable implements IDefaultAccountService {112113declare _serviceBrand: undefined;114115readonly onDidChangeDefaultAccount = Event.None;116117async getDefaultAccount(): Promise<IDefaultAccount | null> {118return null;119}120121setDefaultAccount(account: IDefaultAccount | null): void {122// noop123}124125}126127export class DefaultAccountManagementContribution extends Disposable implements IWorkbenchContribution {128129static ID = 'workbench.contributions.defaultAccountManagement';130131private defaultAccount: IDefaultAccount | null = null;132private readonly accountStatusContext: IContextKey<string>;133134constructor(135@IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService,136@IConfigurationService private readonly configurationService: IConfigurationService,137@IAuthenticationService private readonly authenticationService: IAuthenticationService,138@IExtensionService private readonly extensionService: IExtensionService,139@IProductService private readonly productService: IProductService,140@IRequestService private readonly requestService: IRequestService,141@ILogService private readonly logService: ILogService,142@IContextKeyService contextKeyService: IContextKeyService,143) {144super();145this.accountStatusContext = CONTEXT_DEFAULT_ACCOUNT_STATE.bindTo(contextKeyService);146this.initialize();147}148149private async initialize(): Promise<void> {150if (!this.productService.defaultAccount) {151return;152}153154const { authenticationProvider, tokenEntitlementUrl, chatEntitlementUrl, mcpRegistryDataUrl } = this.productService.defaultAccount;155await this.extensionService.whenInstalledExtensionsRegistered();156157const declaredProvider = this.authenticationService.declaredProviders.find(provider => provider.id === authenticationProvider.id);158if (!declaredProvider) {159this.logService.info(`Default account authentication provider ${authenticationProvider} is not declared.`);160return;161}162163this.registerSignInAction(authenticationProvider.id, declaredProvider.label, authenticationProvider.enterpriseProviderId, authenticationProvider.enterpriseProviderConfig, authenticationProvider.scopes);164this.setDefaultAccount(await this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider.id, authenticationProvider.enterpriseProviderId, authenticationProvider.enterpriseProviderConfig, authenticationProvider.scopes, tokenEntitlementUrl, chatEntitlementUrl, mcpRegistryDataUrl));165166this._register(this.authenticationService.onDidChangeSessions(async e => {167if (e.providerId !== authenticationProvider.id && e.providerId !== authenticationProvider.enterpriseProviderId) {168return;169}170171if (this.defaultAccount && e.event.removed?.some(session => session.id === this.defaultAccount?.sessionId)) {172this.setDefaultAccount(null);173return;174}175this.setDefaultAccount(await this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider.id, authenticationProvider.enterpriseProviderId, authenticationProvider.enterpriseProviderConfig, authenticationProvider.scopes, tokenEntitlementUrl, chatEntitlementUrl, mcpRegistryDataUrl));176}));177178}179180private setDefaultAccount(account: IDefaultAccount | null): void {181this.defaultAccount = account;182this.defaultAccountService.setDefaultAccount(this.defaultAccount);183if (this.defaultAccount) {184this.accountStatusContext.set(DefaultAccountStatus.Available);185} else {186this.accountStatusContext.set(DefaultAccountStatus.Unavailable);187}188}189190private extractFromToken(token: string): Map<string, string> {191const result = new Map<string, string>();192const firstPart = token?.split(':')[0];193const fields = firstPart?.split(';');194for (const field of fields) {195const [key, value] = field.split('=');196result.set(key, value);197}198this.logService.trace(`DefaultAccount#extractFromToken: ${JSON.stringify(Object.fromEntries(result))}`);199return result;200}201202private async getDefaultAccountFromAuthenticatedSessions(authProviderId: string, enterpriseAuthProviderId: string, enterpriseAuthProviderConfig: string, scopes: string[], tokenEntitlementUrl: string, chatEntitlementUrl: string, mcpRegistryDataUrl: string): Promise<IDefaultAccount | null> {203const id = this.configurationService.getValue(enterpriseAuthProviderConfig) === enterpriseAuthProviderId ? enterpriseAuthProviderId : authProviderId;204const sessions = await this.authenticationService.getSessions(id, undefined, undefined, true);205const session = sessions.find(s => this.scopesMatch(s.scopes, scopes));206207if (!session) {208return null;209}210211const [chatEntitlements, tokenEntitlements] = await Promise.all([212this.getChatEntitlements(session.accessToken, chatEntitlementUrl),213this.getTokenEntitlements(session.accessToken, tokenEntitlementUrl),214]);215216const mcpRegistryProvider = this.productService.quality !== 'stable' && tokenEntitlements.mcp && this.configurationService.getValue<boolean>('chat.mcp.enterprise.registry.enabled') === true ? await this.getMcpRegistryProvider(session.accessToken, mcpRegistryDataUrl) : undefined;217218return {219sessionId: session.id,220enterprise: id === enterpriseAuthProviderId || session.account.label.includes('_'),221...chatEntitlements,222...tokenEntitlements,223mcpRegistryUrl: mcpRegistryProvider?.url,224mcpAccess: mcpRegistryProvider?.registry_access,225};226}227228private scopesMatch(scopes: ReadonlyArray<string>, expectedScopes: string[]): boolean {229return scopes.length === expectedScopes.length && expectedScopes.every(scope => scopes.includes(scope));230}231232private async getTokenEntitlements(accessToken: string, tokenEntitlementsUrl: string): Promise<Partial<IDefaultAccount>> {233if (!tokenEntitlementsUrl) {234return {};235}236237try {238const chatContext = await this.requestService.request({239type: 'GET',240url: tokenEntitlementsUrl,241disableCache: true,242headers: {243'Authorization': `Bearer ${accessToken}`244}245}, CancellationToken.None);246247const chatData = await asJson<ITokenEntitlementsResponse>(chatContext);248if (chatData) {249const tokenMap = this.extractFromToken(chatData.token);250return {251// Editor preview features are disabled if the flag is present and set to 0252chat_preview_features_enabled: tokenMap.get('editor_preview_features') !== '0',253chat_agent_enabled: tokenMap.get('agent_mode') !== '0',254// MCP is disabled if the flag is present and set to 0255mcp: tokenMap.get('mcp') !== '0',256};257}258this.logService.error('Failed to fetch token entitlements', 'No data returned');259} catch (error) {260this.logService.error('Failed to fetch token entitlements', getErrorMessage(error));261}262263return {};264}265266private async getChatEntitlements(accessToken: string, chatEntitlementsUrl: string): Promise<Partial<IChatEntitlementsResponse>> {267if (!chatEntitlementsUrl) {268return {};269}270271try {272const context = await this.requestService.request({273type: 'GET',274url: chatEntitlementsUrl,275disableCache: true,276headers: {277'Authorization': `Bearer ${accessToken}`278}279}, CancellationToken.None);280281const data = await asJson<IChatEntitlementsResponse>(context);282if (data) {283return data;284}285this.logService.error('Failed to fetch entitlements', 'No data returned');286} catch (error) {287this.logService.error('Failed to fetch entitlements', getErrorMessage(error));288}289return {};290}291292private async getMcpRegistryProvider(accessToken: string, mcpRegistryDataUrl: string): Promise<IMcpRegistryProvider | undefined> {293if (!mcpRegistryDataUrl) {294return undefined;295}296297try {298const context = await this.requestService.request({299type: 'GET',300url: mcpRegistryDataUrl,301disableCache: true,302headers: {303'Authorization': `Bearer ${accessToken}`304}305}, CancellationToken.None);306307const data = await asJson<IMcpRegistryResponse>(context);308if (data) {309this.logService.debug('Fetched MCP registry providers', data.mcp_registries);310return data.mcp_registries[0];311}312this.logService.error('Failed to fetch MCP registry providers', 'No data returned');313} catch (error) {314this.logService.error('Failed to fetch MCP registry providers', getErrorMessage(error));315}316return undefined;317}318319private registerSignInAction(authProviderId: string, authProviderLabel: string, enterpriseAuthProviderId: string, enterpriseAuthProviderConfig: string, scopes: string[]): void {320const that = this;321this._register(registerAction2(class extends Action2 {322constructor() {323super({324id: DEFAULT_ACCOUNT_SIGN_IN_COMMAND,325title: localize('sign in', "Sign in to {0}", authProviderLabel),326});327}328run(): Promise<any> {329const id = that.configurationService.getValue(enterpriseAuthProviderConfig) === enterpriseAuthProviderId ? enterpriseAuthProviderId : authProviderId;330return that.authenticationService.createSession(id, scopes);331}332}));333}334335}336337338