Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for newer authentication API matching VS Code 1.63.1 #10709

Merged
merged 1 commit into from
Feb 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1837,21 +1839,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