From b445cded3489a973bdd388cc14db7cccef7e2b4a Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 18 Aug 2017 14:47:45 -0700 Subject: [PATCH] Include azure-account extension --- build/npm/postinstall.js | 1 + extensions/azure-account/.vscodeignore | 4 + extensions/azure-account/package.json | 70 +++ extensions/azure-account/package.nls.json | 8 + extensions/azure-account/src/azure-account.ts | 471 ++++++++++++++++++ extensions/azure-account/src/extension.ts | 53 ++ .../src/typings/azure-account.api.d.ts | 34 ++ extensions/azure-account/src/typings/ref.d.ts | 6 + .../src/typings/vscode.rejected.d.ts | 43 ++ extensions/azure-account/tsconfig.json | 19 + 10 files changed, 709 insertions(+) create mode 100644 extensions/azure-account/.vscodeignore create mode 100644 extensions/azure-account/package.json create mode 100644 extensions/azure-account/package.nls.json create mode 100644 extensions/azure-account/src/azure-account.ts create mode 100644 extensions/azure-account/src/extension.ts create mode 100644 extensions/azure-account/src/typings/azure-account.api.d.ts create mode 100644 extensions/azure-account/src/typings/ref.d.ts create mode 100644 extensions/azure-account/src/typings/vscode.rejected.d.ts create mode 100644 extensions/azure-account/tsconfig.json diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 7b156212bec..95205aa1fe1 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -24,6 +24,7 @@ npmInstall('extensions'); // node modules shared by all extensions const extensions = [ 'vscode-api-tests', 'vscode-colorize-tests', + 'azure-account', 'json', 'configuration-editing', 'extension-editing', diff --git a/extensions/azure-account/.vscodeignore b/extensions/azure-account/.vscodeignore new file mode 100644 index 00000000000..24428a6f758 --- /dev/null +++ b/extensions/azure-account/.vscodeignore @@ -0,0 +1,4 @@ +test/** +src/** +tsconfig.json +npm-shrinkwrap.json \ No newline at end of file diff --git a/extensions/azure-account/package.json b/extensions/azure-account/package.json new file mode 100644 index 00000000000..d98aa059e74 --- /dev/null +++ b/extensions/azure-account/package.json @@ -0,0 +1,70 @@ +{ + "name": "azure-account", + "version": "0.1.0", + "publisher": "vscode", + "engines": { + "vscode": "*" + }, + "enableProposedApi": true, + "activationEvents": [ + "*" + ], + "main": "./out/extension", + "contributes": { + "commands": [ + { + "command": "azure-account.login", + "title": "%azure-account.commands.login%", + "category": "%azure-account.commands.azure%" + }, + { + "command": "azure-account.logout", + "title": "%azure-account.commands.logout%", + "category": "%azure-account.commands.azure%" + }, + { + "command": "azure-account.addFilter", + "title": "%azure-account.commands.addResourceFilter%", + "category": "%azure-account.commands.azure%" + }, + { + "command": "azure-account.removeFilter", + "title": "%azure-account.commands.removeResourceFilter%", + "category": "%azure-account.commands.azure%" + }, + { + "command": "azure-account.createAccount", + "title": "%azure-account.commands.createAccount%", + "category": "%azure-account.commands.azure%" + } + ], + "configuration": { + "type": "object", + "title": "Azure configuration", + "properties": { + "azure.resourceFilter": { + "type": "array", + "default": null, + "description": "The resource filter, each element is either a subscription id or a subscription id and a resource group name separated by a slash." + } + } + } + }, + "scripts": { + "compile": "gulp compile-extension:azure-account", + "watch": "gulp watch-extension:azure-account" + }, + "devDependencies": { + "@types/copy-paste": "^1.1.30", + "@types/node": "^6.0.40", + "@types/opn": "^3.0.28" + }, + "dependencies": { + "adal-node": "^0.1.22", + "azure-arm-resource": "^2.0.0-preview", + "copy-paste": "^1.3.0", + "ms-rest-azure": "^2.2.3", + "vscode-nls": "^2.0.2", + "opn": "^5.1.0" + } +} \ No newline at end of file diff --git a/extensions/azure-account/package.nls.json b/extensions/azure-account/package.nls.json new file mode 100644 index 00000000000..4857a376665 --- /dev/null +++ b/extensions/azure-account/package.nls.json @@ -0,0 +1,8 @@ +{ + "azure-account.commands.azure": "Azure", + "azure-account.commands.login": "Login", + "azure-account.commands.logout": "Logout", + "azure-account.commands.addResourceFilter": "Add Resource Filter", + "azure-account.commands.removeResourceFilter": "Remove Resource Filter", + "azure-account.commands.createAccount": "Create an Account" +} \ No newline at end of file diff --git a/extensions/azure-account/src/azure-account.ts b/extensions/azure-account/src/azure-account.ts new file mode 100644 index 00000000000..9d52bc3073b --- /dev/null +++ b/extensions/azure-account/src/azure-account.ts @@ -0,0 +1,471 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const adal = require('adal-node'); +const MemoryCache = adal.MemoryCache; +const AuthenticationContext = adal.AuthenticationContext; +const CacheDriver = require('adal-node/lib/cache-driver'); +const createLogContext = require('adal-node/lib/log').createLogContext; + +import { DeviceTokenCredentials, AzureEnvironment } from 'ms-rest-azure'; +import { SubscriptionClient, ResourceManagementClient, SubscriptionModels } from 'azure-arm-resource'; +import * as opn from 'opn'; +import * as copypaste from 'copy-paste'; +import * as nls from 'vscode-nls'; + +import { window, commands, credentials, EventEmitter, MessageItem, ExtensionContext, workspace, ConfigurationTarget } from 'vscode'; +import { AzureLogin, AzureSession, AzureLoginStatus, AzureResourceFilter } from './typings/azure-account.api'; + +const localize = nls.loadMessageBundle(); + +const defaultEnvironment = (AzureEnvironment).Azure; +const commonTenantId = 'common'; +const authorityHostUrl = defaultEnvironment.activeDirectoryEndpointUrl; +const clientId = '818dee8b-8777-4f45-afc3-f7cc977caae2'; +const authorityUrl = `${authorityHostUrl}${commonTenantId}`; +const resource = defaultEnvironment.activeDirectoryResourceId; + +const credentialsService = 'VSCode Public Azure'; +const credentialsAccount = 'Refresh Token'; + +interface DeviceLogin { + userCode: string; + deviceCode: string; + verificationUrl: string; + expiresIn: number; + interval: number; + message: string; +} + +interface TokenResponse { + tokenType: string; + expiresIn: number; + expiresOn: string; + resource: string; + accessToken: string; + refreshToken: string; + userId: string; + isUserIdDisplayable: boolean; + familyName: string; + givenName: string; + oid: string; + tenantId: string; + isMRRT: boolean; + _clientId: string; + _authority: string; +} + +interface AzureLoginWriteable extends AzureLogin { + status: AzureLoginStatus; +} + +class AzureLoginError extends Error { + constructor(message: string, public _reason: any) { + super(message); + } +} + +export class AzureLoginHelper { + + private onStatusChanged = new EventEmitter(); + private onSessionsChanged = new EventEmitter(); + private onFiltersChanged = new EventEmitter(); + private tokenCache = new MemoryCache(); + private oldResourceFilter: string; + + constructor(context: ExtensionContext) { + const subscriptions = context.subscriptions; + subscriptions.push(commands.registerCommand('azure-account.login', () => this.login().catch(console.error))); + subscriptions.push(commands.registerCommand('azure-account.logout', () => this.logout().catch(console.error))); + subscriptions.push(commands.registerCommand('azure-account.askForLogin', () => this.askForLogin().catch(console.error))); + subscriptions.push(commands.registerCommand('azure-account.addFilter', () => this.addFilter().catch(console.error))); + subscriptions.push(commands.registerCommand('azure-account.removeFilter', () => this.removeFilter().catch(console.error))); + subscriptions.push(this.api.onSessionsChanged(() => this.updateFilters().catch(console.error))); + subscriptions.push(workspace.onDidChangeConfiguration(() => this.updateFilters(true).catch(console.error))); + this.initialize() + .catch(console.error); + } + + api: AzureLogin = { + status: 'Initializing', + onStatusChanged: this.onStatusChanged.event, + sessions: [], + onSessionsChanged: this.onSessionsChanged.event, + filters: [], + onFiltersChanged: this.onFiltersChanged.event + }; + + async login() { + try { + this.beginLoggingIn(); + const deviceLogin = await deviceLogin1(); + const copyAndOpen: MessageItem = { title: localize('azure-account.copyAndOpen', "Copy & Open") }; + const close: MessageItem = { title: localize('azure-account.close', "Close"), isCloseAffordance: true }; + const response = await window.showInformationMessage(deviceLogin.message, copyAndOpen, close); + if (response === copyAndOpen) { + copypaste.copy(deviceLogin.userCode); + opn(deviceLogin.verificationUrl); + } + const tokenResponse = await deviceLogin2(deviceLogin); + const refreshToken = tokenResponse.refreshToken; + const tokenResponses = await tokensFromToken(tokenResponse); + await credentials.writeSecret(credentialsService, credentialsAccount, refreshToken); + await this.updateSessions(tokenResponses); + } finally { + this.updateStatus(); + } + } + + async logout() { + await credentials.deleteSecret(credentialsService, credentialsAccount); + await this.updateSessions([]); + this.updateStatus(); + } + + private async initialize() { + try { + const refreshToken = await credentials.readSecret(credentialsService, credentialsAccount); + if (refreshToken) { + this.beginLoggingIn(); + const tokenResponse = await tokenFromRefreshToken(refreshToken); + const tokenResponses = await tokensFromToken(tokenResponse); + await this.updateSessions(tokenResponses); + } + } catch (err) { + if (!(err instanceof AzureLoginError)) { + throw err; + } + } finally { + this.updateStatus(); + } + } + + private beginLoggingIn() { + if (this.api.status !== 'LoggedIn') { + (this.api).status = 'LoggingIn'; + this.onStatusChanged.fire(this.api.status); + } + } + + private updateStatus() { + const status = this.api.sessions.length ? 'LoggedIn' : 'LoggedOut'; + if (this.api.status !== status) { + (this.api).status = status; + this.onStatusChanged.fire(this.api.status); + } + } + + private async updateSessions(tokenResponses: TokenResponse[]) { + await clearTokenCache(this.tokenCache); + for (const tokenResponse of tokenResponses) { + await addTokenToCache(this.tokenCache, tokenResponse); + } + const sessions = this.api.sessions; + sessions.splice(0, sessions.length, ...tokenResponses.map(tokenResponse => ({ + environment: defaultEnvironment, + userId: tokenResponse.userId, + tenantId: tokenResponse.tenantId, + credentials: new DeviceTokenCredentials({ username: tokenResponse.userId, clientId, tokenCache: this.tokenCache, domain: tokenResponse.tenantId }) + }))); + this.onSessionsChanged.fire(); + } + + private async askForLogin() { + if (this.api.status === 'LoggedIn') { + return; + } + const login = { title: localize('azure-account.login', "Login") }; + const cancel = { title: 'Cancel', isCloseAffordance: true }; + const result = await window.showInformationMessage(localize('azure-account.loginFirst', "Not logged in, log in first."), login, cancel); + return result === login && commands.executeCommand('azure-account.login'); + } + + private async addFilter() { + if (this.api.status !== 'LoggedIn') { + return commands.executeCommand('azure-account.askForLogin'); + } + + const azureConfig = workspace.getConfiguration('azure'); + const resourceFilter = azureConfig.get('resourceFilter') || []; + + const subscriptionItems: { session: AzureSession; subscription: SubscriptionModels.Subscription }[] = []; + for (const session of this.api.sessions) { + const credentials = session.credentials; + const client = new SubscriptionClient(credentials); + const subscriptions = await listAll(client.subscriptions, client.subscriptions.list()); + subscriptionItems.push(...subscriptions.filter(subscription => resourceFilter.indexOf(`${session.tenantId}/${subscription.subscriptionId}`) === -1) + .map(subscription => ({ + session, + subscription + }))); + } + subscriptionItems.sort((a, b) => a.subscription.displayName!.localeCompare(b.subscription.displayName!)); + const subscriptionResult = await window.showQuickPick(subscriptionItems.map(subscription => ({ + label: subscription.subscription.displayName!, + description: subscription.subscription.subscriptionId!, + subscription + }))); + if (!subscriptionResult) { + return; + } + + const { session, subscription } = subscriptionResult.subscription; + const client = new ResourceManagementClient(session.credentials, subscription.subscriptionId!); + const resourceGroups = await listAll(client.resourceGroups, client.resourceGroups.list()); + const resourceGroupFilters: AzureResourceFilter[] = [ + { + ...subscriptionResult.subscription, + allResourceGroups: true, + resourceGroups + } + ]; + resourceGroupFilters.push(...resourceGroups.filter(resourceGroup => resourceFilter.indexOf(`${session.tenantId}/${subscription.subscriptionId}/${resourceGroup.name}`) === -1) + .map(resourceGroup => ({ + session, + subscription, + allResourceGroups: false, + resourceGroups: [resourceGroup] + }))); + resourceGroupFilters.sort((a, b) => (!a.allResourceGroups ? a.resourceGroups[0].name! : '').localeCompare(!b.allResourceGroups ? b.resourceGroups[0].name! : '')); + const resourceGroupResult = await window.showQuickPick(resourceGroupFilters.map(resourceGroup => (!resourceGroup.allResourceGroups ? { + label: resourceGroup.resourceGroups[0].name!, + description: resourceGroup.resourceGroups[0].location, + resourceGroup + } : { + label: localize('azure-account.entireSubscription', "Entire Subscription"), + description: '', + resourceGroup + }))); + if (!resourceGroupResult) { + return; + } + + const resourceGroup = resourceGroupResult.resourceGroup; + if (!resourceGroup.allResourceGroups) { + resourceFilter.push(`${resourceGroup.session.tenantId}/${resourceGroup.subscription.subscriptionId}/${resourceGroup.resourceGroups[0].name}`); + } else { + resourceFilter.splice(0, resourceFilter.length, ...resourceFilter.filter(c => !c.startsWith(`${resourceGroup.session.tenantId}/${resourceGroup.subscription.subscriptionId}/`))); + resourceFilter.push(`${resourceGroup.session.tenantId}/${resourceGroup.subscription.subscriptionId}`); + } + + const resourceFilterConfig = azureConfig.inspect('resourceFilter'); + let target = ConfigurationTarget.Global; + if (resourceFilterConfig) { + if (resourceFilterConfig.workspaceFolderValue) { + target = ConfigurationTarget.WorkspaceFolder; + } else if (resourceFilterConfig.workspaceValue) { + target = ConfigurationTarget.Workspace; + } else if (resourceFilterConfig.globalValue) { + target = ConfigurationTarget.Global; + } + } + await azureConfig.update('resourceFilter', resourceFilter, target); + } + + private async removeFilter() { + if (this.api.status !== 'LoggedIn') { + return commands.executeCommand('azure-account.askForLogin'); + } + + const azureConfig = workspace.getConfiguration('azure'); + let resourceFilter = azureConfig.get('resourceFilter') || []; + + const filters = resourceFilter.length ? this.api.filters.reduce((list, filter) => { + if (filter.allResourceGroups) { + list.push(filter); + } else { + list.push(...filter.resourceGroups.map(resourceGroup => ({ + ...filter, + resourceGroups: [resourceGroup] + }))); + } + return list; + }, []) : []; + filters.sort((a, b) => (!a.allResourceGroups ? a.resourceGroups[0].name! : `/${a.subscription.displayName}`).localeCompare(!b.allResourceGroups ? b.resourceGroups[0].name! : `/${b.subscription.displayName}`)); + const filterResult = await window.showQuickPick(filters.map(filter => (!filter.allResourceGroups ? { + label: filter.resourceGroups[0].name!, + description: filter.subscription.displayName!, + filter + } : { + label: filter.subscription.displayName!, + description: filter.subscription.subscriptionId!, + filter + }))); + if (!filterResult) { + return; + } + + const filter = filterResult.filter; + const remove = !filter.allResourceGroups ? + `${filter.session.tenantId}/${filter.subscription.subscriptionId}/${filter.resourceGroups[0].name}` : + `${filter.session.tenantId}/${filter.subscription.subscriptionId}`; + resourceFilter = resourceFilter.filter(e => e !== remove); + + const resourceFilterConfig = azureConfig.inspect('resourceFilter'); + let target = ConfigurationTarget.Global; + if (resourceFilterConfig) { + if (resourceFilterConfig.workspaceFolderValue) { + target = ConfigurationTarget.WorkspaceFolder; + } else if (resourceFilterConfig.workspaceValue) { + target = ConfigurationTarget.Workspace; + } else if (resourceFilterConfig.globalValue) { + target = ConfigurationTarget.Global; + } + } + await azureConfig.update('resourceFilter', resourceFilter.length ? resourceFilter : undefined, target); + } + + private async updateFilters(configChange = false) { + const azureConfig = workspace.getConfiguration('azure'); + let resourceFilter = azureConfig.get('resourceFilter'); + if (configChange && JSON.stringify(resourceFilter) === this.oldResourceFilter) { + return; + } + this.oldResourceFilter = JSON.stringify(resourceFilter); + if (resourceFilter && !Array.isArray(resourceFilter)) { + resourceFilter = []; + } + const filters = resourceFilter && resourceFilter.map(s => typeof s === 'string' ? s.split('/') : []) + .filter(s => s.length === 2 || s.length === 3) + .map(([tenantId, subscriptionId, resourceGroup]) => ({ tenantId, subscriptionId, resourceGroup })); + const tenantIds = filters && filters.reduce | boolean>>>((result, filter) => { + const tenant = result[filter.tenantId] || (result[filter.tenantId] = {}); + const resourceGroups = tenant[filter.subscriptionId] || (tenant[filter.subscriptionId] = (filter.resourceGroup ? {} : true)); + if (typeof resourceGroups === 'object' && filter.resourceGroup) { + resourceGroups[filter.resourceGroup] = true; + } + return result; + }, {}); + + const newFilters: AzureResourceFilter[] = []; + const sessions = tenantIds ? this.api.sessions.filter(session => tenantIds[session.tenantId]) : this.api.sessions; + for (const session of sessions) { + const client = new SubscriptionClient(session.credentials); + const subscriptionIds = tenantIds && tenantIds[session.tenantId]; + const subscriptions = await listAll(client.subscriptions, client.subscriptions.list()); + const filteredSubscriptions = subscriptionIds ? subscriptions.filter(subscription => subscriptionIds[subscription.subscriptionId!]) : subscriptions; + for (const subscription of filteredSubscriptions) { + const client = new ResourceManagementClient(session.credentials, subscription.subscriptionId!); + const resourceGroupNames = subscriptionIds && subscriptionIds[subscription.subscriptionId!]; + const allResourceGroups = !(resourceGroupNames && typeof resourceGroupNames === 'object'); + const unfilteredResourceGroups = await listAll(client.resourceGroups, client.resourceGroups.list()); + const resourceGroups = allResourceGroups ? unfilteredResourceGroups : unfilteredResourceGroups.filter(resourceGroup => (>resourceGroupNames!)[resourceGroup.name!]); + newFilters.push({ session, subscription, allResourceGroups, resourceGroups }); + } + } + this.api.filters.splice(0, this.api.filters.length, ...newFilters); + this.onFiltersChanged.fire(); + } +} + +async function deviceLogin1(): Promise { + return new Promise((resolve, reject) => { + const cache = new MemoryCache(); + const context = new AuthenticationContext(authorityUrl, null, cache); + context.acquireUserCode(resource, clientId, 'en-us', function (err: any, response: any) { + if (err) { + reject(new AzureLoginError(localize('azure-account.userCodeFailed', "Aquiring user code failed"), err)); + } else { + resolve(response); + } + }); + }); +} + +async function deviceLogin2(deviceLogin: DeviceLogin) { + return new Promise((resolve, reject) => { + const tokenCache = new MemoryCache(); + const context = new AuthenticationContext(authorityUrl, null, tokenCache); + context.acquireTokenWithDeviceCode(resource, clientId, deviceLogin, function (err: any, tokenResponse: TokenResponse) { + if (err) { + reject(new AzureLoginError(localize('azure-account.tokenFailed', "Aquiring token with device code"), err)); + } else { + resolve(tokenResponse); + } + }); + }); +} + +async function tokenFromRefreshToken(refreshToken: string, tenantId = commonTenantId) { + return new Promise((resolve, reject) => { + const tokenCache = new MemoryCache(); + const context = new AuthenticationContext(`${authorityHostUrl}${tenantId}`, null, tokenCache); + context.acquireTokenWithRefreshToken(refreshToken, clientId, null, function (err: any, tokenResponse: TokenResponse) { + if (err) { + reject(new AzureLoginError(localize('azure-account.tokenFromRefreshTokenFailed', "Aquiring token with refresh token"), err)); + } else { + resolve(tokenResponse); + } + }); + }); +} + +async function tokensFromToken(firstTokenResponse: TokenResponse) { + const tokenResponses = [firstTokenResponse]; + const tokenCache = new MemoryCache(); + await addTokenToCache(tokenCache, firstTokenResponse); + const credentials = new DeviceTokenCredentials({ username: firstTokenResponse.userId, clientId, tokenCache }); + const client = new SubscriptionClient(credentials); + const tenants = await listAll(client.tenants, client.tenants.list()); + for (const tenant of tenants) { + if (tenant.tenantId !== firstTokenResponse.tenantId) { + const tokenResponse = await tokenFromRefreshToken(firstTokenResponse.refreshToken, tenant.tenantId); + tokenResponses.push(tokenResponse); + } + } + return tokenResponses; +} + +async function addTokenToCache(tokenCache: any, tokenResponse: TokenResponse) { + return new Promise((resolve, reject) => { + const driver = new CacheDriver( + { _logContext: createLogContext('') }, + `${authorityHostUrl}${tokenResponse.tenantId}`, + tokenResponse.resource, + clientId, + tokenCache, + (entry: any, resource: any, callback: (err: any, response: any) => {}) => { + callback(null, entry); + } + ); + driver.add(tokenResponse, function (err: any) { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} + +async function clearTokenCache(tokenCache: any) { + await new Promise((resolve, reject) => { + tokenCache.find({}, (err: any, entries: any[]) => { + if (err) { + reject(err); + } else { + tokenCache.remove(entries, (err: any) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + } + }); + }); +} + +export interface PartialList extends Array { + nextLink?: string; +} + +export async function listAll(client: { listNext(nextPageLink: string): Promise>; }, first: Promise>): Promise { + const all: T[] = []; + for (let list = await first; list.length || list.nextLink; list = list.nextLink ? await client.listNext(list.nextLink) : []) { + all.push(...list); + } + return all; +} \ No newline at end of file diff --git a/extensions/azure-account/src/extension.ts b/extensions/azure-account/src/extension.ts new file mode 100644 index 00000000000..4b67cf387d2 --- /dev/null +++ b/extensions/azure-account/src/extension.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { window, ExtensionContext, commands, credentials } from 'vscode'; +import { AzureLoginHelper } from './azure-account'; +import { AzureLogin } from './typings/azure-account.api'; +import * as opn from 'opn'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +export function activate(context: ExtensionContext) { + if (!credentials) { + return; // Proposed API not available. + } + const azureLogin = new AzureLoginHelper(context); + const subscriptions = context.subscriptions; + subscriptions.push(createStatusBarItem(azureLogin.api)); + subscriptions.push(commands.registerCommand('azure-account.createAccount', createAccount)); + return azureLogin.api; +} + +function createAccount() { + opn('https://azure.microsoft.com/en-us/free'); +} + +function createStatusBarItem(api: AzureLogin) { + const statusBarItem = window.createStatusBarItem(); + function updateStatusBar() { + switch (api.status) { + case 'LoggingIn': + statusBarItem.text = localize('azure-account.loggingIn', "Azure: Logging in..."); + statusBarItem.show(); + break; + case 'LoggedIn': + statusBarItem.text = localize('azure-account.loggedIn', "Azure: {0}", api.sessions[0].userId); + statusBarItem.show(); + break; + default: + statusBarItem.hide(); + break; + } + } + api.onStatusChanged(updateStatusBar); + api.onSessionsChanged(updateStatusBar); + updateStatusBar(); + return statusBarItem; +} + +export function deactivate() { +} \ No newline at end of file diff --git a/extensions/azure-account/src/typings/azure-account.api.d.ts b/extensions/azure-account/src/typings/azure-account.api.d.ts new file mode 100644 index 00000000000..b1f2c167b45 --- /dev/null +++ b/extensions/azure-account/src/typings/azure-account.api.d.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vscode'; +import { ServiceClientCredentials } from 'ms-rest'; +import { AzureEnvironment } from 'ms-rest-azure'; +import { SubscriptionModels, ResourceModels } from 'azure-arm-resource'; + +export type AzureLoginStatus = 'Initializing' | 'LoggingIn' | 'LoggedIn' | 'LoggedOut'; + +export interface AzureLogin { + readonly status: AzureLoginStatus; + readonly onStatusChanged: Event; + readonly sessions: AzureSession[]; + readonly onSessionsChanged: Event; + readonly filters: AzureResourceFilter[]; + readonly onFiltersChanged: Event; +} + +export interface AzureSession { + readonly environment: AzureEnvironment; + readonly userId: string; + readonly tenantId: string; + readonly credentials: ServiceClientCredentials; +} + +export interface AzureResourceFilter { + readonly session: AzureSession; + readonly subscription: SubscriptionModels.Subscription; + readonly allResourceGroups: boolean; + readonly resourceGroups: ResourceModels.ResourceGroup[]; +} \ No newline at end of file diff --git a/extensions/azure-account/src/typings/ref.d.ts b/extensions/azure-account/src/typings/ref.d.ts new file mode 100644 index 00000000000..216911a680e --- /dev/null +++ b/extensions/azure-account/src/typings/ref.d.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// diff --git a/extensions/azure-account/src/typings/vscode.rejected.d.ts b/extensions/azure-account/src/typings/vscode.rejected.d.ts new file mode 100644 index 00000000000..7f6f64df560 --- /dev/null +++ b/extensions/azure-account/src/typings/vscode.rejected.d.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// This is the place for API experiments and proposal. + +declare module 'vscode' { + + /** + * Namespace for handling credentials. + */ + export namespace credentials { + + /** + * Read a previously stored secret from the credential store. + * + * @param service The service of the credential. + * @param account The account of the credential. + * @return A promise for the secret of the credential. + */ + export function readSecret(service: string, account: string): Thenable; + + /** + * Write a secret to the credential store. + * + * @param service The service of the credential. + * @param account The account of the credential. + * @param secret The secret of the credential to write to the credential store. + * @return A promise indicating completion of the operation. + */ + export function writeSecret(service: string, account: string, secret: string): Thenable; + + /** + * Delete a previously stored secret from the credential store. + * + * @param service The service of the credential. + * @param account The account of the credential. + * @return A promise resolving to true if there was a secret for that service and account. + */ + export function deleteSecret(service: string, account: string): Thenable; + } +} diff --git a/extensions/azure-account/tsconfig.json b/extensions/azure-account/tsconfig.json new file mode 100644 index 00000000000..61265d449a4 --- /dev/null +++ b/extensions/azure-account/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "strict": true, + "noUnusedLocals": true, + "outDir": "./out", + "lib": [ + "es6" + ], + "sourceMap": true + }, + "exclude": [ + "node_modules" + ], + "include": [ + "src/**/*" + ] +} \ No newline at end of file