Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/microsoft-authentication/src/betterSecretStorage.ts
3316 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import Logger from './logger';
7
import { Event, EventEmitter, ExtensionContext, SecretStorage, SecretStorageChangeEvent } from 'vscode';
8
9
export interface IDidChangeInOtherWindowEvent<T> {
10
added: string[];
11
updated: string[];
12
removed: Array<{ key: string; value: T }>;
13
}
14
15
export class BetterTokenStorage<T> {
16
// set before and after _tokensPromise is set so getTokens can handle multiple operations.
17
private _operationInProgress = false;
18
// the current state. Don't use this directly and call getTokens() so that you ensure you
19
// have awaited for all operations.
20
private _tokensPromise: Promise<Map<string, T>> = Promise.resolve(new Map());
21
22
// The vscode SecretStorage instance for this extension.
23
private readonly _secretStorage: SecretStorage;
24
25
private _didChangeInOtherWindow = new EventEmitter<IDidChangeInOtherWindowEvent<T>>();
26
public onDidChangeInOtherWindow: Event<IDidChangeInOtherWindowEvent<T>> = this._didChangeInOtherWindow.event;
27
28
/**
29
*
30
* @param keylistKey The key in the secret storage that will hold the list of keys associated with this instance of BetterTokenStorage
31
* @param context the vscode Context used to register disposables and retreive the vscode.SecretStorage for this instance of VS Code
32
*/
33
constructor(private keylistKey: string, context: ExtensionContext) {
34
this._secretStorage = context.secrets;
35
context.subscriptions.push(context.secrets.onDidChange((e) => this.handleSecretChange(e)));
36
this.initialize();
37
}
38
39
private initialize(): void {
40
this._operationInProgress = true;
41
this._tokensPromise = new Promise((resolve, _) => {
42
this._secretStorage.get(this.keylistKey).then(
43
keyListStr => {
44
if (!keyListStr) {
45
resolve(new Map());
46
return;
47
}
48
49
const keyList: Array<string> = JSON.parse(keyListStr);
50
// Gather promises that contain key value pairs our of secret storage
51
const promises = keyList.map(key => new Promise<{ key: string; value: string | undefined }>((res, rej) => {
52
this._secretStorage.get(key).then((value) => {
53
res({ key, value });
54
}, rej);
55
}));
56
Promise.allSettled(promises).then((results => {
57
const tokens = new Map<string, T>();
58
results.forEach(p => {
59
if (p.status === 'fulfilled' && p.value.value) {
60
const secret = this.parseSecret(p.value.value);
61
tokens.set(p.value.key, secret);
62
} else if (p.status === 'rejected') {
63
Logger.error(p.reason);
64
} else {
65
Logger.error('Key was not found in SecretStorage.');
66
}
67
});
68
resolve(tokens);
69
}));
70
},
71
err => {
72
Logger.error(err);
73
resolve(new Map());
74
});
75
});
76
this._operationInProgress = false;
77
}
78
79
async get(key: string): Promise<T | undefined> {
80
const tokens = await this.getTokens();
81
return tokens.get(key);
82
}
83
84
async getAll(predicate?: (item: T) => boolean): Promise<T[]> {
85
const tokens = await this.getTokens();
86
const values = new Array<T>();
87
for (const [_, value] of tokens) {
88
if (!predicate || predicate(value)) {
89
values.push(value);
90
}
91
}
92
return values;
93
}
94
95
async store(key: string, value: T): Promise<void> {
96
const tokens = await this.getTokens();
97
98
const isAddition = !tokens.has(key);
99
tokens.set(key, value);
100
const valueStr = this.serializeSecret(value);
101
this._operationInProgress = true;
102
this._tokensPromise = new Promise((resolve, _) => {
103
const promises = [this._secretStorage.store(key, valueStr)];
104
105
// if we are adding a secret we need to update the keylist too
106
if (isAddition) {
107
promises.push(this.updateKeyList(tokens));
108
}
109
110
Promise.allSettled(promises).then(results => {
111
results.forEach(r => {
112
if (r.status === 'rejected') {
113
Logger.error(r.reason);
114
}
115
});
116
resolve(tokens);
117
});
118
});
119
this._operationInProgress = false;
120
}
121
122
async delete(key: string): Promise<void> {
123
const tokens = await this.getTokens();
124
if (!tokens.has(key)) {
125
return;
126
}
127
tokens.delete(key);
128
129
this._operationInProgress = true;
130
this._tokensPromise = new Promise((resolve, _) => {
131
Promise.allSettled([
132
this._secretStorage.delete(key),
133
this.updateKeyList(tokens)
134
]).then(results => {
135
results.forEach(r => {
136
if (r.status === 'rejected') {
137
Logger.error(r.reason);
138
}
139
});
140
resolve(tokens);
141
});
142
});
143
this._operationInProgress = false;
144
}
145
146
async deleteAll(predicate?: (item: T) => boolean): Promise<void> {
147
const tokens = await this.getTokens();
148
const promises = [];
149
for (const [key, value] of tokens) {
150
if (!predicate || predicate(value)) {
151
promises.push(this.delete(key));
152
}
153
}
154
await Promise.all(promises);
155
}
156
157
private async updateKeyList(tokens: Map<string, T>) {
158
const keyList = [];
159
for (const [key] of tokens) {
160
keyList.push(key);
161
}
162
163
const keyListStr = JSON.stringify(keyList);
164
await this._secretStorage.store(this.keylistKey, keyListStr);
165
}
166
167
protected parseSecret(secret: string): T {
168
return JSON.parse(secret);
169
}
170
171
protected serializeSecret(secret: T): string {
172
return JSON.stringify(secret);
173
}
174
175
// This is the one way to get tokens to ensure all other operations that
176
// came before you have been processed.
177
private async getTokens(): Promise<Map<string, T>> {
178
let tokens;
179
do {
180
tokens = await this._tokensPromise;
181
} while (this._operationInProgress);
182
return tokens;
183
}
184
185
// This is a crucial function that handles whether or not the token has changed in
186
// a different window of VS Code and sends the necessary event if it has.
187
// Scenarios this should cover:
188
// * Added in another window
189
// * Updated in another window
190
// * Deleted in another window
191
// * Added in this window
192
// * Updated in this window
193
// * Deleted in this window
194
private async handleSecretChange(e: SecretStorageChangeEvent) {
195
const key = e.key;
196
197
// The KeyList is only a list of keys to aid initial start up of VS Code to know which
198
// Keys are associated with this handler.
199
if (key === this.keylistKey) {
200
return;
201
}
202
const tokens = await this.getTokens();
203
204
this._operationInProgress = true;
205
this._tokensPromise = new Promise((resolve, _) => {
206
this._secretStorage.get(key).then(
207
storageSecretStr => {
208
if (!storageSecretStr) {
209
// true -> secret was deleted in another window
210
// false -> secret was deleted in this window
211
if (tokens.has(key)) {
212
const value = tokens.get(key)!;
213
tokens.delete(key);
214
this._didChangeInOtherWindow.fire({ added: [], updated: [], removed: [{ key, value }] });
215
}
216
return tokens;
217
}
218
219
const storageSecret = this.parseSecret(storageSecretStr);
220
const cachedSecret = tokens.get(key);
221
222
if (!cachedSecret) {
223
// token was added in another window
224
tokens.set(key, storageSecret);
225
this._didChangeInOtherWindow.fire({ added: [key], updated: [], removed: [] });
226
return tokens;
227
}
228
229
const cachedSecretStr = this.serializeSecret(cachedSecret);
230
if (storageSecretStr !== cachedSecretStr) {
231
// token was updated in another window
232
tokens.set(key, storageSecret);
233
this._didChangeInOtherWindow.fire({ added: [], updated: [key], removed: [] });
234
}
235
236
// what's in our token cache and what's in storage must be the same
237
// which means this should cover the last two scenarios of
238
// Added in this window & Updated in this window.
239
return tokens;
240
},
241
err => {
242
Logger.error(err);
243
return tokens;
244
}).then(resolve);
245
});
246
this._operationInProgress = false;
247
}
248
}
249
250