diff --git a/packages/core/src/browser/authentication-service.ts b/packages/core/src/browser/authentication-service.ts index 79ee0f99ecad6..d7ce49a9b3d7a 100644 --- a/packages/core/src/browser/authentication-service.ts +++ b/packages/core/src/browser/authentication-service.ts @@ -28,6 +28,12 @@ import { ACCOUNTS_MENU, ACCOUNTS_SUBMENU, MenuModelRegistry } from '../common/me import { CommandRegistry } from '../common/command'; import { DisposableCollection } from '../common/disposable'; +export interface AuthenticationSessionsChangeEvent { + added: ReadonlyArray; + removed: ReadonlyArray; + changed: ReadonlyArray; +} + export interface AuthenticationSession { id: string; accessToken: string; @@ -38,12 +44,6 @@ export interface AuthenticationSession { scopes: ReadonlyArray; } -export interface AuthenticationSessionsChangeEvent { - added: ReadonlyArray; - removed: ReadonlyArray; - changed: ReadonlyArray; -} - export interface AuthenticationProviderInformation { id: string; label: string; @@ -85,7 +85,7 @@ export interface AuthenticationService { registerAuthenticationProvider(id: string, provider: AuthenticationProvider): void; unregisterAuthenticationProvider(id: string): void; requestNewSession(id: string, scopes: string[], extensionId: string, extensionName: string): void; - sessionsUpdate(providerId: string, event: AuthenticationSessionsChangeEvent): void; + updateSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void; readonly onDidRegisterAuthenticationProvider: Event; readonly onDidUnregisterAuthenticationProvider: Event; @@ -197,10 +197,14 @@ export class AuthenticationServiceImpl implements AuthenticationService { } registerAuthenticationProvider(id: string, authenticationProvider: AuthenticationProvider): void { + if (this.authenticationProviders.get(id)) { + throw new Error(`An authentication provider with id '${id}' is already registered.`); + } this.authenticationProviders.set(id, authenticationProvider); this.onDidRegisterAuthenticationProviderEmitter.fire({ id, label: authenticationProvider.label }); this.updateAccountsMenuItem(); + console.log(`An authentication provider with id '${id}' was registered.`); } unregisterAuthenticationProvider(id: string): void { @@ -209,10 +213,11 @@ export class AuthenticationServiceImpl implements AuthenticationService { this.authenticationProviders.delete(id); this.onDidUnregisterAuthenticationProviderEmitter.fire({ id, label: provider.label }); this.updateAccountsMenuItem(); + console.log(`An authentication provider with id '${id}' was unregistered.`); } } - async sessionsUpdate(id: string, event: AuthenticationSessionsChangeEvent): Promise { + async updateSessions(id: string, event: AuthenticationSessionsChangeEvent): Promise { const provider = this.authenticationProviders.get(id); if (provider) { await provider.updateSessionItems(event); @@ -290,11 +295,11 @@ export class AuthenticationServiceImpl implements AuthenticationService { const allowList = await readAllowedExtensions(this.storageService, providerId, session.account.label); if (!allowList.find(allowed => allowed.id === extensionId)) { allowList.push({ id: extensionId, name: extensionName }); - this.storageService.setData(`${providerId}-${session.account.label}`, JSON.stringify(allowList)); + this.storageService.setData(`authentication-trusted-extensions-${providerId}-${session.account.label}`, JSON.stringify(allowList)); } // And also set it as the preferred account for the extension - this.storageService.setData(`${extensionName}-${providerId}`, session.id); + this.storageService.setData(`authentication-${extensionName}-${providerId}`, session.id); } }); @@ -380,12 +385,12 @@ export interface AllowedExtension { export async function readAllowedExtensions(storageService: StorageService, providerId: string, accountName: string): Promise { let trustedExtensions: AllowedExtension[] = []; try { - const trustedExtensionSrc: string | undefined = await storageService.getData(`${providerId}-${accountName}`); + const trustedExtensionSrc: string | undefined = await storageService.getData(`authentication-trusted-extensions-${providerId}-${accountName}`); if (trustedExtensionSrc) { trustedExtensions = JSON.parse(trustedExtensionSrc); } } catch (err) { - console.log(err); + console.error(err); } return trustedExtensions; diff --git a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts index a3ae9663f6f03..82b712f9c1f4e 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts @@ -540,3 +540,21 @@ export interface LinePreview { text: string; character: number; } + +export interface AuthenticationSession { + id: string; + accessToken: string; + account: { id: string, label: string }; + scopes: ReadonlyArray; +} + +export interface AuthenticationSessionsChangeEvent { + added: ReadonlyArray; + removed: ReadonlyArray; + changed: ReadonlyArray; +} + +export interface AuthenticationProviderInformation { + id: string; + label: string; +} diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index a7fa8c8eb650d..e46808f9d7a92 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -64,7 +64,10 @@ import { SelectionRange, CallHierarchyDefinition, CallHierarchyReference, - SearchInWorkspaceResult + SearchInWorkspaceResult, + AuthenticationSession, + AuthenticationSessionsChangeEvent, + AuthenticationProviderInformation } from './plugin-api-rpc-model'; import { ExtPluginApi } from './plugin-ext-api-contribution'; import { KeysToAnyValues, KeysToKeysToAnyValue } from './types'; @@ -78,10 +81,6 @@ import { QuickTitleButton } from '@theia/core/lib/common/quick-open-model'; import * as files from '@theia/filesystem/lib/common/files'; import { BinaryBuffer } from '@theia/core/lib/common/buffer'; import { ResourceLabelFormatter } from '@theia/core/lib/common/label-protocol'; -import { - AuthenticationProviderInformation, - AuthenticationSessionsChangeEvent -} from '@theia/core/lib/browser/authentication-service'; export interface PreferenceData { [scope: number]: any; @@ -1503,30 +1502,25 @@ export interface TasksMain { } export interface AuthenticationExt { - $getSessions(id: string): Promise>; - $getSessionAccessToken(id: string, sessionId: string): Promise; - $login(id: string, scopes: string[]): Promise; + $getSessions(id: string): Promise>; + $login(id: string, scopes: string[]): Promise; $logout(id: string, sessionId: string): Promise; $onDidChangeAuthenticationSessions(id: string, label: string, event: AuthenticationSessionsChangeEvent): Promise; - $onDidChangeAuthenticationProviders(added: AuthenticationProviderInformation[], removed: theia.AuthenticationProviderInformation[]): Promise; + $onDidChangeAuthenticationProviders(added: AuthenticationProviderInformation[], removed: AuthenticationProviderInformation[]): Promise; } export interface AuthenticationMain { $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): void; $unregisterAuthenticationProvider(id: string): void; $getProviderIds(): Promise; - $sendDidChangeSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void; - $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, - options: { createIfNone?: boolean, clearSessionPreference?: boolean }): Promise; + $updateSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void; $selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, - potentialSessions: theia.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise; + potentialSessions: AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise; $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise; $loginPrompt(providerName: string, extensionName: string): Promise; $setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise; $requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; - $getSessions(providerId: string): Promise>; - $login(providerId: string, scopes: string[]): Promise; $logout(providerId: string, sessionId: string): Promise; } diff --git a/packages/plugin-ext/src/main/browser/authentication-main.ts b/packages/plugin-ext/src/main/browser/authentication-main.ts index 1a76260542ff5..26eaddcc2d829 100644 --- a/packages/plugin-ext/src/main/browser/authentication-main.ts +++ b/packages/plugin-ext/src/main/browser/authentication-main.ts @@ -28,11 +28,13 @@ import { StorageService } from '@theia/core/lib/browser'; import { AuthenticationProvider, AuthenticationService, - AuthenticationSession, - AuthenticationSessionsChangeEvent, readAllowedExtensions } from '@theia/core/lib/browser/authentication-service'; import { QuickPickItem, QuickPickService } from '@theia/core/lib/common/quick-pick-service'; +import { + AuthenticationSession, + AuthenticationSessionsChangeEvent +} from '../../common/plugin-api-rpc-model'; export class AuthenticationMainImpl implements AuthenticationMain { private readonly proxy: AuthenticationExt; @@ -62,7 +64,7 @@ export class AuthenticationMainImpl implements AuthenticationMain { } async $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): Promise { - const provider = new AuthenticationProviderImp(this.proxy, id, label, supportsMultipleAccounts, this.storageService, this.messageService); + const provider = new AuthenticationProviderImpl(this.proxy, id, label, supportsMultipleAccounts, this.storageService, this.messageService); this.authenticationService.registerAuthenticationProvider(id, provider); } @@ -70,16 +72,8 @@ export class AuthenticationMainImpl implements AuthenticationMain { this.authenticationService.unregisterAuthenticationProvider(id); } - async $sendDidChangeSessions(id: string, event: AuthenticationSessionsChangeEvent): Promise { - this.authenticationService.sessionsUpdate(id, event); - } - - $getSessions(id: string): Promise> { - return this.authenticationService.getSessions(id); - } - - $login(providerId: string, scopes: string[]): Promise { - return this.authenticationService.login(providerId, scopes); + async $updateSessions(id: string, event: AuthenticationSessionsChangeEvent): Promise { + this.authenticationService.updateSessions(id, event); } $logout(providerId: string, sessionId: string): Promise { @@ -90,43 +84,6 @@ export class AuthenticationMainImpl implements AuthenticationMain { return this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName); } - async $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, - options: { createIfNone: boolean, clearSessionPreference: boolean }): Promise { - const orderedScopes = scopes.sort().join(' '); - const sessions = (await this.$getSessions(providerId)).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes); - const label = this.authenticationService.getLabel(providerId); - - if (sessions.length) { - if (!this.authenticationService.supportsMultipleAccounts(providerId)) { - const session = sessions[0]; - const allowed = await this.$getSessionsPrompt(providerId, session.account.label, label, extensionId, extensionName); - if (allowed) { - return session; - } else { - throw new Error('User did not consent to login.'); - } - } - - // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid - const selected = await this.$selectSession(providerId, label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); - return sessions.find(session => session.id === selected.id); - } else { - if (options.createIfNone) { - const isAllowed = await this.$loginPrompt(label, extensionName); - if (!isAllowed) { - throw new Error('User did not consent to login.'); - } - - const session = await this.authenticationService.login(providerId, scopes); - await this.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id); - return session; - } else { - await this.$requestNewSession(providerId, scopes, extensionId, extensionName); - return undefined; - } - } - } - async $selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise { if (!potentialSessions.length) { @@ -134,9 +91,9 @@ export class AuthenticationMainImpl implements AuthenticationMain { } if (clearSessionPreference) { - await this.storageService.setData(`${extensionName}-${providerId}`, undefined); + await this.storageService.setData(`authentication-session-${extensionName}-${providerId}`, undefined); } else { - const existingSessionPreference = await this.storageService.getData(`${extensionName}-${providerId}`); + const existingSessionPreference = await this.storageService.getData(`authentication-session-${extensionName}-${providerId}`); if (existingSessionPreference) { const matchingSession = potentialSessions.find(session => session.id === existingSessionPreference); if (matchingSession) { @@ -169,10 +126,10 @@ export class AuthenticationMainImpl implements AuthenticationMain { const allowList = await readAllowedExtensions(this.storageService, providerId, accountName); if (!allowList.find(allowed => allowed.id === extensionId)) { allowList.push({ id: extensionId, name: extensionName }); - this.storageService.setData(`${providerId}-${accountName}`, JSON.stringify(allowList)); + this.storageService.setData(`authentication-trusted-extensions-${providerId}-${accountName}`, JSON.stringify(allowList)); } - this.storageService.setData(`${extensionName}-${providerId}`, session.id); + this.storageService.setData(`authentication-session-${extensionName}-${providerId}`, session.id); resolve(session); @@ -195,7 +152,7 @@ export class AuthenticationMainImpl implements AuthenticationMain { if (allow) { await addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); allowList.push({ id: extensionId, name: extensionName }); - this.storageService.setData(`${providerId}-${accountName}`, JSON.stringify(allowList)); + this.storageService.setData(`authentication-trusted-extensions-${providerId}-${accountName}`, JSON.stringify(allowList)); } return allow; @@ -210,13 +167,13 @@ export class AuthenticationMainImpl implements AuthenticationMain { const allowList = await readAllowedExtensions(this.storageService, providerId, accountName); if (!allowList.find(allowed => allowed.id === extensionId)) { allowList.push({ id: extensionId, name: extensionName }); - this.storageService.setData(`${providerId}-${accountName}`, JSON.stringify(allowList)); + this.storageService.setData(`authentication-trusted-extensions-${providerId}-${accountName}`, JSON.stringify(allowList)); } } } async function addAccountUsage(storageService: StorageService, providerId: string, accountName: string, extensionId: string, extensionName: string): Promise { - const accountKey = `${providerId}-${accountName}-usages`; + const accountKey = `authentication-${providerId}-${accountName}-usages`; const usages = await readAccountUsages(storageService, providerId, accountName); const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId); @@ -243,7 +200,7 @@ interface AccountUsage { lastUsed: number; } -export class AuthenticationProviderImp implements AuthenticationProvider { +export class AuthenticationProviderImpl implements AuthenticationProvider { private accounts = new Map(); // Map account name to session ids private sessions = new Map(); // Map account id to name @@ -322,7 +279,7 @@ export class AuthenticationProviderImp implements AuthenticationProvider { } async function readAccountUsages(storageService: StorageService, providerId: string, accountName: string): Promise { - const accountKey = `${providerId}-${accountName}-usages`; + const accountKey = `authentication-${providerId}-${accountName}-usages`; const storedUsages: string | undefined = await storageService.getData(accountKey); let usages: AccountUsage[] = []; if (storedUsages) { @@ -337,6 +294,6 @@ async function readAccountUsages(storageService: StorageService, providerId: str } function removeAccountUsage(storageService: StorageService, providerId: string, accountName: string): void { - const accountKey = `${providerId}-${accountName}-usages`; + const accountKey = `authentication-${providerId}-${accountName}-usages`; storageService.setData(accountKey, undefined); } diff --git a/packages/plugin-ext/src/plugin/authentication-ext.ts b/packages/plugin-ext/src/plugin/authentication-ext.ts index 10fe85bfea61a..0082a90f93fc6 100644 --- a/packages/plugin-ext/src/plugin/authentication-ext.ts +++ b/packages/plugin-ext/src/plugin/authentication-ext.ts @@ -29,7 +29,7 @@ import { import { RPCProtocol } from '../common/rpc-protocol'; import { Emitter, Event } from '@theia/core/lib/common/event'; import * as theia from '@theia/plugin'; -import { AuthenticationSessionsChangeEvent } from '@theia/core/lib/browser/authentication-service'; +import { AuthenticationSessionsChangeEvent } from '../common/plugin-api-rpc-model'; export class AuthenticationExtImpl implements AuthenticationExt { private proxy: AuthenticationMain; @@ -70,7 +70,7 @@ export class AuthenticationExtImpl implements AuthenticationExt { const extensionId = requestingExtension.model.id.toLowerCase(); if (!provider) { - return this.proxy.$getSession(providerId, scopes, extensionId, extensionName, options); + throw new Error(`An authentication provider with id '${providerId}' was not found.`); } const orderedScopes = scopes.sort().join(' '); @@ -134,7 +134,7 @@ export class AuthenticationExtImpl implements AuthenticationExt { } const listener = provider.onDidChangeSessions(e => { - this.proxy.$sendDidChangeSessions(provider.id, e); + this.proxy.$updateSessions(provider.id, e); }); this.proxy.$registerAuthenticationProvider(provider.id, provider.label, provider.supportsMultipleAccounts); @@ -183,21 +183,6 @@ export class AuthenticationExtImpl implements AuthenticationExt { throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } - async $getSessionAccessToken(providerId: string, sessionId: string): Promise { - const authProvider = this.authenticationProviders.get(providerId); - if (authProvider) { - const sessions = await authProvider.getSessions(); - const session = sessions.find(s => s.id === sessionId); - if (session) { - return session.accessToken; - } - - throw new Error(`Unable to find session with id: ${sessionId}`); - } - - throw new Error(`Unable to find authentication provider with handle: ${providerId}`); - } - $onDidChangeAuthenticationSessions(id: string, label: string, event: AuthenticationSessionsChangeEvent): Promise { this.onDidChangeSessionsEmitter.fire({ provider: { id, label }, ...event }); return Promise.resolve();