Skip to content

Commit

Permalink
Remove dependency on Azure Account extension (#551)
Browse files Browse the repository at this point in the history
* Use vscode authentication library instead of Azure Account extension

* remove msal-node dependency

* remove azure auth

* remove duplication of getDefaultScope function and add error handling

* auth fixes for switching between signed-in tenants

* If no subscriptions are selected, display them all in the treeview

* remove unused properties from jwt interface

* fix URLs in select-subscriptions command

* get tenant id from auth session and new filter option for listing subs

* avoid passing treenodes to cluster library functions

* avoid exposing ISubscriptionContext from tree nodes

* handle session changes when tenants change and not when we triggered the change, and get session silently whenever possible

* Select a tenant by default rather than forcing user to do that

* list clusters using resource management client (as we were before)

* dedicated command for tenant selection, and abstract session provider with interface
  • Loading branch information
peterbom authored Apr 26, 2024
1 parent 9503e20 commit 127ed98
Show file tree
Hide file tree
Showing 43 changed files with 1,648 additions and 6,483 deletions.
6,075 changes: 72 additions & 6,003 deletions package-lock.json

Large diffs are not rendered by default.

27 changes: 19 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@
"configuration": {
"title": "AKS",
"properties": {
"aks.selectedSubscriptions": {
"type": "array",
"description": "Selected Azure subscriptions",
"items": {
"type": "string"
}
},
"aks.periscope.repoOrg": {
"type": "string",
"default": "azure",
Expand Down Expand Up @@ -157,6 +164,7 @@
{
"command": "aks.selectSubscriptions",
"title": "Select subscriptions...",
"category": "AKS",
"icon": {
"dark": "resources/dark/filter.svg",
"light": "resources/light/filter.svg"
Expand All @@ -170,6 +178,16 @@
"light": "resources/light/refresh.svg"
}
},
{
"command": "aks.signInToAzure",
"title": "Sign in to Azure...",
"category": "AKS"
},
{
"command": "aks.selectTenant",
"title": "Select Tenant...",
"category": "AKS"
},
{
"command": "aks.periscope",
"title": "Run AKS Periscope"
Expand Down Expand Up @@ -273,10 +291,6 @@
],
"menus": {
"commandPalette": [
{
"command": "aks.selectSubscriptions",
"when": "never"
},
{
"command": "aks.refreshSubscription",
"when": "never"
Expand Down Expand Up @@ -477,7 +491,6 @@
"test": "npm run test-compile && node ./out/src/tests/runTests.js"
},
"extensionDependencies": [
"ms-vscode.azure-account",
"ms-kubernetes-tools.vscode-kubernetes-tools"
],
"devDependencies": {
Expand All @@ -499,7 +512,6 @@
"chai": "^5.1.0",
"eslint": "^8.57.0",
"eslint-webpack-plugin": "^4.1.0",
"filemanager-webpack-plugin": "^8.0.0",
"glob": "^10.3.12",
"mocha": "^10.4.0",
"prettier": "^3.2.5",
Expand All @@ -514,15 +526,14 @@
"@azure/arm-containerservice": "^19.7.0",
"@azure/arm-monitor": "^7.0.0",
"@azure/arm-resources": "^5.2.0",
"@azure/arm-resources-subscriptions": "^2.1.0",
"@azure/arm-storage": "^18.2.0",
"@azure/arm-subscriptions": "^5.1.0",
"@azure/core-auth": "^1.7.2",
"@azure/identity": "^4.1.0",
"@azure/ms-rest-azure-env": "^2.0.0",
"@azure/msal-node": "^2.6.6",
"@azure/storage-blob": "^12.17.0",
"@microsoft/microsoft-graph-client": "^3.0.7",
"@microsoft/vscode-azext-azureutils": "^3.0.1",
"@microsoft/vscode-azext-utils": "^2.4.0",
"@vscode/extension-telemetry": "^0.9.6",
"cross-fetch": "^4.0.0",
Expand Down
25 changes: 25 additions & 0 deletions resources/azure.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions resources/azureSubscription.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
136 changes: 136 additions & 0 deletions src/auth/azureAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import {
AuthenticationSession,
Disposable as VsCodeDisposable,
ProgressLocation,
ProgressOptions,
QuickPickItem,
window,
} from "vscode";
import { AzureSessionProvider, ReadyAzureSessionProvider, Tenant, TokenInfo, isReady } from "./types";
import { Environment } from "@azure/ms-rest-azure-env";
import { getConfiguredAzureEnv } from "../commands/utils/config";
import { Errorable, failed } from "../commands/utils/errorable";
import { TokenCredential } from "@azure/core-auth";
import { parseJson } from "../commands/utils/json";
import { getSessionProvider } from "./azureSessionProvider";

export function getEnvironment(): Environment {
return getConfiguredAzureEnv();
}

export async function getReadySessionProvider(): Promise<Errorable<ReadyAzureSessionProvider>> {
const sessionProvider = getSessionProvider();
if (isReady(sessionProvider)) {
return { succeeded: true, result: sessionProvider };
}

switch (sessionProvider.signInStatus) {
case "Initializing":
case "SigningIn":
await waitForSignIn(sessionProvider);
break;
case "SignedOut":
await sessionProvider.signIn();
break;
case "SignedIn":
break;
}

// Get a session, which will prompt the user to select a tenant if necessary.
const session = await sessionProvider.getAuthSession();
if (failed(session)) {
return { succeeded: false, error: `Failed to get authentication session: ${session.error}` };
}

if (!isReady(sessionProvider)) {
return { succeeded: false, error: "Not signed in." };
}

return { succeeded: true, result: sessionProvider };
}

async function waitForSignIn(sessionProvider: AzureSessionProvider): Promise<void> {
const options: ProgressOptions = {
location: ProgressLocation.Notification,
title: "Waiting for sign-in",
cancellable: true,
};

await window.withProgress(options, (_, token) => {
let listener: VsCodeDisposable | undefined;
token.onCancellationRequested(listener?.dispose());
return new Promise((resolve) => {
listener = sessionProvider.signInStatusChangeEvent((status) => {
if (status === "SignedIn") {
listener?.dispose();
resolve(undefined);
}
});
});
});
}

export function getCredential(sessionProvider: ReadyAzureSessionProvider): TokenCredential {
return {
getToken: async () => {
const session = await sessionProvider.getAuthSession();
if (failed(session)) {
throw new Error(`No Microsoft authentication session found: ${session.error}`);
}

return { token: session.result.accessToken, expiresOnTimestamp: 0 };
},
};
}

export function getTokenInfo(session: AuthenticationSession): Errorable<TokenInfo> {
const tokenParts = session.accessToken.split(".");
if (tokenParts.length !== 3) {
return { succeeded: false, error: `Access token not a valid JWT: ${session.accessToken}` };
}

const body = tokenParts[1];
let jsonBody: string;
try {
jsonBody = Buffer.from(body, "base64").toString();
} catch (e) {
return { succeeded: false, error: `Failed to decode JWT token body: ${body}` };
}

const jwt = parseJson<Jwt>(jsonBody);
if (failed(jwt)) {
return jwt;
}

const tokenInfo: TokenInfo = {
token: session.accessToken,
expiry: new Date(jwt.result.exp * 1000),
};

return { succeeded: true, result: tokenInfo };
}

export function getDefaultScope(endpointUrl: string): string {
// Endpoint URL is that of the audience, e.g. for ARM in the public cloud
// it would be "https://management.azure.com".
return endpointUrl.endsWith("/") ? `${endpointUrl}.default` : `${endpointUrl}/.default`;
}

/**
* The type of a JSON-parsed JWT body. Right now we only make use of the 'exp' field,
* but other standard claims could be added here if needed.
*/
interface Jwt {
exp: number;
}

export async function quickPickTenant(tenants: Tenant[]): Promise<Tenant | undefined> {
const items: (QuickPickItem & { tenant: Tenant })[] = tenants.map((t) => ({
label: `${t.name} (${t.id})`,
tenant: t,
}));
const result = await window.showQuickPick(items, {
placeHolder: "Select a tenant",
});
return result ? result.tenant : undefined;
}
Loading

0 comments on commit 127ed98

Please sign in to comment.