diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 192b58dc438..8290cc50bc5 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -162,6 +162,9 @@ export type ServerResource = SyncResource | 'machines'; export interface IUserDataSyncStoreService { readonly _serviceBrand: undefined; readonly userDataSyncStore: IUserDataSyncStore | undefined; + readonly onTokenFailed: Event; + readonly onTokenSucceed: Event; + setAuthToken(token: string, type: string): void; read(resource: ServerResource, oldValue: IUserData | null): Promise; write(resource: ServerResource, content: string, ref: string | null): Promise; manifest(): Promise; diff --git a/src/vs/platform/userDataSync/common/userDataSyncAccount.ts b/src/vs/platform/userDataSync/common/userDataSyncAccount.ts index 987ccd23784..eade8a601b4 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncAccount.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncAccount.ts @@ -6,6 +6,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; +import { IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync'; export interface IUserDataSyncAccount { readonly authenticationProviderId: string; @@ -16,12 +17,11 @@ export const IUserDataSyncAccountService = createDecorator; readonly account: IUserDataSyncAccount | undefined; readonly onDidChangeAccount: Event; - updateAccount(userDataSyncAuthToken: IUserDataSyncAccount | undefined): Promise; + updateAccount(account: IUserDataSyncAccount | undefined): Promise; - readonly onTokenFailed: Event; - sendTokenFailed(): void; } export class UserDataSyncAccountService extends Disposable implements IUserDataSyncAccountService { @@ -33,19 +33,32 @@ export class UserDataSyncAccountService extends Disposable implements IUserDataS private _onDidChangeAccount = this._register(new Emitter()); readonly onDidChangeAccount = this._onDidChangeAccount.event; - private _onTokenFailed: Emitter = this._register(new Emitter()); - readonly onTokenFailed: Event = this._onTokenFailed.event; + private _onTokenFailed: Emitter = this._register(new Emitter()); + readonly onTokenFailed: Event = this._onTokenFailed.event; + + private wasTokenFailed: boolean = false; + + constructor( + @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService + ) { + super(); + this._register(userDataSyncStoreService.onTokenFailed(() => { + this.updateAccount(undefined); + this._onTokenFailed.fire(this.wasTokenFailed); + this.wasTokenFailed = true; + })); + this._register(userDataSyncStoreService.onTokenSucceed(() => this.wasTokenFailed = false)); + } async updateAccount(account: IUserDataSyncAccount | undefined): Promise { if (account && this._account ? account.token !== this._account.token || account.authenticationProviderId !== this._account.authenticationProviderId : account !== this._account) { this._account = account; + if (this._account) { + this.userDataSyncStoreService.setAuthToken(this._account.token, this._account.authenticationProviderId); + } this._onDidChangeAccount.fire(account); } } - sendTokenFailed(): void { - this.updateAccount(undefined); - this._onTokenFailed.fire(); - } } diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 102c3983b1d..1e82668fdca 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -195,7 +195,7 @@ export class UserDataSyncAccountServiceChannel implements IServerChannel { listen(_: unknown, event: string): Event { switch (event) { - case 'onDidChangeToken': return this.service.onDidChangeAccount; + case 'onDidChangeAccount': return this.service.onDidChangeAccount; case 'onTokenFailed': return this.service.onTokenFailed; } throw new Error(`Event not found: ${event}`); @@ -203,6 +203,7 @@ export class UserDataSyncAccountServiceChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { + case '_getInitialData': return Promise.resolve(this.service.account); case 'updateAccount': return this.service.updateAccount(args); } throw new Error('Invalid call'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index f50ab4f0019..320a11ed626 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -10,7 +10,6 @@ import { joinPath, relativePath } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; import { IProductService } from 'vs/platform/product/common/productService'; import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -19,6 +18,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { assign } from 'vs/base/common/objects'; import { generateUuid } from 'vs/base/common/uuid'; import { isWeb } from 'vs/base/common/platform'; +import { Emitter, Event } from 'vs/base/common/event'; const USER_SESSION_ID_KEY = 'sync.user-session-id'; const MACHINE_SESSION_ID_KEY = 'sync.machine-session-id'; @@ -30,14 +30,20 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn _serviceBrand: any; readonly userDataSyncStore: IUserDataSyncStore | undefined; + private authToken: { token: string, type: string } | undefined; private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>; private readonly session: RequestsSession; + private _onTokenFailed: Emitter = this._register(new Emitter()); + readonly onTokenFailed: Event = this._onTokenFailed.event; + + private _onTokenSucceed: Emitter = this._register(new Emitter()); + readonly onTokenSucceed: Event = this._onTokenSucceed.event; + constructor( @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService, @IRequestService private readonly requestService: IRequestService, - @IUserDataSyncAccountService private readonly authTokenService: IUserDataSyncAccountService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, @@ -62,6 +68,10 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService); } + setAuthToken(token: string, type: string): void { + this.authToken = { token, type }; + } + async getAllRefs(resource: ServerResource): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); @@ -228,15 +238,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } private async request(options: IRequestOptions, token: CancellationToken): Promise { - const authToken = this.authTokenService.account; - if (!authToken) { + if (!this.authToken) { throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized); } const commonHeaders = await this.commonHeadersPromise; options.headers = assign(options.headers || {}, commonHeaders, { - 'X-Account-Type': authToken.authenticationProviderId, - 'authorization': `Bearer ${authToken.token}`, + 'X-Account-Type': this.authToken.type, + 'authorization': `Bearer ${this.authToken.token}`, }); // Add session headers @@ -256,10 +265,13 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } if (context.res.statusCode === 401) { - this.authTokenService.sendTokenFailed(); + this.authToken = undefined; + this._onTokenFailed.fire(); throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized); } + this._onTokenSucceed.fire(); + if (context.res.statusCode === 410) { throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because the requested resource is not longer available (410).`, UserDataSyncErrorCode.Gone); } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index cc5e1e4eb5e..97f57166ec0 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -719,7 +719,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } async run(): Promise { try { - await that.userDataSyncWorkbenchService.pickAccount(); + await that.userDataSyncWorkbenchService.signIn(); } catch (e) { that.notificationService.error(e); } diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 5a569b0a3c7..e4d16d2f2b6 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -27,6 +27,7 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { Action } from 'vs/base/common/actions'; type UserAccountClassification = { id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' }; @@ -137,11 +138,12 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat this.authenticationService.onDidRegisterAuthenticationProvider, this.authenticationService.onDidUnregisterAuthenticationProvider, ), authenticationProviderId => this.isSupportedAuthenticationProviderId(authenticationProviderId)), - this.userDataSyncAccountService.onTokenFailed) + Event.filter(this.userDataSyncAccountService.onTokenFailed, isSuccessive => !isSuccessive)) (() => this.update())); this._register(Event.filter(this.authenticationService.onDidChangeSessions, e => this.isSupportedAuthenticationProviderId(e.providerId))(({ event }) => this.onDidChangeSessions(event))); this._register(this.storageService.onDidChangeStorage(e => this.onDidChangeStorage(e))); + this._register(Event.filter(this.userDataSyncAccountService.onTokenFailed, isSuccessive => isSuccessive)(() => this.onDidSuccessiveAuthFailures())); } private async update(): Promise { @@ -201,10 +203,6 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat const previous = this._accountStatus; this.logService.debug('Sync account status changed', previous, accountStatus); - if (previous === AccountStatus.Available && accountStatus === AccountStatus.Unavailable) { - this.turnoff(false); - } - this._accountStatus = accountStatus; this.accountStatusContext.set(accountStatus); this._onDidChangeAccountStatus.fire(accountStatus); @@ -283,7 +281,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat return account.sessionId === this.currentSessionId; } - async pickAccount(): Promise { + async signIn(): Promise { await this.pick(); } @@ -387,6 +385,20 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat await this.update(); } + private async onDidSuccessiveAuthFailures(): Promise { + this.telemetryService.publicLog2('sync/successiveAuthFailures'); + this.currentSessionId = undefined; + await this.update(); + + this.notificationService.notify({ + severity: Severity.Error, + message: localize('successive auth failures', "Preferences sync was turned off because of successive authorization failures. Please sign in again to continue synchronizing"), + actions: { + primary: [new Action('sign in', localize('sign in', "Sign in"), undefined, true, () => this.signIn())] + } + }); + } + private onDidChangeSessions(e: AuthenticationSessionsChangeEvent): void { if (this.currentSessionId && e.removed.includes(this.currentSessionId)) { this.currentSessionId = undefined; diff --git a/src/vs/workbench/services/userDataSync/common/userDataSync.ts b/src/vs/workbench/services/userDataSync/common/userDataSync.ts index 2ee77970650..a63eb22746f 100644 --- a/src/vs/workbench/services/userDataSync/common/userDataSync.ts +++ b/src/vs/workbench/services/userDataSync/common/userDataSync.ts @@ -28,7 +28,7 @@ export interface IUserDataSyncWorkbenchService { turnOn(): Promise; turnoff(everyWhere: boolean): Promise; - pickAccount(): Promise; + signIn(): Promise; } export function getSyncAreaLabel(source: SyncResource): string { diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncAccountService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncAccountService.ts index 05cf1a6bd65..4be8abf9893 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncAccountService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncAccountService.ts @@ -7,7 +7,7 @@ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Disposable } from 'vs/base/common/lifecycle'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Event, Emitter } from 'vs/base/common/event'; import { IUserDataSyncAccountService, IUserDataSyncAccount } from 'vs/platform/userDataSync/common/userDataSyncAccount'; export class UserDataSyncAccountService extends Disposable implements IUserDataSyncAccountService { @@ -16,30 +16,32 @@ export class UserDataSyncAccountService extends Disposable implements IUserDataS private readonly channel: IChannel; - private _token: IUserDataSyncAccount | undefined; - get account(): IUserDataSyncAccount | undefined { return this._token; } - private _onDidChangeToken = this._register(new Emitter()); - readonly onDidChangeAccount = this._onDidChangeToken.event; + private _account: IUserDataSyncAccount | undefined; + get account(): IUserDataSyncAccount | undefined { return this._account; } - private _onTokenFailed: Emitter = this._register(new Emitter()); - readonly onTokenFailed: Event = this._onTokenFailed.event; + get onTokenFailed(): Event { return this.channel.listen('onTokenFailed'); } + + private _onDidChangeAccount: Emitter = this._register(new Emitter()); + readonly onDidChangeAccount: Event = this._onDidChangeAccount.event; constructor( @ISharedProcessService sharedProcessService: ISharedProcessService, ) { super(); this.channel = sharedProcessService.getChannel('userDataSyncAccount'); - this._register(this.channel.listen('onTokenFailed')(_ => this.sendTokenFailed())); + this.channel.call('_getInitialData').then(account => { + this._account = account; + this._register(this.channel.listen('onDidChangeAccount')(account => { + this._account = account; + this._onDidChangeAccount.fire(account); + })); + }); } updateAccount(account: IUserDataSyncAccount | undefined): Promise { - this._token = account; return this.channel.call('updateAccount', account); } - sendTokenFailed(): void { - this._onTokenFailed.fire(); - } } registerSingleton(IUserDataSyncAccountService, UserDataSyncAccountService);