From 92dafb390cdfed6abc029c833a53cb4be7caebbf Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 15 Aug 2019 13:13:58 +0200 Subject: [PATCH] web - implement credentials provider and add API --- .../workbench/api/browser/mainThreadKeytar.ts | 52 +++---- src/vs/workbench/browser/web.main.ts | 7 +- .../credentials/browser/credentialsService.ts | 132 ++++++++++++++++++ .../credentials/common/credentials.ts | 6 +- .../credentials/node/credentialsService.ts | 8 +- .../environment/browser/environmentService.ts | 18 +-- .../environment/common/environmentService.ts | 3 + .../fileUserDataProvider.test.ts | 6 +- src/vs/workbench/workbench.desktop.main.ts | 4 +- src/vs/workbench/workbench.web.api.ts | 6 + src/vs/workbench/workbench.web.main.ts | 1 + tslint.json | 1 + 12 files changed, 186 insertions(+), 58 deletions(-) create mode 100644 src/vs/workbench/services/credentials/browser/credentialsService.ts rename src/vs/{platform => workbench/services}/credentials/common/credentials.ts (84%) rename src/vs/{platform => workbench/services}/credentials/node/credentialsService.ts (80%) diff --git a/src/vs/workbench/api/browser/mainThreadKeytar.ts b/src/vs/workbench/api/browser/mainThreadKeytar.ts index fff0a902e2c..f07b7688801 100644 --- a/src/vs/workbench/api/browser/mainThreadKeytar.ts +++ b/src/vs/workbench/api/browser/mainThreadKeytar.ts @@ -5,49 +5,33 @@ import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { MainContext, MainThreadKeytarShape, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; @extHostNamedCustomer(MainContext.MainThreadKeytar) export class MainThreadKeytar implements MainThreadKeytarShape { - private readonly _credentialsService?: ICredentialsService; - constructor( _extHostContext: IExtHostContext, - @optional(ICredentialsService) credentialsService: ICredentialsService, - ) { - this._credentialsService = credentialsService; + @ICredentialsService private readonly _credentialsService: ICredentialsService, + ) { } + + async $getPassword(service: string, account: string): Promise { + return this._credentialsService.getPassword(service, account); + } + + async $setPassword(service: string, account: string, password: string): Promise { + return this._credentialsService.setPassword(service, account, password); + } + + async $deletePassword(service: string, account: string): Promise { + return this._credentialsService.deletePassword(service, account); + } + + async $findPassword(service: string): Promise { + return this._credentialsService.findPassword(service); } dispose(): void { // } - - async $getPassword(service: string, account: string): Promise { - if (this._credentialsService) { - return this._credentialsService.getPassword(service, account); - } - return null; - } - - async $setPassword(service: string, account: string, password: string): Promise { - if (this._credentialsService) { - return this._credentialsService.setPassword(service, account, password); - } - } - - async $deletePassword(service: string, account: string): Promise { - if (this._credentialsService) { - return this._credentialsService.deletePassword(service, account); - } - return false; - } - - async $findPassword(service: string): Promise { - if (this._credentialsService) { - return this._credentialsService.findPassword(service); - } - return null; - } } diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 909f7578e4c..812663e25d4 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -117,12 +117,7 @@ class CodeRendererMain extends Disposable { const payload = await this.resolveWorkspaceInitializationPayload(); // Environment - const environmentService = new BrowserWorkbenchEnvironmentService({ - workspaceId: payload.id, - remoteAuthority: this.configuration.remoteAuthority, - webviewEndpoint: this.configuration.webviewEndpoint, - connectionToken: this.configuration.connectionToken - }); + const environmentService = new BrowserWorkbenchEnvironmentService(payload.id, this.configuration); serviceCollection.set(IWorkbenchEnvironmentService, environmentService); // Product diff --git a/src/vs/workbench/services/credentials/browser/credentialsService.ts b/src/vs/workbench/services/credentials/browser/credentialsService.ts new file mode 100644 index 00000000000..b7613b9c23c --- /dev/null +++ b/src/vs/workbench/services/credentials/browser/credentialsService.ts @@ -0,0 +1,132 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; + +export interface ICredentialsProvider { + getPassword(service: string, account: string): Promise; + setPassword(service: string, account: string, password: string): Promise; + deletePassword(service: string, account: string): Promise; + findPassword(service: string): Promise; +} + +export class BrowserCredentialsService implements ICredentialsService { + + _serviceBrand!: ServiceIdentifier; + + private credentialsProvider: ICredentialsProvider; + + constructor(@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService) { + if (environmentService.options && environmentService.options.credentialsProvider) { + this.credentialsProvider = environmentService.options.credentialsProvider; + } else { + this.credentialsProvider = new LocalStorageCredentialsProvider(); + } + } + + async getPassword(service: string, account: string): Promise { + return this.credentialsProvider.getPassword(service, account); + } + + async setPassword(service: string, account: string, password: string): Promise { + return this.credentialsProvider.setPassword(service, account, password); + } + + async deletePassword(service: string, account: string): Promise { + return this.credentialsProvider.deletePassword(service, account); + } + + async findPassword(service: string): Promise { + return this.credentialsProvider.findPassword(service); + } +} + +interface ICredential { + service: string; + account: string; + password: string; +} + +class LocalStorageCredentialsProvider implements ICredentialsProvider { + + static readonly CREDENTIALS_OPENED_KEY = 'credentials.provider'; + + private _credentials: ICredential[]; + private get credentials(): ICredential[] { + if (!this._credentials) { + try { + const serializedCredentials = window.localStorage.getItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY); + if (serializedCredentials) { + this._credentials = JSON.parse(serializedCredentials); + } + } catch (error) { + // ignore + } + + if (!Array.isArray(this._credentials)) { + this._credentials = []; + } + } + + return this._credentials; + } + + private save(): void { + window.localStorage.setItem(LocalStorageCredentialsProvider.CREDENTIALS_OPENED_KEY, JSON.stringify(this.credentials)); + } + + async getPassword(service: string, account: string): Promise { + return this.doGetPassword(service, account); + } + + private async doGetPassword(service: string, account?: string): Promise { + for (const credential of this.credentials) { + if (credential.service === service) { + if (typeof account !== 'string' || account === credential.account) { + return credential.password; + } + } + } + + return null; + } + + async setPassword(service: string, account: string, password: string): Promise { + this.deletePassword(service, account); + + this.credentials.push({ service, account, password }); + + this.save(); + } + + async deletePassword(service: string, account: string): Promise { + let found = false; + + this._credentials = this.credentials.filter(credential => { + if (credential.service === service && credential.account === account) { + found = true; + + return false; + } + + return true; + }); + + if (found) { + this.save(); + } + + return found; + } + + async findPassword(service: string): Promise { + return this.doGetPassword(service); + } +} + +registerSingleton(ICredentialsService, BrowserCredentialsService, true); diff --git a/src/vs/platform/credentials/common/credentials.ts b/src/vs/workbench/services/credentials/common/credentials.ts similarity index 84% rename from src/vs/platform/credentials/common/credentials.ts rename to src/vs/workbench/services/credentials/common/credentials.ts index dc6618d89f7..8fa520c374e 100644 --- a/src/vs/platform/credentials/common/credentials.ts +++ b/src/vs/workbench/services/credentials/common/credentials.ts @@ -3,12 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; export const ICredentialsService = createDecorator('ICredentialsService'); export interface ICredentialsService { - _serviceBrand: any; + + _serviceBrand: ServiceIdentifier; + getPassword(service: string, account: string): Promise; setPassword(service: string, account: string, password: string): Promise; deletePassword(service: string, account: string): Promise; diff --git a/src/vs/platform/credentials/node/credentialsService.ts b/src/vs/workbench/services/credentials/node/credentialsService.ts similarity index 80% rename from src/vs/platform/credentials/node/credentialsService.ts rename to src/vs/workbench/services/credentials/node/credentialsService.ts index 282b761fe2e..1361f169be3 100644 --- a/src/vs/platform/credentials/node/credentialsService.ts +++ b/src/vs/workbench/services/credentials/node/credentialsService.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; +import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials'; import { IdleValue } from 'vs/base/common/async'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; type KeytarModule = { getPassword(service: string, account: string): Promise; @@ -15,7 +17,7 @@ type KeytarModule = { export class KeytarCredentialsService implements ICredentialsService { - _serviceBrand: any; + _serviceBrand!: ServiceIdentifier; private readonly _keytar = new IdleValue>(() => import('keytar')); @@ -39,3 +41,5 @@ export class KeytarCredentialsService implements ICredentialsService { return keytar.findPassword(service); } } + +registerSingleton(ICredentialsService, KeytarCredentialsService, true); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 250fd43582e..c8e2227fc0d 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IWindowConfiguration, IPath, IPathsToWaitFor } from 'vs/platform/windows/common/windows'; -import { IEnvironmentService, IExtensionHostDebugParams, IDebugParams, BACKUPS } from 'vs/platform/environment/common/environment'; +import { IExtensionHostDebugParams, IDebugParams, BACKUPS } from 'vs/platform/environment/common/environment'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; import { IProcessEnvironment } from 'vs/base/common/platform'; @@ -13,6 +13,8 @@ import { ExportData } from 'vs/base/common/performance'; import { LogLevel } from 'vs/platform/log/common/log'; import { joinPath } from 'vs/base/common/resources'; import { Schemas } from 'vs/base/common/network'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; export class BrowserWindowConfiguration implements IWindowConfiguration { @@ -66,26 +68,26 @@ export interface IBrowserWindowConfiguration { connectionToken?: string; } -export class BrowserWorkbenchEnvironmentService implements IEnvironmentService { +export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironmentService { - _serviceBrand!: ServiceIdentifier; + _serviceBrand!: ServiceIdentifier; readonly configuration: IWindowConfiguration = new BrowserWindowConfiguration(); - constructor(configuration: IBrowserWindowConfiguration) { + constructor(workspaceId: string, public readonly options: IWorkbenchConstructionOptions) { this.args = { _: [] }; this.appRoot = '/web/'; this.appNameLong = 'Visual Studio Code - Web'; - this.configuration.remoteAuthority = configuration.remoteAuthority; + this.configuration.remoteAuthority = options.remoteAuthority; this.userRoamingDataHome = URI.file('/User').with({ scheme: Schemas.userData }); this.settingsResource = joinPath(this.userRoamingDataHome, 'settings.json'); this.keybindingsResource = joinPath(this.userRoamingDataHome, 'keybindings.json'); this.keyboardLayoutResource = joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); this.localeResource = joinPath(this.userRoamingDataHome, 'locale.json'); this.backupHome = joinPath(this.userRoamingDataHome, BACKUPS); - this.configuration.backupWorkspaceResource = joinPath(this.backupHome, configuration.workspaceId); - this.configuration.connectionToken = configuration.connectionToken || this.getConnectionTokenFromLocation(); + this.configuration.backupWorkspaceResource = joinPath(this.backupHome, workspaceId); + this.configuration.connectionToken = options.connectionToken || this.getConnectionTokenFromLocation(); this.logsPath = '/web/logs'; @@ -94,7 +96,7 @@ export class BrowserWorkbenchEnvironmentService implements IEnvironmentService { break: false }; - this.webviewEndpoint = configuration.webviewEndpoint; + this.webviewEndpoint = options.webviewEndpoint; this.untitledWorkspacesHome = URI.from({ scheme: Schemas.untitled, path: 'Workspaces' }); if (document && document.location && document.location.search) { diff --git a/src/vs/workbench/services/environment/common/environmentService.ts b/src/vs/workbench/services/environment/common/environmentService.ts index fd4beaf134d..72c98796b9a 100644 --- a/src/vs/workbench/services/environment/common/environmentService.ts +++ b/src/vs/workbench/services/environment/common/environmentService.ts @@ -6,6 +6,7 @@ import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; export const IWorkbenchEnvironmentService = createDecorator('environmentService'); @@ -14,4 +15,6 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService { _serviceBrand: ServiceIdentifier; readonly configuration: IWindowConfiguration; + + readonly options?: IWorkbenchConstructionOptions; } diff --git a/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts b/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts index b8ba819bba8..854e9582f5a 100644 --- a/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts +++ b/src/vs/workbench/services/userData/test/electron-browser/fileUserDataProvider.test.ts @@ -47,7 +47,7 @@ suite('FileUserDataProvider', () => { userDataResource = URI.file(userDataPath).with({ scheme: Schemas.userData }); await Promise.all([pfs.mkdirp(userDataPath), pfs.mkdirp(backupsPath)]); - const environmentService = new BrowserWorkbenchEnvironmentService({ workspaceId: 'workspaceId' }); + const environmentService = new BrowserWorkbenchEnvironmentService('workspaceId', { remoteAuthority: 'remote' }); environmentService.userRoamingDataHome = userDataResource; const userDataFileSystemProvider = new FileUserDataProvider(URI.file(userDataPath), URI.file(backupsPath), diskFileSystemProvider, environmentService); @@ -321,7 +321,7 @@ suite('FileUserDataProvider - Watching', () => { localUserDataResource = URI.file(userDataPath); userDataResource = localUserDataResource.with({ scheme: Schemas.userData }); - const environmentService = new BrowserWorkbenchEnvironmentService({ workspaceId: 'workspaceId' }); + const environmentService = new BrowserWorkbenchEnvironmentService('workspaceId', { remoteAuthority: 'remote' }); environmentService.userRoamingDataHome = userDataResource; const userDataFileSystemProvider = new FileUserDataProvider(localUserDataResource, localBackupsResource, new TestFileSystemProvider(fileEventEmitter.event), environmentService); @@ -475,4 +475,4 @@ suite('FileUserDataProvider - Watching', () => { type: FileChangeType.DELETED }]); }); -}); \ No newline at end of file +}); diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index f6a48a02812..3373be0bd35 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -49,6 +49,7 @@ import 'vs/workbench/services/accessibility/node/accessibilityService'; import 'vs/workbench/services/remote/node/tunnelService'; import 'vs/workbench/services/backup/node/backupFileService'; import 'vs/workbench/services/opener/electron-browser/openerService'; +import 'vs/workbench/services/credentials/node/credentialsService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -74,8 +75,6 @@ import { IMenubarService } from 'vs/platform/menubar/common/menubar'; import { MenubarService } from 'vs/platform/menubar/electron-browser/menubarService'; import { IURLService } from 'vs/platform/url/common/url'; import { RelayURLService } from 'vs/platform/url/electron-browser/urlService'; -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; -import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; registerSingleton(IClipboardService, ClipboardService, true); registerSingleton(IRequestService, RequestService, true); @@ -89,7 +88,6 @@ registerSingleton(IIssueService, IssueService); registerSingleton(IWorkspacesService, WorkspacesService); registerSingleton(IMenubarService, MenubarService); registerSingleton(IURLService, RelayURLService); -registerSingleton(ICredentialsService, KeytarCredentialsService, true); //#endregion diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 15156252d69..2727fa5eb5d 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -8,6 +8,7 @@ import { main } from 'vs/workbench/browser/web.main'; import { UriComponents } from 'vs/base/common/uri'; import { IFileSystemProvider } from 'vs/platform/files/common/files'; import { IWebSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; +import { ICredentialsProvider } from 'vs/workbench/services/credentials/browser/credentialsService'; export interface IWorkbenchConstructionOptions { @@ -53,6 +54,11 @@ export interface IWorkbenchConstructionOptions { * Experimental: Whether to enable the smoke test driver. */ driver?: boolean; + + /** + * Experimental: The credentials provider to store and retrieve secrets. + */ + credentialsProvider?: ICredentialsProvider; } /** diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 8d06f34a906..40384b249b0 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -36,6 +36,7 @@ import 'vs/workbench/services/extensions/browser/extensionService'; import 'vs/workbench/services/extensionManagement/common/extensionManagementServerService'; import 'vs/workbench/services/telemetry/browser/telemetryService'; import 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; +import 'vs/workbench/services/credentials/browser/credentialsService'; import 'vs/workbench/browser/web.simpleservices'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; diff --git a/tslint.json b/tslint.json index 548b9a51191..4b23918f0fc 100644 --- a/tslint.json +++ b/tslint.json @@ -436,6 +436,7 @@ "**/vs/base/**/common/**", "**/vs/platform/**/common/**", "**/vs/editor/common/**", + "**/vs/workbench/workbench.web.api", "**/vs/workbench/common/**", "**/vs/workbench/services/**/common/**", "**/vs/workbench/api/**/common/**",