Path: blob/main/extensions/microsoft-authentication/src/common/accountAccess.ts
3320 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 { Disposable, Event, EventEmitter, LogOutputChannel, SecretStorage } from 'vscode';6import { AccountInfo } from '@azure/msal-node';78export interface IAccountAccess {9onDidAccountAccessChange: Event<void>;10isAllowedAccess(account: AccountInfo): boolean;11setAllowedAccess(account: AccountInfo, allowed: boolean): Promise<void>;12}1314export class ScopedAccountAccess implements IAccountAccess, Disposable {15private readonly _onDidAccountAccessChangeEmitter = new EventEmitter<void>();16readonly onDidAccountAccessChange = this._onDidAccountAccessChangeEmitter.event;1718private value = new Array<string>();1920private readonly _disposable: Disposable;2122private constructor(23private readonly _accountAccessSecretStorage: IAccountAccessSecretStorage,24disposables: Disposable[] = []25) {26this._disposable = Disposable.from(27...disposables,28this._onDidAccountAccessChangeEmitter,29this._accountAccessSecretStorage.onDidChange(() => this.update())30);31}3233static async create(34secretStorage: SecretStorage,35cloudName: string,36logger: LogOutputChannel,37migrations: { clientId: string; authority: string }[] | undefined,38): Promise<ScopedAccountAccess> {39const storage = await AccountAccessSecretStorage.create(secretStorage, cloudName, logger, migrations);40const access = new ScopedAccountAccess(storage, [storage]);41await access.initialize();42return access;43}4445dispose() {46this._disposable.dispose();47}4849private async initialize(): Promise<void> {50await this.update();51}5253isAllowedAccess(account: AccountInfo): boolean {54return this.value.includes(account.homeAccountId);55}5657async setAllowedAccess(account: AccountInfo, allowed: boolean): Promise<void> {58if (allowed) {59if (this.value.includes(account.homeAccountId)) {60return;61}62await this._accountAccessSecretStorage.store([...this.value, account.homeAccountId]);63return;64}65await this._accountAccessSecretStorage.store(this.value.filter(id => id !== account.homeAccountId));66}6768private async update() {69const current = new Set(this.value);70const value = await this._accountAccessSecretStorage.get();7172this.value = value ?? [];73if (current.size !== this.value.length || !this.value.every(id => current.has(id))) {74this._onDidAccountAccessChangeEmitter.fire();75}76}77}7879interface IAccountAccessSecretStorage {80get(): Promise<string[] | undefined>;81store(value: string[]): Thenable<void>;82delete(): Thenable<void>;83onDidChange: Event<void>;84}8586class AccountAccessSecretStorage implements IAccountAccessSecretStorage, Disposable {87private _disposable: Disposable;8889private readonly _onDidChangeEmitter = new EventEmitter<void>();90readonly onDidChange: Event<void> = this._onDidChangeEmitter.event;9192private readonly _key: string;9394private constructor(95private readonly _secretStorage: SecretStorage,96private readonly _cloudName: string,97private readonly _logger: LogOutputChannel,98private readonly _migrations?: { clientId: string; authority: string }[],99) {100this._key = `accounts-${this._cloudName}`;101102this._disposable = Disposable.from(103this._onDidChangeEmitter,104this._secretStorage.onDidChange(e => {105if (e.key === this._key) {106this._onDidChangeEmitter.fire();107}108})109);110}111112static async create(113secretStorage: SecretStorage,114cloudName: string,115logger: LogOutputChannel,116migrations?: { clientId: string; authority: string }[],117): Promise<AccountAccessSecretStorage> {118const storage = new AccountAccessSecretStorage(secretStorage, cloudName, logger, migrations);119await storage.initialize();120return storage;121}122123/**124* TODO: Remove this method after a release with the migration125*/126private async initialize(): Promise<void> {127if (!this._migrations) {128return;129}130const current = await this.get();131// If the secret storage already has the new key, we have already run the migration132if (current) {133return;134}135try {136const allValues = new Set<string>();137for (const { clientId, authority } of this._migrations) {138const oldKey = `accounts-${this._cloudName}-${clientId}-${authority}`;139const value = await this._secretStorage.get(oldKey);140if (value) {141const parsed = JSON.parse(value) as string[];142parsed.forEach(v => allValues.add(v));143}144}145if (allValues.size > 0) {146await this.store(Array.from(allValues));147}148} catch (e) {149// Migration is best effort150this._logger.error(`Failed to migrate account access secret storage: ${e}`);151}152}153154async get(): Promise<string[] | undefined> {155const value = await this._secretStorage.get(this._key);156if (!value) {157return undefined;158}159return JSON.parse(value);160}161162store(value: string[]): Thenable<void> {163return this._secretStorage.store(this._key, JSON.stringify(value));164}165166delete(): Thenable<void> {167return this._secretStorage.delete(this._key);168}169170dispose() {171this._disposable.dispose();172}173}174175176