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.
This commit is contained in:
jawelton74 2024-04-09 10:55:08 -07:00 committed by GitHub
parent 7f6338b68b
commit 6925fa8e4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 78 additions and 48 deletions

View File

@ -8,6 +8,9 @@ on:
pull_request: pull_request:
branches: branches:
- master - master
permissions:
id-token: write
contents: read
jobs: jobs:
codemetrics: codemetrics:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -134,7 +137,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }} AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -145,11 +148,18 @@ jobs:
- ./test/mongo/container.spec.ts - ./test/mongo/container.spec.ts
- ./test/mongo/container32.spec.ts - ./test/mongo/container32.spec.ts
- ./test/selfServe/selfServeExample.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/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts - ./test/tables/container.spec.ts
steps: steps:
- uses: actions/checkout@v4 - 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 - name: Use Node.js 18.x
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:

View File

@ -9,6 +9,10 @@ on:
# Once every hour # Once every hour
- cron: "0 15 * * *" - 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 # A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs: jobs:
# This workflow contains a single job called "build" # This workflow contains a single job called "build"
@ -16,10 +20,17 @@ jobs:
name: "Cleanup Test Database Accounts" name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }} AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
steps: steps:
- uses: actions/checkout@v2 - 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 - name: Use Node.js 18.x
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:

View File

