Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/microsoft-authentication/src/common/accountAccess.ts
3320 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 { Disposable, Event, EventEmitter, LogOutputChannel, SecretStorage } from 'vscode';
7
import { AccountInfo } from '@azure/msal-node';
8
9
export interface IAccountAccess {
10
onDidAccountAccessChange: Event<void>;
11
isAllowedAccess(account: AccountInfo): boolean;
12
setAllowedAccess(account: AccountInfo, allowed: boolean): Promise<void>;
13
}
14
15
export class ScopedAccountAccess implements IAccountAccess, Disposable {
16
private readonly _onDidAccountAccessChangeEmitter = new EventEmitter<void>();
17
readonly onDidAccountAccessChange = this._onDidAccountAccessChangeEmitter.event;
18
19
private value = new Array<string>();
20
21
private readonly _disposable: Disposable;
22
23
private constructor(
24
private readonly _accountAccessSecretStorage: IAccountAccessSecretStorage,
25
disposables: Disposable[] = []
26
) {
27
this._disposable = Disposable.from(
28
...disposables,
29
this._onDidAccountAccessChangeEmitter,
30
this._accountAccessSecretStorage.onDidChange(() => this.update())
31
);
32
}
33
34
static async create(
35
secretStorage: SecretStorage,
36
cloudName: string,
37
logger: LogOutputChannel,
38
migrations: { clientId: string; authority: string }[] | undefined,
39
): Promise<ScopedAccountAccess> {
40
const storage = await AccountAccessSecretStorage.create(secretStorage, cloudName, logger, migrations);
41
const access = new ScopedAccountAccess(storage, [storage]);
42
await access.initialize();
43
return access;
44
}
45
46
dispose() {
47
this._disposable.dispose();
48
}
49
50
private async initialize(): Promise<void> {
51
await this.update();
52
}
53
54
isAllowedAccess(account: AccountInfo): boolean {
55
return this.value.includes(account.homeAccountId);
56
}
57
58
async setAllowedAccess(account: AccountInfo, allowed: boolean): Promise<void> {
59
if (allowed) {
60
if (this.value.includes(account.homeAccountId)) {
61
return;
62
}
63
await this._accountAccessSecretStorage.store([...this.value, account.homeAccountId]);
64
return;
65
}
66
await this._accountAccessSecretStorage.store(this.value.filter(id => id !== account.homeAccountId));
67
}
68
69
private async update() {
70
const current = new Set(this.value);
71
const value = await this._accountAccessSecretStorage.get();
72
73
this.value = value ?? [];
74
if (current.size !== this.value.length || !this.value.every(id => current.has(id))) {
75
this._onDidAccountAccessChangeEmitter.fire();
76
}
77
}
78
}
79
80
interface IAccountAccessSecretStorage {
81
get(): Promise<string[] | undefined>;
82
store(value: string[]): Thenable<void>;
83
delete(): Thenable<void>;
84
onDidChange: Event<void>;
85
}
86
87
class AccountAccessSecretStorage implements IAccountAccessSecretStorage, Disposable {
88
private _disposable: Disposable;
89
90
private readonly _onDidChangeEmitter = new EventEmitter<void>();
91
readonly onDidChange: Event<void> = this._onDidChangeEmitter.event;
92
93
private readonly _key: string;
94
95
private constructor(
96
private readonly _secretStorage: SecretStorage,
97
private readonly _cloudName: string,
98
private readonly _logger: LogOutputChannel,
99
private readonly _migrations?: { clientId: string; authority: string }[],
100
) {
101
this._key = `accounts-${this._cloudName}`;
102
103
this._disposable = Disposable.from(
104
this._onDidChangeEmitter,
105
this._secretStorage.onDidChange(e => {
106
if (e.key === this._key) {
107
this._onDidChangeEmitter.fire();
108
}
109
})
110
);
111
}
112
113
static async create(
114
secretStorage: SecretStorage,
115
cloudName: string,
116
logger: LogOutputChannel,
117
migrations?: { clientId: string; authority: string }[],
118
): Promise<AccountAccessSecretStorage> {
119
const storage = new AccountAccessSecretStorage(secretStorage, cloudName, logger, migrations);
120
await storage.initialize();
121
return storage;
122
}
123
124
/**
125
* TODO: Remove this method after a release with the migration
126
*/
127
private async initialize(): Promise<void> {
128
if (!this._migrations) {
129
return;
130
}
131
const current = await this.get();
132
// If the secret storage already has the new key, we have already run the migration
133
if (current) {
134
return;
135
}
136
try {
137
const allValues = new Set<string>();
138
for (const { clientId, authority } of this._migrations) {
139
const oldKey = `accounts-${this._cloudName}-${clientId}-${authority}`;
140
const value = await this._secretStorage.get(oldKey);
141
if (value) {
142
const parsed = JSON.parse(value) as string[];
143
parsed.forEach(v => allValues.add(v));
144
}
145
}
146
if (allValues.size > 0) {
147
await this.store(Array.from(allValues));
148
}
149
} catch (e) {
150
// Migration is best effort
151
this._logger.error(`Failed to migrate account access secret storage: ${e}`);
152
}
153
}
154
155
async get(): Promise<string[] | undefined> {
156
const value = await this._secretStorage.get(this._key);
157
if (!value) {
158
return undefined;
159
}
160
return JSON.parse(value);
161
}
162
163
store(value: string[]): Thenable<void> {
164
return this._secretStorage.store(this._key, JSON.stringify(value));
165
}
166
167
delete(): Thenable<void> {
168
return this._secretStorage.delete(this._key);
169
}
170
171
dispose() {
172
this._disposable.dispose();
173
}
174
}
175
176