Skip to content

Commit

Permalink
Support new authentication API matching VS Code 1.63.1 (eclipse-theia…
Browse files Browse the repository at this point in the history
…#10709)

- Stay compatible with newer and previous authentication API
-- Keep prev authentication API matching VS Code 1.53.0-next.ea1b3f27db
-- Add newer, stable authentication API to existing types
-- Remove matching stable API from proposed API
-- Bridge API version gap in plugin context and with merged types

- Add support for 'onAuthenticationRequest' activation event
-- Allow dedicated trigger through 'ensureProvider' call
-- Trigger activation if session of provider is requested
  • Loading branch information
martin-fleck-at authored and thegecko committed Feb 17, 2022
1 parent 92accc1 commit cb7bdd6
Show file tree
Hide file tree
Showing 13 changed files with 563 additions and 206 deletions.
158 changes: 101 additions & 57 deletions packages/core/src/browser/authentication-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,17 @@ import { StorageService } from '../browser/storage-service';
import { Disposable, DisposableCollection } from '../common/disposable';
import { ACCOUNTS_MENU, ACCOUNTS_SUBMENU, MenuModelRegistry } from '../common/menu';
import { Command, CommandRegistry } from '../common/command';
import { nls } from '../common/nls';

export interface AuthenticationSessionsChangeEvent {
added: ReadonlyArray<string>;
removed: ReadonlyArray<string>;
changed: ReadonlyArray<string>;
export interface AuthenticationSessionAccountInformation {
readonly id: string;
readonly label: string;
}

export interface AuthenticationSession {
id: string;
accessToken: string;
account: {
label: string;
id: string;
}
account: AuthenticationSessionAccountInformation;
scopes: ReadonlyArray<string>;
}

Expand All @@ -48,6 +45,13 @@ export interface AuthenticationProviderInformation {
label: string;
}

/** Should match the definition from the theia/vscode types */
export interface AuthenticationProviderAuthenticationSessionsChangeEvent {
readonly added: ReadonlyArray<AuthenticationSession | string | undefined>;
readonly removed: ReadonlyArray<AuthenticationSession | string | undefined>;
readonly changed: ReadonlyArray<AuthenticationSession | string | undefined>;
}

export interface SessionRequest {
disposables: Disposable[];
requestingExtensionIds: string[];
Expand All @@ -57,6 +61,7 @@ export interface SessionRequestInfo {
[scopes: string]: SessionRequest;
}

/** Should match the definition from the theia/vscode types */
export interface AuthenticationProvider {
id: string;

Expand All @@ -68,13 +73,40 @@ export interface AuthenticationProvider {

signOut(accountName: string): Promise<void>;

getSessions(): Promise<ReadonlyArray<AuthenticationSession>>;
getSessions(scopes?: string[]): Promise<ReadonlyArray<AuthenticationSession>>;

updateSessionItems(event: AuthenticationSessionsChangeEvent): Promise<void>;
updateSessionItems(event: AuthenticationProviderAuthenticationSessionsChangeEvent): Promise<void>;

login(scopes: string[]): Promise<AuthenticationSession>;

logout(sessionId: string): Promise<void>;

/**
* An [event](#Event) which fires when the array of sessions has changed, or data
* within a session has changed.
*/
readonly onDidChangeSessions: Omit<Event<AuthenticationProviderAuthenticationSessionsChangeEvent>, 'maxListeners'>;

/**
* Get a list of sessions.
* @param scopes An optional list of scopes. If provided, the sessions returned should match
* these permissions, otherwise all sessions should be returned.
* @returns A promise that resolves to an array of authentication sessions.
*/
getSessions(scopes?: string[]): Thenable<ReadonlyArray<AuthenticationSession>>;

/**
* Prompts a user to login.
* @param scopes A list of scopes, permissions, that the new session should be created with.
* @returns A promise that resolves to an authentication session.
*/
createSession(scopes: string[]): Thenable<AuthenticationSession>;

/**
* Removes the session corresponding to session id.
* @param sessionId The id of the session to remove.
*/
removeSession(sessionId: string): Thenable<void>;
}
export const AuthenticationService = Symbol('AuthenticationService');

Expand All @@ -84,13 +116,13 @@ export interface AuthenticationService {
registerAuthenticationProvider(id: string, provider: AuthenticationProvider): void;
unregisterAuthenticationProvider(id: string): void;
requestNewSession(id: string, scopes: string[], extensionId: string, extensionName: string): void;
updateSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void;
updateSessions(providerId: string, event: AuthenticationProviderAuthenticationSessionsChangeEvent): void;

readonly onDidRegisterAuthenticationProvider: Event<AuthenticationProviderInformation>;
readonly onDidUnregisterAuthenticationProvider: Event<AuthenticationProviderInformation>;

readonly onDidChangeSessions: Event<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }>;
getSessions(providerId: string): Promise<ReadonlyArray<AuthenticationSession>>;
readonly onDidChangeSessions: Event<{ providerId: string, label: string, event: AuthenticationProviderAuthenticationSessionsChangeEvent }>;
getSessions(providerId: string, scopes?: string[]): Promise<ReadonlyArray<AuthenticationSession>>;
getLabel(providerId: string): string;
supportsMultipleAccounts(providerId: string): boolean;
login(providerId: string, scopes: string[]): Promise<AuthenticationSession>;
Expand All @@ -99,72 +131,84 @@ export interface AuthenticationService {
signOutOfAccount(providerId: string, accountName: string): Promise<void>;
}

export interface SessionChangeEvent {
providerId: string,
label: string,
event: AuthenticationProviderAuthenticationSessionsChangeEvent
}

@injectable()
export class AuthenticationServiceImpl implements AuthenticationService {
private noAccountsMenuItem: Disposable | undefined;
private noAccountsCommand: Command = { id: 'noAccounts' };
private signInRequestItems = new Map<string, SessionRequestInfo>();
private sessionMap = new Map<string, DisposableCollection>();

private authenticationProviders: Map<string, AuthenticationProvider> = new Map<string, AuthenticationProvider>();
protected authenticationProviders: Map<string, AuthenticationProvider> = new Map<string, AuthenticationProvider>();

private onDidRegisterAuthenticationProviderEmitter: Emitter<AuthenticationProviderInformation> = new Emitter<AuthenticationProviderInformation>();
readonly onDidRegisterAuthenticationProvider: Event<AuthenticationProviderInformation> = this.onDidRegisterAuthenticationProviderEmitter.event;

private onDidUnregisterAuthenticationProviderEmitter: Emitter<AuthenticationProviderInformation> = new Emitter<AuthenticationProviderInformation>();
readonly onDidUnregisterAuthenticationProvider: Event<AuthenticationProviderInformation> = this.onDidUnregisterAuthenticationProviderEmitter.event;

private onDidChangeSessionsEmitter: Emitter<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }> =
new Emitter<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }>();
readonly onDidChangeSessions: Event<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }> = this.onDidChangeSessionsEmitter.event;
private onDidChangeSessionsEmitter: Emitter<SessionChangeEvent> = new Emitter<SessionChangeEvent>();
readonly onDidChangeSessions: Event<SessionChangeEvent> = this.onDidChangeSessionsEmitter.event;

@inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry;
@inject(CommandRegistry) protected readonly commands: CommandRegistry;
@inject(StorageService) protected readonly storageService: StorageService;

@postConstruct()
init(): void {
const disposableMap = new Map<string, DisposableCollection>();
this.onDidChangeSessions(async e => {
if (e.event.added.length > 0) {
const sessions = await this.getSessions(e.providerId);
sessions.forEach(session => {
if (sessions.find(s => disposableMap.get(s.id))) {
return;
}
const disposables = new DisposableCollection();
const commandId = `account-sign-out-${e.providerId}-${session.id}`;
const command = this.commands.registerCommand({ id: commandId }, {
execute: async () => {
this.signOutOfAccount(e.providerId, session.account.label);
}
});
const subSubMenuPath = [...ACCOUNTS_SUBMENU, 'account-sub-menu'];
this.menus.registerSubmenu(subSubMenuPath, `${session.account.label} (${e.label})`);
const menuAction = this.menus.registerMenuAction(subSubMenuPath, {
label: 'Sign Out',
commandId
});
disposables.push(menuAction);
disposables.push(command);
disposableMap.set(session.id, disposables);
});
}
if (e.event.removed.length > 0) {
e.event.removed.forEach(removed => {
const toDispose = disposableMap.get(removed);
if (toDispose) {
toDispose.dispose();
disposableMap.delete(removed);
}
});
}
});
this.onDidChangeSessions(event => this.handleSessionChange(event));
this.commands.registerCommand(this.noAccountsCommand, {
execute: () => { },
isEnabled: () => false
});
}

protected async handleSessionChange(changeEvent: SessionChangeEvent): Promise<void> {
if (changeEvent.event.added.length > 0) {
const sessions = await this.getSessions(changeEvent.providerId);
sessions.forEach(session => {
if (!this.sessionMap.get(session.id)) {
this.sessionMap.set(session.id, this.createAccountUi(changeEvent.providerId, changeEvent.label, session));
}
});
}
for (const removed of changeEvent.event.removed) {
const sessionId = typeof removed === 'string' ? removed : removed?.id;
if (sessionId) {
this.sessionMap.get(sessionId)?.dispose();
this.sessionMap.delete(sessionId);
}
}
}

protected createAccountUi(providerId: string, providerLabel: string, session: AuthenticationSession): DisposableCollection {
// unregister old commands and menus if present (there is only one per account but there may be several sessions per account)
const providerAccountId = `account-sign-out-${providerId}-${session.account.id}`;
this.commands.unregisterCommand(providerAccountId);

const providerAccountSubmenu = [...ACCOUNTS_SUBMENU, providerAccountId];
this.menus.unregisterMenuAction({ commandId: providerAccountId }, providerAccountSubmenu);

// register new command and menu entry for the sessions account
const disposables = new DisposableCollection();
disposables.push(this.commands.registerCommand({ id: providerAccountId }, {
execute: async () => {
this.signOutOfAccount(providerId, session.account.label);
}
}));
this.menus.registerSubmenu(providerAccountSubmenu, `${session.account.label} (${providerLabel})`);
disposables.push(this.menus.registerMenuAction(providerAccountSubmenu, {
label: nls.localizeByDefault('Sign Out'),
commandId: providerAccountId
}));
return disposables;
}

getProviderIds(): string[] {
const providerIds: string[] = [];
this.authenticationProviders.forEach(provider => {
Expand Down Expand Up @@ -219,7 +263,7 @@ export class AuthenticationServiceImpl implements AuthenticationService {
}
}

async updateSessions(id: string, event: AuthenticationSessionsChangeEvent): Promise<void> {
async updateSessions(id: string, event: AuthenticationProviderAuthenticationSessionsChangeEvent): Promise<void> {
const provider = this.authenticationProviders.get(id);
if (provider) {
await provider.updateSessionItems(event);
Expand Down Expand Up @@ -268,7 +312,7 @@ export class AuthenticationServiceImpl implements AuthenticationService {
this.onDidRegisterAuthenticationProvider(e => {
if (e.id === providerId) {
provider = this.authenticationProviders.get(providerId);
resolve();
resolve(undefined);
}
});
});
Expand Down Expand Up @@ -344,10 +388,10 @@ export class AuthenticationServiceImpl implements AuthenticationService {
}
}

async getSessions(id: string): Promise<ReadonlyArray<AuthenticationSession>> {
async getSessions(id: string, scopes?: string[]): Promise<ReadonlyArray<AuthenticationSession>> {
const authProvider = this.authenticationProviders.get(id);
if (authProvider) {
return authProvider.getSessions();
return authProvider.getSessions(scopes);
} else {
throw new Error(`No authentication provider '${id}' is currently registered.`);
}
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/common/promise-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ export function timeout(ms: number, token = CancellationToken.None): Promise<voi
return deferred.promise;
}

/**
* Creates a promise that is rejected after the given amount of time. A typical use case is to wait for another promise until a specified timeout using:
* ```
* Promise.race([ promiseToPerform, timeoutReject(timeout, 'Timeout error message') ]);
* ```
*
* @param ms timeout in milliseconds
* @param message error message on promise rejection
* @returns rejection promise
*/
export function timeoutReject<T>(ms: number, message?: string): Promise<T> {
const deferred = new Deferred<T>();
setTimeout(() => deferred.reject(new Error(message)), ms);
return deferred.promise;
}

export async function retry<T>(task: () => Promise<T>, retryDelay: number, retries: number): Promise<T> {
let lastError: Error | undefined;

Expand Down
3 changes: 2 additions & 1 deletion packages/monaco/src/browser/monaco-editor-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
import { ProtocolToMonacoConverter } from './protocol-to-monaco-converter';
import { FileSystemPreferences } from '@theia/filesystem/lib/browser';
import { MonacoQuickInputImplementation } from './monaco-quick-input-service';
import { timeoutReject } from '@theia/core/lib/common/promise-util';

export const MonacoEditorFactory = Symbol('MonacoEditorFactory');
export interface MonacoEditorFactory {
Expand Down Expand Up @@ -304,7 +305,7 @@ export class MonacoEditorProvider {
if (formatOnSave) {
const formatOnSaveTimeout = this.editorPreferences.get({ preferenceName: 'editor.formatOnSaveTimeout', overrideIdentifier }, undefined, uri)!;
await Promise.race([
new Promise((_, reject) => setTimeout(() => reject(new Error(`Aborted format on save after ${formatOnSaveTimeout}ms`)), formatOnSaveTimeout)),
timeoutReject(formatOnSaveTimeout, `Aborted format on save after ${formatOnSaveTimeout}ms`),
editor.runAction('editor.action.formatDocument')
]);
}
Expand Down
24 changes: 12 additions & 12 deletions packages/plugin-ext/src/common/plugin-api-rpc-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,22 +569,22 @@ export interface LinePreview {
character: number;
}

export interface AuthenticationSession {
id: string;
accessToken: string;
account: { id: string, label: string };
scopes: ReadonlyArray<string>;
/**
* @deprecated Use {@link theia.AuthenticationSession} instead.
*/
export interface AuthenticationSession extends theia.AuthenticationSession {
}

export interface AuthenticationSessionsChangeEvent {
added: ReadonlyArray<string>;
removed: ReadonlyArray<string>;
changed: ReadonlyArray<string>;
/**
* @deprecated Use {@link theia.AuthenticationProviderAuthenticationSessionsChangeEvent} instead.
*/
export interface AuthenticationSessionsChangeEvent extends theia.AuthenticationProviderAuthenticationSessionsChangeEvent {
}

export interface AuthenticationProviderInformation {
id: string;
label: string;
/**
* @deprecated Use {@link theia.AuthenticationProviderInformation} instead.
*/
export interface AuthenticationProviderInformation extends theia.AuthenticationProviderInformation {
}

export interface CommentOptions {
Expand Down
30 changes: 17 additions & 13 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@ import {
CallHierarchyDefinition,
CallHierarchyReference,
SearchInWorkspaceResult,
AuthenticationSession,
AuthenticationSessionsChangeEvent,
AuthenticationProviderInformation,
Comment,
CommentOptions,
CommentThreadCollapsibleState,
Expand All @@ -76,7 +73,12 @@ import {
} from './plugin-api-rpc-model';
import { ExtPluginApi } from './plugin-ext-api-contribution';
import { KeysToAnyValues, KeysToKeysToAnyValue } from './types';
import { CancellationToken, Progress, ProgressOptions } from '@theia/plugin';
import {
AuthenticationProviderAuthenticationSessionsChangeEvent,
CancellationToken,
Progress,
ProgressOptions,
} from '@theia/plugin';
import { DebuggerDescription } from '@theia/debug/lib/common/debug-service';
import { DebugProtocol } from 'vscode-debugprotocol';
import { SymbolInformation } from '@theia/core/shared/vscode-languageserver-protocol';
Expand Down Expand Up @@ -1838,21 +1840,23 @@ export interface TasksMain {
}

export interface AuthenticationExt {
$getSessions(id: string): Promise<ReadonlyArray<AuthenticationSession>>;
$login(id: string, scopes: string[]): Promise<AuthenticationSession>;
$logout(id: string, sessionId: string): Promise<void>;
$onDidChangeAuthenticationSessions(id: string, label: string, event: AuthenticationSessionsChangeEvent): Promise<void>;
$onDidChangeAuthenticationProviders(added: AuthenticationProviderInformation[], removed: AuthenticationProviderInformation[]): Promise<void>;
$getSessions(id: string, scopes?: string[]): Promise<ReadonlyArray<theia.AuthenticationSession>>;
$createSession(id: string, scopes: string[]): Promise<theia.AuthenticationSession>;
$removeSession(id: string, sessionId: string): Promise<void>;
$onDidChangeAuthenticationSessions(id: string, label: string): Promise<void>;
$onDidChangeAuthenticationProviders(added: theia.AuthenticationProviderInformation[], removed: theia.AuthenticationProviderInformation[]): Promise<void>;
$setProviders(providers: theia.AuthenticationProviderInformation[]): Promise<void>;
}

export interface AuthenticationMain {
$registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): void;
$unregisterAuthenticationProvider(id: string): void;
$getProviderIds(): Promise<string[]>;
$updateSessions(providerId: string, event: AuthenticationSessionsChangeEvent): void;
$getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string,
options: { createIfNone?: boolean, clearSessionPreference?: boolean }): Promise<theia.AuthenticationSession | undefined>;
$logout(providerId: string, sessionId: string): Promise<void>;
$ensureProvider(id: string): Promise<void>;
$sendDidChangeSessions(providerId: string, event: AuthenticationProviderAuthenticationSessionsChangeEvent): void;
$getSession(providerId: string, scopes: readonly string[], extensionId: string, extensionName: string,
options: theia.AuthenticationGetSessionOptions): Promise<theia.AuthenticationSession | undefined>;
$removeSession(providerId: string, sessionId: string): Promise<void>;
}

export interface RawColorInfo {
Expand Down
Loading

0 comments on commit cb7bdd6

Please sign in to comment.