Path: blob/main/extensions/microsoft-authentication/src/betterSecretStorage.ts
3316 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import Logger from './logger';6import { Event, EventEmitter, ExtensionContext, SecretStorage, SecretStorageChangeEvent } from 'vscode';78export interface IDidChangeInOtherWindowEvent<T> {9added: string[];10updated: string[];11removed: Array<{ key: string; value: T }>;12}1314export class BetterTokenStorage<T> {15// set before and after _tokensPromise is set so getTokens can handle multiple operations.16private _operationInProgress = false;17// the current state. Don't use this directly and call getTokens() so that you ensure you18// have awaited for all operations.19private _tokensPromise: Promise<Map<string, T>> = Promise.resolve(new Map());2021// The vscode SecretStorage instance for this extension.22private readonly _secretStorage: SecretStorage;2324private _didChangeInOtherWindow = new EventEmitter<IDidChangeInOtherWindowEvent<T>>();25public onDidChangeInOtherWindow: Event<IDidChangeInOtherWindowEvent<T>> = this._didChangeInOtherWindow.event;2627/**28*29* @param keylistKey The key in the secret storage that will hold the list of keys associated with this instance of BetterTokenStorage30* @param context the vscode Context used to register disposables and retreive the vscode.SecretStorage for this instance of VS Code31*/32constructor(private keylistKey: string, context: ExtensionContext) {33this._secretStorage = context.secrets;34context.subscriptions.push(context.secrets.onDidChange((e) => this.handleSecretChange(e)));35this.initialize();36}3738private initialize(): void {39this._operationInProgress = true;40this._tokensPromise = new Promise((resolve, _) => {41this._secretStorage.get(this.keylistKey).then(42keyListStr => {43if (!keyListStr) {44resolve(new Map());45return;46}4748const keyList: Array<string> = JSON.parse(keyListStr);49// Gather promises that contain key value pairs our of secret storage50const promises = keyList.map(key => new Promise<{ key: string; value: string | undefined }>((res, rej) => {51this._secretStorage.get(key).then((value) => {52res({ key, value });53}, rej);54}));55Promise.allSettled(promises).then((results => {56const tokens = new Map<string, T>();57results.forEach(p => {58if (p.status === 'fulfilled' && p.value.value) {59const secret = this.parseSecret(p.value.value);60tokens.set(p.value.key, secret);61} else if (p.status === 'rejected') {62Logger.error(p.reason);63} else {64Logger.error('Key was not found in SecretStorage.');65}66});67resolve(tokens);68}));69},70err => {71Logger.error(err);72resolve(new Map());73});74});75this._operationInProgress = false;76}7778async get(key: string): Promise<T | undefined> {79const tokens = await this.getTokens();80return tokens.get(key);81}8283async getAll(predicate?: (item: T) => boolean): Promise<T[]> {84const tokens = await this.getTokens();85const values = new Array<T>();86for (const [_, value] of tokens) {87if (!predicate || predicate(value)) {88values.push(value);89}90}91return values;92}9394async store(key: string, value: T): Promise<void> {95const tokens = await this.getTokens();9697const isAddition = !tokens.has(key);98tokens.set(key, value);99const valueStr = this.serializeSecret(value);100this._operationInProgress = true;101this._tokensPromise = new Promise((resolve, _) => {102const promises = [this._secretStorage.store(key, valueStr)];103104// if we are adding a secret we need to update the keylist too105if (isAddition) {106promises.push(this.updateKeyList(tokens));107}108109Promise.allSettled(promises).then(results => {110results.forEach(r => {111if (r.status === 'rejected') {112Logger.error(r.reason);113}114});115resolve(tokens);116});117});118this._operationInProgress = false;119}120121async delete(key: string): Promise<void> {122const tokens = await this.getTokens();123if (!tokens.has(key)) {124return;125}126tokens.delete(key);127128this._operationInProgress = true;129this._tokensPromise = new Promise((resolve, _) => {130Promise.allSettled([131this._secretStorage.delete(key),132this.updateKeyList(tokens)133]).then(results => {134results.forEach(r => {135if (r.status === 'rejected') {136Logger.error(r.reason);137}138});139resolve(tokens);140});141});142this._operationInProgress = false;143}144145async deleteAll(predicate?: (item: T) => boolean): Promise<void> {146const tokens = await this.getTokens();147const promises = [];148for (const [key, value] of tokens) {149if (!predicate || predicate(value)) {150promises.push(this.delete(key));151}152}153await Promise.all(promises);154}155156private async updateKeyList(tokens: Map<string, T>) {157const keyList = [];158for (const [key] of tokens) {159keyList.push(key);160}161162const keyListStr = JSON.stringify(keyList);163await this._secretStorage.store(this.keylistKey, keyListStr);164}165166protected parseSecret(secret: string): T {167return JSON.parse(secret);168}169170protected serializeSecret(secret: T): string {171return JSON.stringify(secret);172}173174// This is the one way to get tokens to ensure all other operations that175// came before you have been processed.176private async getTokens(): Promise<Map<string, T>> {177let tokens;178do {179tokens = await this._tokensPromise;180} while (this._operationInProgress);181return tokens;182}183184// This is a crucial function that handles whether or not the token has changed in185// a different window of VS Code and sends the necessary event if it has.186// Scenarios this should cover:187// * Added in another window188// * Updated in another window189// * Deleted in another window190// * Added in this window191// * Updated in this window192// * Deleted in this window193private async handleSecretChange(e: SecretStorageChangeEvent) {194const key = e.key;195196// The KeyList is only a list of keys to aid initial start up of VS Code to know which197// Keys are associated with this handler.198if (key === this.keylistKey) {199return;200}201const tokens = await this.getTokens();202203this._operationInProgress = true;204this._tokensPromise = new Promise((resolve, _) => {205this._secretStorage.get(key).then(206storageSecretStr => {207if (!storageSecretStr) {208// true -> secret was deleted in another window209// false -> secret was deleted in this window210if (tokens.has(key)) {211const value = tokens.get(key)!;212tokens.delete(key);213this._didChangeInOtherWindow.fire({ added: [], updated: [], removed: [{ key, value }] });214}215return tokens;216}217218const storageSecret = this.parseSecret(storageSecretStr);219const cachedSecret = tokens.get(key);220221if (!cachedSecret) {222// token was added in another window223tokens.set(key, storageSecret);224this._didChangeInOtherWindow.fire({ added: [key], updated: [], removed: [] });225return tokens;226}227228const cachedSecretStr = this.serializeSecret(cachedSecret);229if (storageSecretStr !== cachedSecretStr) {230// token was updated in another window231tokens.set(key, storageSecret);232this._didChangeInOtherWindow.fire({ added: [], updated: [key], removed: [] });233}234235// what's in our token cache and what's in storage must be the same236// which means this should cover the last two scenarios of237// Added in this window & Updated in this window.238return tokens;239},240err => {241Logger.error(err);242return tokens;243}).then(resolve);244});245this._operationInProgress = false;246}247}248249250