From 6925fa8e4e49766720137a83e3c6bff52e363b1b Mon Sep 17 00:00:00 2001 From: jawelton74 <103591340+jawelton74@users.noreply.github.com> Date: Tue, 9 Apr 2024 10:55:08 -0700 Subject: [PATCH] Replace Entra app client secret auth with OpenID Connect in E2E tests. (#1792) * Use Az login with OpenID connection to get test credentials. * Set subscription id environment variable. * Update testExplorer and cleanup job. * Retrieve access token in test case and pass to testExplorer. * Add debug tracing for tests. * Set up other mongo test to use Az CLI creds. * Revert subscription id retrieval. * Add CLI credentials retrieval to rest of tests. * Fix missing imports. * Clean up redundant code. * Remove commented import statement. --- .github/workflows/ci.yml | 14 ++++++++++++-- .github/workflows/cleanup.yml | 15 +++++++++++++-- test/cassandra/container.spec.ts | 7 +++++-- test/graph/container.spec.ts | 7 +++++-- test/mongo/container.spec.ts | 7 +++++-- test/mongo/container32.spec.ts | 7 +++++-- test/selfServe/selfServeExample.spec.ts | 7 ++++++- test/sql/container.spec.ts | 7 +++++-- test/sql/resourceToken.spec.ts | 10 +++------- test/tables/container.spec.ts | 6 ++++-- test/testExplorer/TestExplorer.ts | 19 +------------------ test/utils/shared.ts | 11 +++++++++++ utils/cleanupDBs.js | 9 +++------ 13 files changed, 78 insertions(+), 48 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a53b92645..fd097a194 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: pull_request: branches: - master +permissions: + id-token: write + contents: read jobs: codemetrics: runs-on: ubuntu-latest @@ -134,7 +137,7 @@ jobs: runs-on: ubuntu-latest env: NODE_TLS_REJECT_UNAUTHORIZED: 0 - NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} strategy: fail-fast: false matrix: @@ -145,11 +148,18 @@ jobs: - ./test/mongo/container.spec.ts - ./test/mongo/container32.spec.ts - ./test/selfServe/selfServeExample.spec.ts - # - ./test/notebooks/upload.spec.ts // TEMP disabled since notebooks service is off - ./test/sql/resourceToken.spec.ts - ./test/tables/container.spec.ts steps: - uses: actions/checkout@v4 + + - name: "Az CLI login" + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Use Node.js 18.x uses: actions/setup-node@v4 with: diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml index 229477f0b..6698951ae 100644 --- a/.github/workflows/cleanup.yml +++ b/.github/workflows/cleanup.yml @@ -9,6 +9,10 @@ on: # Once every hour - cron: "0 15 * * *" +permissions: + id-token: write + contents: read + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" @@ -16,10 +20,17 @@ jobs: name: "Cleanup Test Database Accounts" runs-on: ubuntu-latest env: - NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }} - NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} + AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} steps: - uses: actions/checkout@v2 + + - name: "Az CLI login" + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Use Node.js 18.x uses: actions/setup-node@v1 with: diff --git a/test/cassandra/container.spec.ts b/test/cassandra/container.spec.ts index 48acddb79..80d5df41d 100644 --- a/test/cassandra/container.spec.ts +++ b/test/cassandra/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Cassandra keyspace and table CRUD", async () => { const keyspaceId = generateUniqueName("keyspace"); const tableId = generateUniqueName("table"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-cassandra-runner&token=${token}`); await page.waitForSelector("iframe"); const explorer = await waitForExplorer(); diff --git a/test/graph/container.spec.ts b/test/graph/container.spec.ts index 3e6155f1c..e7f288da5 100644 --- a/test/graph/container.spec.ts +++ b/test/graph/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Graph CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-gremlin-runner&token=${token}`); const explorer = await waitForExplorer(); // Create new database and graph diff --git a/test/mongo/container.spec.ts b/test/mongo/container.spec.ts index 88a311784..baafefbbe 100644 --- a/test/mongo/container.spec.ts +++ b/test/mongo/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo-runner&token=${token}`); const explorer = await waitForExplorer(); // Create new database and collection diff --git a/test/mongo/container32.spec.ts b/test/mongo/container32.spec.ts index 25466e266..c71f9d0cc 100644 --- a/test/mongo/container32.spec.ts +++ b/test/mongo/container32.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; +import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(240000); test("Mongo CRUD", async () => { const databaseId = generateDatabaseNameWithTimestamp(); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-mongo32-runner&token=${token}`); const explorer = await waitForExplorer(); // Create new database and collection diff --git a/test/selfServe/selfServeExample.spec.ts b/test/selfServe/selfServeExample.spec.ts index 7e10c1ce2..3678f5b35 100644 --- a/test/selfServe/selfServeExample.spec.ts +++ b/test/selfServe/selfServeExample.spec.ts @@ -1,5 +1,10 @@ +import { getAzureCLICredentialsToken } from "../utils/shared"; + test("Self Serve", async () => { - await page.goto("https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html"); + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); + + await page.goto(`https://localhost:1234/testExplorer.html?iframeSrc=selfServe.html&token=${token}`); const handle = await page.waitForSelector("iframe"); const frame = await handle.contentFrame(); diff --git a/test/sql/container.spec.ts b/test/sql/container.spec.ts index aead57b92..a1aacfd42 100644 --- a/test/sql/container.spec.ts +++ b/test/sql/container.spec.ts @@ -1,15 +1,18 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("SQL CRUD", async () => { const databaseId = generateUniqueName("db"); const containerId = generateUniqueName("container"); + + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-sql-runner-west-us&token=${token}`); const explorer = await waitForExplorer(); await explorer.click('[data-test="New Container"]'); diff --git a/test/sql/resourceToken.spec.ts b/test/sql/resourceToken.spec.ts index f2e9d94ef..18228c7ed 100644 --- a/test/sql/resourceToken.spec.ts +++ b/test/sql/resourceToken.spec.ts @@ -1,19 +1,15 @@ import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; import { CosmosClient, PermissionMode } from "@azure/cosmos"; -import * as msRestNodeAuth from "@azure/ms-rest-nodeauth"; import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentials } from "../utils/shared"; jest.setTimeout(120000); -const clientId = "fd8753b0-0707-4e32-84e9-2532af865fb4"; -const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"]; -const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; -const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c"; +const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"] ?? ""; const resourceGroupName = "runners"; test("Resource token", async () => { - const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId); + const credentials = await getAzureCLICredentials(); const armClient = new CosmosDBManagementClient(credentials, subscriptionId); const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us"); const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us"); diff --git a/test/tables/container.spec.ts b/test/tables/container.spec.ts index d8daa0a16..98687d60f 100644 --- a/test/tables/container.spec.ts +++ b/test/tables/container.spec.ts @@ -1,15 +1,17 @@ import { jest } from "@jest/globals"; import "expect-playwright"; -import { generateUniqueName } from "../utils/shared"; +import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared"; import { waitForExplorer } from "../utils/waitForExplorer"; jest.setTimeout(120000); test("Tables CRUD", async () => { const tableId = generateUniqueName("table"); + // We can't retrieve AZ CLI credentials from the browser so we get them here. + const token = await getAzureCLICredentialsToken(); page.setDefaultTimeout(50000); - await page.goto("https://localhost:1234/testExplorer.html?accountName=portal-tables-runner"); + await page.goto(`https://localhost:1234/testExplorer.html?accountName=portal-tables-runner&token=${token}`); const explorer = await waitForExplorer(); await page.waitForSelector('text="Querying databases"', { state: "detached" }); diff --git a/test/testExplorer/TestExplorer.ts b/test/testExplorer/TestExplorer.ts index 684673590..a415b5bd6 100644 --- a/test/testExplorer/TestExplorer.ts +++ b/test/testExplorer/TestExplorer.ts @@ -1,5 +1,4 @@ /* eslint-disable no-console */ -import { ClientSecretCredential } from "@azure/identity"; import "../../less/hostedexplorer.less"; import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels"; import { updateUserContext } from "../../src/UserContext"; @@ -11,29 +10,13 @@ const urlSearchParams = new URLSearchParams(window.location.search); const accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us"; const selfServeType = urlSearchParams.get("selfServeType") || "example"; const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache"; - -if (!process.env.AZURE_CLIENT_SECRET) { - throw new Error( - "process.env.AZURE_CLIENT_SECRET was not set! Set it in your .env file and restart webpack dev server", - ); -} - -// Azure SDK clients accept the credential as a parameter -const credentials = new ClientSecretCredential( - process.env.AZURE_TENANT_ID, - process.env.AZURE_CLIENT_ID, - process.env.AZURE_CLIENT_SECRET, - { - authorityHost: "https://localhost:1234", - }, -); +const token = urlSearchParams.get("token"); console.log("Resource Group:", resourceGroup); console.log("Subcription: ", subscriptionId); console.log("Account Name: ", accountName); const initTestExplorer = async (): Promise => { - const { token } = await credentials.getToken("https://management.azure.com//.default"); updateUserContext({ authorizationToken: `bearer ${token}`, }); diff --git a/test/utils/shared.ts b/test/utils/shared.ts index 118736129..59ef0994c 100644 --- a/test/utils/shared.ts +++ b/test/utils/shared.ts @@ -1,3 +1,4 @@ +import { AzureCliCredentials } from "@azure/ms-rest-nodeauth"; import crypto from "crypto"; export function generateUniqueName(baseName = "", length = 4): string { @@ -7,3 +8,13 @@ export function generateUniqueName(baseName = "", length = 4): string { export function generateDatabaseNameWithTimestamp(baseName = "db", length = 1): string { return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`; } + +export async function getAzureCLICredentials(): Promise { + return await AzureCliCredentials.create(); +} + +export async function getAzureCLICredentialsToken(): Promise { + const credentials = await getAzureCLICredentials(); + const token = (await credentials.getToken()).accessToken; + return token; +} diff --git a/utils/cleanupDBs.js b/utils/cleanupDBs.js index 72fcfbafd..b2bbf0be8 100644 --- a/utils/cleanupDBs.js +++ b/utils/cleanupDBs.js @@ -2,10 +2,7 @@ const msRestNodeAuth = require("@azure/ms-rest-nodeauth"); const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb"); const ms = require("ms"); -const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"]; -const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"]; -const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47"; -const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c"; +const subscriptionId = process.env["AZURE_SUBSCRIPTION_ID"]; const resourceGroupName = "runners"; const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime(); @@ -19,7 +16,7 @@ function friendlyTime(date) { } async function main() { - const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId); + const credentials = await msRestNodeAuth.AzureCliCredentials.create(); const client = new CosmosDBManagementClient(credentials, subscriptionId); const accounts = await client.databaseAccounts.list(resourceGroupName); for (const account of accounts) { @@ -38,7 +35,7 @@ async function main() { } else if (account.capabilities.find((c) => c.name === "EnableCassandra")) { const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces( resourceGroupName, - account.name + account.name, ); for (const database of cassandraDatabases) { const timestamp = Number(database.resource._ts) * 1000;