@ -1,15 +1,18 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName } from "../utils/shared"; import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000); jest.setTimeout(120000);
test("Cassandra keyspace and table CRUD", async () => { test("Cassandra keyspace and table CRUD", async () => {
const keyspaceId = generateUniqueName("keyspace"); const keyspaceId = generateUniqueName("keyspace");
const tableId = generateUniqueName("table"); 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); 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"); await page.waitForSelector("iframe");
const explorer = await waitForExplorer(); const explorer = await waitForExplorer();

View File

@ -1,15 +1,18 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
test("Graph CRUD", async () => { test("Graph CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp(); const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container"); 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); 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(); const explorer = await waitForExplorer();
// Create new database and graph // Create new database and graph

View File

@ -1,15 +1,18 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
test("Mongo CRUD", async () => { test("Mongo CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp(); const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container"); 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); 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(); const explorer = await waitForExplorer();
// Create new database and collection // Create new database and collection

View File

@ -1,15 +1,18 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateDatabaseNameWithTimestamp, generateUniqueName } from "../utils/shared"; import { generateDatabaseNameWithTimestamp, generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(240000); jest.setTimeout(240000);
test("Mongo CRUD", async () => { test("Mongo CRUD", async () => {
const databaseId = generateDatabaseNameWithTimestamp(); const databaseId = generateDatabaseNameWithTimestamp();
const containerId = generateUniqueName("container"); 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); 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(); const explorer = await waitForExplorer();
// Create new database and collection // Create new database and collection

View File

@ -1,5 +1,10 @@
import { getAzureCLICredentialsToken } from "../utils/shared";
test("Self Serve", async () => { 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 handle = await page.waitForSelector("iframe");
const frame = await handle.contentFrame(); const frame = await handle.contentFrame();

View File

@ -1,15 +1,18 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName } from "../utils/shared"; import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000); jest.setTimeout(120000);
test("SQL CRUD", async () => { test("SQL CRUD", async () => {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
const containerId = generateUniqueName("container"); 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); 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(); const explorer = await waitForExplorer();
await explorer.click('[data-test="New Container"]'); await explorer.click('[data-test="New Container"]');

View File

@ -1,19 +1,15 @@
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import { CosmosClient, PermissionMode } from "@azure/cosmos"; import { CosmosClient, PermissionMode } from "@azure/cosmos";
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName } from "../utils/shared"; import { generateUniqueName, getAzureCLICredentials } from "../utils/shared";
jest.setTimeout(120000); jest.setTimeout(120000);
const clientId = "fd8753b0-0707-4e32-84e9-2532af865fb4"; const subscriptionId = process.env["AZURE_SUBSCRIPTION_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 resourceGroupName = "runners"; const resourceGroupName = "runners";
test("Resource token", async () => { test("Resource token", async () => {
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId); const credentials = await getAzureCLICredentials();
const armClient = new CosmosDBManagementClient(credentials, subscriptionId); const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us"); const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner-west-us");
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us"); const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner-west-us");

View File

@ -1,15 +1,17 @@
import { jest } from "@jest/globals"; import { jest } from "@jest/globals";
import "expect-playwright"; import "expect-playwright";
import { generateUniqueName } from "../utils/shared"; import { generateUniqueName, getAzureCLICredentialsToken } from "../utils/shared";
import { waitForExplorer } from "../utils/waitForExplorer"; import { waitForExplorer } from "../utils/waitForExplorer";
jest.setTimeout(120000); jest.setTimeout(120000);
test("Tables CRUD", async () => { test("Tables CRUD", async () => {
const tableId = generateUniqueName("table"); 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); 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(); const explorer = await waitForExplorer();
await page.waitForSelector('text="Querying databases"', { state: "detached" }); await page.waitForSelector('text="Querying databases"', { state: "detached" });

View File

@ -1,5 +1,4 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { ClientSecretCredential } from "@azure/identity";
import "../../less/hostedexplorer.less"; import "../../less/hostedexplorer.less";
import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels"; import { DataExplorerInputsFrame } from "../../src/Contracts/ViewModels";
import { updateUserContext } from "../../src/UserContext"; 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 accountName = urlSearchParams.get("accountName") || "portal-sql-runner-west-us";
const selfServeType = urlSearchParams.get("selfServeType") || "example"; const selfServeType = urlSearchParams.get("selfServeType") || "example";
const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache"; const iframeSrc = urlSearchParams.get("iframeSrc") || "explorer.html?platform=Portal&disablePortalInitCache";
const token = urlSearchParams.get("token");
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",
},
);
console.log("Resource Group:", resourceGroup); console.log("Resource Group:", resourceGroup);
console.log("Subcription: ", subscriptionId); console.log("Subcription: ", subscriptionId);
console.log("Account Name: ", accountName); console.log("Account Name: ", accountName);
const initTestExplorer = async (): Promise<void> => { const initTestExplorer = async (): Promise<void> => {
const { token } = await credentials.getToken("https://management.azure.com//.default");
updateUserContext({ updateUserContext({
authorizationToken: `bearer ${token}`, authorizationToken: `bearer ${token}`,
}); });

View File

@ -1,3 +1,4 @@
import { AzureCliCredentials } from "@azure/ms-rest-nodeauth";
import crypto from "crypto"; import crypto from "crypto";
export function generateUniqueName(baseName = "", length = 4): string { 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 { export function generateDatabaseNameWithTimestamp(baseName = "db", length = 1): string {
return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`; return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`;
} }
export async function getAzureCLICredentials(): Promise<AzureCliCredentials> {
return await AzureCliCredentials.create();
}
export async function getAzureCLICredentialsToken(): Promise<string> {
const credentials = await getAzureCLICredentials();
const token = (await credentials.getToken()).accessToken;
return token;
}

View File

@ -2,10 +2,7 @@ const msRestNodeAuth = require("@azure/ms-rest-nodeauth");
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb"); const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
const ms = require("ms"); const ms = require("ms");
const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"]; const subscriptionId = process.env["AZURE_SUBSCRIPTION_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 resourceGroupName = "runners"; const resourceGroupName = "runners";
const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime(); const thirtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 30).getTime();
@ -19,7 +16,7 @@ function friendlyTime(date) {
} }
async function main() { async function main() {
const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId); const credentials = await msRestNodeAuth.AzureCliCredentials.create();
const client = new CosmosDBManagementClient(credentials, subscriptionId); const client = new CosmosDBManagementClient(credentials, subscriptionId);
const accounts = await client.databaseAccounts.list(resourceGroupName); const accounts = await client.databaseAccounts.list(resourceGroupName);
for (const account of accounts) { for (const account of accounts) {
@ -38,7 +35,7 @@ async function main() {
} else if (account.capabilities.find((c) => c.name === "EnableCassandra")) { } else if (account.capabilities.find((c) => c.name === "EnableCassandra")) {
const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces( const cassandraDatabases = await client.cassandraResources.listCassandraKeyspaces(
resourceGroupName, resourceGroupName,
account.name account.name,
); );
for (const database of cassandraDatabases) { for (const database of cassandraDatabases) {
const timestamp = Number(database.resource._ts) * 1000; const timestamp = Number(database.resource._ts) * 1000;