diff --git a/e2e-tests/admin/pages/host-catalogs.js b/e2e-tests/admin/pages/host-catalogs.js index ac464e9db..f2a6af1f4 100644 --- a/e2e-tests/admin/pages/host-catalogs.js +++ b/e2e-tests/admin/pages/host-catalogs.js @@ -37,7 +37,7 @@ export class HostCatalogsPage extends BaseResourcePage { await this.page.getByLabel('Name').fill(hostCatalogName); await this.page.getByLabel('Description').fill('This is an automated test'); await this.page - .getByRole('group', { name: 'Type' }) + .getByRole('group', { name: 'Type', exact: 'true' }) .getByLabel('Static') .click(); await this.page.getByRole('button', { name: 'Save' }).click(); diff --git a/e2e-tests/admin/pages/sessions.js b/e2e-tests/admin/pages/sessions.js index b49492b5c..a4158536f 100644 --- a/e2e-tests/admin/pages/sessions.js +++ b/e2e-tests/admin/pages/sessions.js @@ -17,15 +17,16 @@ export class SessionsPage extends BasePage { .getByRole('navigation', { name: 'Resources' }) .getByRole('link', { name: 'Sessions' }) .click(); - await expect( - this.page - .getByRole('navigation', { name: 'breadcrumbs' }) - .getByText('Sessions'), - ).toBeVisible(); let i = 0; let sessionIsVisible = false; let sessionIsActive = false; do { + await expect( + this.page + .getByRole('navigation', { name: 'breadcrumbs' }) + .getByText('Sessions'), + ).toBeVisible(); + i = i + 1; sessionIsVisible = await this.page .getByRole('cell', { name: targetName }) diff --git a/e2e-tests/admin/playwright.config.js b/e2e-tests/admin/playwright.config.js index 87adcf57b..4e302ae3a 100644 --- a/e2e-tests/admin/playwright.config.js +++ b/e2e-tests/admin/playwright.config.js @@ -4,6 +4,7 @@ */ import { defineConfig, devices } from '@playwright/test'; +import { authenticatedState } from '../global-setup'; /** @type {import('@playwright/test').PlaywrightTestConfig} */ export default defineConfig({ @@ -13,12 +14,13 @@ export default defineConfig({ workers: 1, // Tests need to be run in serial, otherwise there may be conflicts when using the CLI use: { baseURL: process.env.BOUNDARY_ADDR, - trace: 'retain-on-failure', - screenshot: 'only-on-failure', extraHTTPHeaders: { // This token is set in global-setup.js Authorization: `Bearer ${process.env.E2E_TOKEN}`, }, + screenshot: 'only-on-failure', + storageState: authenticatedState, + trace: 'retain-on-failure', }, projects: [ { diff --git a/e2e-tests/admin/tests/alias-ent.spec.js b/e2e-tests/admin/tests/alias-ent.spec.js index fc6ba3a65..8c6fa99e3 100644 --- a/e2e-tests/admin/tests/alias-ent.spec.js +++ b/e2e-tests/admin/tests/alias-ent.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; import { customAlphabet } from 'nanoid'; @@ -12,10 +12,9 @@ import { AliasesPage } from '../pages/aliases.js'; import { CredentialStoresPage } from '../pages/credential-stores.js'; import { OrgsPage } from '../pages/orgs.js'; import { ProjectsPage } from '../pages/projects.js'; +import { SessionsPage } from '../pages/sessions.js'; import { TargetsPage } from '../pages/targets.js'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); }); @@ -35,6 +34,7 @@ test.describe('Aliases (Enterprise)', async () => { await page.goto('/'); let orgName; let alias; + let connect; const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 10); try { const orgsPage = new OrgsPage(page); @@ -83,9 +83,16 @@ test.describe('Aliases (Enterprise)', async () => { adminLoginName, adminPassword, ); - await boundaryCli.authorizeSessionByAlias(alias); + connect = await boundaryCli.connectSshToAlias(alias); + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); // Clear destination from alias + await page + .getByRole('navigation', { name: 'Resources' }) + .getByRole('link', { name: 'Targets' }) + .click(); + await page.getByRole('link', { name: targetName }).click(); await page.getByRole('link', { name: alias }).click(); // Note: On the Target details page, there is a section with the header // "Aliases". The extra check here is to ensure that we are on the Alias @@ -101,6 +108,9 @@ test.describe('Aliases (Enterprise)', async () => { ).toBeVisible(); await page.getByRole('button', { name: 'Dismiss' }).click(); } finally { + if (connect) { + connect.kill('SIGTERM'); + } await boundaryCli.authenticateBoundary( baseURL, adminAuthMethodId, @@ -133,6 +143,7 @@ test.describe('Aliases (Enterprise)', async () => { await page.goto('/'); let orgName; let alias; + let connect; const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 10); try { const orgsPage = new OrgsPage(page); @@ -165,8 +176,13 @@ test.describe('Aliases (Enterprise)', async () => { ); // Connect to target using alias - await boundaryCli.authorizeSessionByAlias(alias); + connect = await boundaryCli.connectSshToAlias(alias); + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); } finally { + if (connect) { + connect.kill('SIGTERM'); + } await boundaryCli.authenticateBoundary( baseURL, adminAuthMethodId, @@ -201,6 +217,7 @@ test.describe('Aliases (Enterprise)', async () => { const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 10); let orgId; let alias; + let connect; try { const orgsPage = new OrgsPage(page); const orgName = await orgsPage.createOrg(); @@ -243,8 +260,19 @@ test.describe('Aliases (Enterprise)', async () => { alias = 'example.alias.' + nanoid(); const aliasesPage = new AliasesPage(page); await aliasesPage.createAliasForTarget(alias, targetId); - await boundaryCli.authorizeSessionByAlias(alias); + connect = await boundaryCli.connectSshToAlias(alias); + await page + .getByRole('navigation', { name: 'General' }) + .getByRole('link', { name: 'Orgs' }) + .click(); + await page.getByRole('link', { name: orgName }).click(); + await page.getByRole('link', { name: projectName }).click(); + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); } finally { + if (connect) { + connect.kill('SIGTERM'); + } await boundaryCli.authenticateBoundary( baseURL, adminAuthMethodId, diff --git a/e2e-tests/admin/tests/alias.spec.js b/e2e-tests/admin/tests/alias.spec.js index 441d965ef..6fa355d3c 100644 --- a/e2e-tests/admin/tests/alias.spec.js +++ b/e2e-tests/admin/tests/alias.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; import { customAlphabet } from 'nanoid'; @@ -11,10 +11,9 @@ import * as boundaryCli from '../../helpers/boundary-cli'; import { AliasesPage } from '../pages/aliases.js'; import { OrgsPage } from '../pages/orgs.js'; import { ProjectsPage } from '../pages/projects.js'; +import { SessionsPage } from '../pages/sessions.js'; import { TargetsPage } from '../pages/targets.js'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); }); @@ -28,10 +27,13 @@ test.describe('Aliases', async () => { adminPassword, targetAddress, targetPort, + sshUser, + sshKeyPath, }) => { await page.goto('/'); let orgName; let alias; + let connect; const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 10); try { const orgsPage = new OrgsPage(page); @@ -39,7 +41,10 @@ test.describe('Aliases', async () => { const projectsPage = new ProjectsPage(page); await projectsPage.createProject(); const targetsPage = new TargetsPage(page); - await targetsPage.createTargetWithAddress(targetAddress, targetPort); + const targetName = await targetsPage.createTargetWithAddress( + targetAddress, + targetPort, + ); // Create alias for target const aliasName = 'Alias ' + nanoid(); @@ -61,9 +66,16 @@ test.describe('Aliases', async () => { adminLoginName, adminPassword, ); - await boundaryCli.authorizeSessionByAlias(alias); + connect = await boundaryCli.connectToAlias(alias, sshUser, sshKeyPath); + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); // Clear destination from alias + await page + .getByRole('navigation', { name: 'Resources' }) + .getByRole('link', { name: 'Targets' }) + .click(); + await page.getByRole('link', { name: targetName }).click(); await page.getByRole('link', { name: alias }).click(); // Note: On the Target details page, there is a section with the header // "Aliases". The extra check here is to ensure that we are on the Alias @@ -79,6 +91,9 @@ test.describe('Aliases', async () => { ).toBeVisible(); await page.getByRole('button', { name: 'Dismiss' }).click(); } finally { + if (connect) { + connect.kill('SIGTERM'); + } await boundaryCli.authenticateBoundary( baseURL, adminAuthMethodId, @@ -105,10 +120,13 @@ test.describe('Aliases', async () => { adminPassword, targetAddress, targetPort, + sshUser, + sshKeyPath, }) => { await page.goto('/'); let orgName; let alias; + let connect; const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 10); try { const orgsPage = new OrgsPage(page); @@ -123,15 +141,20 @@ test.describe('Aliases', async () => { await projectsPage.createProject(); alias = 'example.alias.' + nanoid(); const targetsPage = new TargetsPage(page); - await targetsPage.createTargetWithAddressAndAlias( + const targetName = await targetsPage.createTargetWithAddressAndAlias( targetAddress, targetPort, alias, ); // Connect to target using alias - await boundaryCli.authorizeSessionByAlias(alias); + connect = await boundaryCli.connectToAlias(alias, sshUser, sshKeyPath); + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); } finally { + if (connect) { + connect.kill('SIGTERM'); + } await boundaryCli.authenticateBoundary( baseURL, adminAuthMethodId, @@ -158,11 +181,14 @@ test.describe('Aliases', async () => { adminPassword, targetAddress, targetPort, + sshUser, + sshKeyPath, }) => { await page.goto('/'); const nanoid = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 10); let orgId; let alias; + let connect; try { const orgsPage = new OrgsPage(page); const orgName = await orgsPage.createOrg(); @@ -194,8 +220,18 @@ test.describe('Aliases', async () => { alias = 'example.alias.' + nanoid(); const aliasesPage = new AliasesPage(page); await aliasesPage.createAliasForTarget(alias, targetId); - await boundaryCli.authorizeSessionByAlias(alias); + await page.getByRole('link', { name: 'Orgs', exact: true }).click(); + await expect(page.getByRole('heading', { name: 'Orgs' })).toBeVisible(); + await page.getByRole('link', { name: orgName }).click(); + await page.getByRole('link', { name: projectName }).click(); + + connect = await boundaryCli.connectToAlias(alias, sshUser, sshKeyPath); + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); } finally { + if (connect) { + connect.kill('SIGTERM'); + } await boundaryCli.authenticateBoundary( baseURL, adminAuthMethodId, diff --git a/e2e-tests/admin/tests/auth-method-ldap.spec.js b/e2e-tests/admin/tests/auth-method-ldap.spec.js index 084571359..3614facb3 100644 --- a/e2e-tests/admin/tests/auth-method-ldap.spec.js +++ b/e2e-tests/admin/tests/auth-method-ldap.spec.js @@ -14,6 +14,9 @@ import { OrgsPage } from '../pages/orgs.js'; import { RolesPage } from '../pages/roles.js'; import { UsersPage } from '../pages/users.js'; +// Reset storage state for this file to avoid being authenticated +test.use({ storageState: { cookies: [], origins: [] } }); + test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); }); diff --git a/e2e-tests/admin/tests/auth-method-password.spec.js b/e2e-tests/admin/tests/auth-method-password.spec.js index 7d2f0698a..a4db707e9 100644 --- a/e2e-tests/admin/tests/auth-method-password.spec.js +++ b/e2e-tests/admin/tests/auth-method-password.spec.js @@ -13,6 +13,9 @@ import { LoginPage } from '../pages/login.js'; import { OrgsPage } from '../pages/orgs.js'; import { UsersPage } from '../pages/users.js'; +// Reset storage state for this file to avoid being authenticated +test.use({ storageState: { cookies: [], origins: [] } }); + test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); }); diff --git a/e2e-tests/admin/tests/credential-store-static-ent.spec.js b/e2e-tests/admin/tests/credential-store-static-ent.spec.js index 8e2a752ab..6579bdaee 100644 --- a/e2e-tests/admin/tests/credential-store-static-ent.spec.js +++ b/e2e-tests/admin/tests/credential-store-static-ent.spec.js @@ -3,17 +3,17 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; +import { readFile } from 'fs/promises'; import * as boundaryCli from '../../helpers/boundary-cli'; import { CredentialStoresPage } from '../pages/credential-stores.js'; import { OrgsPage } from '../pages/orgs.js'; import { ProjectsPage } from '../pages/projects.js'; +import { SessionsPage } from '../pages/sessions.js'; import { TargetsPage } from '../pages/targets.js'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); }); @@ -36,7 +36,7 @@ test('Multiple Credential Stores (ENT) @ent @aws @docker', async ({ const orgsPage = new OrgsPage(page); orgName = await orgsPage.createOrg(); const projectsPage = new ProjectsPage(page); - await projectsPage.createProject(); + const projectName = await projectsPage.createProject(); const targetsPage = new TargetsPage(page); const targetName = await targetsPage.createSshTargetWithAddressEnt( targetAddress, @@ -100,6 +100,36 @@ test('Multiple Credential Stores (ENT) @ent @aws @docker', async ({ page.getByRole('alert').getByText('Success', { exact: true }), ).toBeVisible(); await page.getByRole('button', { name: 'Dismiss' }).click(); + + // Verify credentials + await boundaryCli.authenticateBoundary( + baseURL, + adminAuthMethodId, + adminLoginName, + adminPassword, + ); + const orgId = await boundaryCli.getOrgIdFromName(orgName); + const projectId = await boundaryCli.getProjectIdFromName( + orgId, + projectName, + ); + const targetId = await boundaryCli.getTargetIdFromName( + projectId, + targetName, + ); + const session = await boundaryCli.authorizeSessionByTargetId(targetId); + const retrievedUser = session.item.credentials[0].credential.username; + const retrievedKey = session.item.credentials[0].credential.private_key; + + expect(retrievedUser).toBe(sshUser); + + const keyData = await readFile(sshKeyPath, { + encoding: 'utf-8', + }); + expect(retrievedKey).toBe(keyData); + + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); } finally { if (orgName) { await boundaryCli.authenticateBoundary( diff --git a/e2e-tests/admin/tests/credential-store-static.spec.js b/e2e-tests/admin/tests/credential-store-static.spec.js index cd9c251bf..c3dbf33c5 100644 --- a/e2e-tests/admin/tests/credential-store-static.spec.js +++ b/e2e-tests/admin/tests/credential-store-static.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; import { readFile } from 'fs/promises'; import { nanoid } from 'nanoid'; @@ -12,10 +12,9 @@ import * as boundaryCli from '../../helpers/boundary-cli'; import { CredentialStoresPage } from '../pages/credential-stores.js'; import { OrgsPage } from '../pages/orgs.js'; import { ProjectsPage } from '../pages/projects.js'; +import { SessionsPage } from '../pages/sessions.js'; import { TargetsPage } from '../pages/targets.js'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); }); @@ -86,9 +85,10 @@ test('Static Credential Store (User & Key Pair) @ce @aws @docker', async ({ const keyData = await readFile(sshKeyPath, { encoding: 'utf-8', }); - if (keyData != retrievedKey) { - throw new Error('Stored Key does not match'); - } + expect(retrievedKey).toBe(keyData); + + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); } finally { if (orgName) { await boundaryCli.authenticateBoundary( @@ -148,6 +148,9 @@ test('Static Credential Store (Username & Password) @ce @aws @docker', async ({ expect(retrievedUser).toBe(sshUser); expect(retrievedPassword).toBe(testPassword); + + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); } finally { if (orgName) { await boundaryCli.authenticateBoundary( @@ -227,6 +230,9 @@ test('Static Credential Store (JSON) @ce @aws @docker', async ({ expect(retrievedUser).toBe(testName); expect(retrievedPassword).toBe(testPassword); expect(retrievedId).toBe(testId); + + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); } finally { if (orgName) { await boundaryCli.authenticateBoundary( diff --git a/e2e-tests/admin/tests/credential-store-vault-ent.spec.js b/e2e-tests/admin/tests/credential-store-vault-ent.spec.js index a8d66e808..00b218922 100644 --- a/e2e-tests/admin/tests/credential-store-vault-ent.spec.js +++ b/e2e-tests/admin/tests/credential-store-vault-ent.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { execSync } from 'child_process'; import * as boundaryCli from '../../helpers/boundary-cli'; @@ -11,6 +11,7 @@ import * as vaultCli from '../../helpers/vault-cli'; import { CredentialStoresPage } from '../pages/credential-stores.js'; import { OrgsPage } from '../pages/orgs.js'; import { ProjectsPage } from '../pages/projects.js'; +import { SessionsPage } from '../pages/sessions.js'; import { TargetsPage } from '../pages/targets.js'; const secretsPath = 'e2e_secrets'; @@ -18,8 +19,6 @@ const secretName = 'cred'; const secretPolicyName = 'kv-policy'; const boundaryPolicyName = 'boundary-controller'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); await vaultCli.checkVaultCli(); @@ -46,6 +45,7 @@ test('Vault Credential Store (User & Key Pair) @ent @aws @docker', async ({ vaultAddr, }) => { let orgId; + let connect; try { execSync( `vault policy write ${boundaryPolicyName} ./admin/tests/fixtures/boundary-controller-policy.hcl`, @@ -97,6 +97,7 @@ test('Vault Credential Store (User & Key Pair) @ent @aws @docker', async ({ credentialLibraryName, ); + // Verify that session can be established await boundaryCli.authenticateBoundary( baseURL, adminAuthMethodId, @@ -112,8 +113,14 @@ test('Vault Credential Store (User & Key Pair) @ent @aws @docker', async ({ projectId, targetName, ); - await boundaryCli.authorizeSessionByTargetId(targetId); + connect = await boundaryCli.connectSshToTarget(targetId); + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); } finally { + if (connect) { + connect.kill('SIGTERM'); + } + if (orgId) { await boundaryCli.deleteScope(orgId); } diff --git a/e2e-tests/admin/tests/credential-store-vault.spec.js b/e2e-tests/admin/tests/credential-store-vault.spec.js index bb303138e..b848f7e1f 100644 --- a/e2e-tests/admin/tests/credential-store-vault.spec.js +++ b/e2e-tests/admin/tests/credential-store-vault.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; import { execSync } from 'child_process'; import { nanoid } from 'nanoid'; @@ -14,6 +14,7 @@ import * as vaultCli from '../../helpers/vault-cli'; import { CredentialStoresPage } from '../pages/credential-stores.js'; import { OrgsPage } from '../pages/orgs.js'; import { ProjectsPage } from '../pages/projects.js'; +import { SessionsPage } from '../pages/sessions.js'; import { TargetsPage } from '../pages/targets.js'; const secretsPath = 'e2e_secrets'; @@ -21,8 +22,6 @@ const secretName = 'cred'; const secretPolicyName = 'kv-policy'; const boundaryPolicyName = 'boundary-controller'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); await vaultCli.checkVaultCli(); @@ -114,6 +113,7 @@ test('Vault Credential Store (User & Key Pair) @ce @aws @docker', async ({ credentialLibraryName, ); + // Verify correct credentials are returned after authorizing session await boundaryCli.authenticateBoundary( baseURL, adminAuthMethodId, @@ -136,13 +136,13 @@ test('Vault Credential Store (User & Key Pair) @ce @aws @docker', async ({ session.item.credentials[0].secret.decoded.data.private_key; expect(retrievedUser).toBe(sshUser); - const keyData = await readFile(sshKeyPath, { encoding: 'utf-8', }); - if (keyData !== retrievedKey) { - throw new Error('Stored Key does not match'); - } + expect(retrievedKey).toBe(keyData); + + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); } finally { if (orgId) { await boundaryCli.deleteScope(orgId); diff --git a/e2e-tests/admin/tests/delete-resources-ent.spec.js b/e2e-tests/admin/tests/delete-resources-ent.spec.js index eefdbac98..3606a4441 100644 --- a/e2e-tests/admin/tests/delete-resources-ent.spec.js +++ b/e2e-tests/admin/tests/delete-resources-ent.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { execSync } from 'node:child_process'; import * as boundaryCli from '../../helpers/boundary-cli'; @@ -15,8 +15,6 @@ import { WorkersPage } from '../pages/workers.js'; const secretPolicyName = 'kv-policy'; const boundaryPolicyName = 'boundary-controller'; -test.use({ storageState: authenticatedState }); - // Setting the test timeout to 120s // This test can often exceed the globally defined timeout due to the number of // network calls. diff --git a/e2e-tests/admin/tests/delete-resources.spec.js b/e2e-tests/admin/tests/delete-resources.spec.js index a3904630a..265fd5356 100644 --- a/e2e-tests/admin/tests/delete-resources.spec.js +++ b/e2e-tests/admin/tests/delete-resources.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { execSync } from 'node:child_process'; import * as boundaryCli from '../../helpers/boundary-cli'; @@ -15,8 +15,6 @@ import { WorkersPage } from '../pages/workers.js'; const secretPolicyName = 'kv-policy'; const boundaryPolicyName = 'boundary-controller'; -test.use({ storageState: authenticatedState }); - // Setting the test timeout to 180s // This test can often exceed the globally defined timeout due to the number of // network calls. diff --git a/e2e-tests/admin/tests/dynamic-host-catalog-aws-ent.spec.js b/e2e-tests/admin/tests/dynamic-host-catalog-aws-ent.spec.js new file mode 100644 index 000000000..91ceb1608 --- /dev/null +++ b/e2e-tests/admin/tests/dynamic-host-catalog-aws-ent.spec.js @@ -0,0 +1,236 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { test } from '../../global-setup.js'; +import { expect } from '@playwright/test'; +import { nanoid } from 'nanoid'; + +import * as boundaryCli from '../../helpers/boundary-cli'; +import { CredentialStoresPage } from '../pages/credential-stores.js'; +import { HostCatalogsPage } from '../pages/host-catalogs.js'; +import { OrgsPage } from '../pages/orgs.js'; +import { ProjectsPage } from '../pages/projects.js'; +import { SessionsPage } from '../pages/sessions.js'; +import { TargetsPage } from '../pages/targets.js'; + +test.beforeEach(async ({ page }) => { + await page.goto('/'); +}); + +test.describe('AWS', async () => { + test('Create an AWS Dynamic Host Catalog and set up Host Sets @ent @aws', async ({ + page, + baseURL, + adminAuthMethodId, + adminLoginName, + adminPassword, + awsAccessKeyId, + awsHostSetFilter, + awsHostSetIps, + awsRegion, + awsSecretAccessKey, + targetPort, + sshUser, + sshKeyPath, + }) => { + let orgName; + let connect; + try { + const orgsPage = new OrgsPage(page); + orgName = await orgsPage.createOrg(); + const projectsPage = new ProjectsPage(page); + const projectName = await projectsPage.createProject(); + + // Create host catalog + const hostCatalogName = 'Host Catalog ' + nanoid(); + await page + .getByRole('navigation', { name: 'Resources' }) + .getByRole('link', { name: 'Host Catalogs' }) + .click(); + await page.getByRole('link', { name: 'New', exact: true }).click(); + await page.getByLabel('Name').fill(hostCatalogName); + await page.getByLabel('Description').fill('This is an automated test'); + await page + .getByRole('group', { name: 'Type' }) + .getByLabel('Dynamic') + .click(); + await page + .getByRole('group', { name: 'Provider' }) + .getByLabel('AWS') + .click(); + await page.getByLabel('AWS Region').fill(awsRegion); + await page.getByLabel('Access Key ID').fill(awsAccessKeyId); + await page.getByLabel('Secret Access Key').fill(awsSecretAccessKey); + await page + .getByLabel('Disable credential rotation') + .click({ force: true }); + await page.getByRole('button', { name: 'Save' }).click(); + await expect( + page.getByRole('alert').getByText('Success', { exact: true }), + ).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + await expect( + page + .getByRole('navigation', { name: 'breadcrumbs' }) + .getByText(hostCatalogName), + ).toBeVisible(); + + // Create first host set + const hostSetName = 'Host Set ' + nanoid(); + await page.getByRole('link', { name: 'Host Sets' }).click(); + await page.getByRole('link', { name: 'New', exact: true }).click(); + await page.getByLabel('Name (Optional)').fill(hostSetName); + await page.getByLabel('Description').fill('This is an automated test'); + await page + .getByRole('group', { name: 'Filter' }) + .getByRole('textbox') + .fill(awsHostSetFilter); + await page + .getByRole('group', { name: 'Filter' }) + .getByRole('button', { name: 'Add' }) + .click(); + await page.getByRole('button', { name: 'Save' }).click(); + await expect( + page.getByRole('alert').getByText('Success', { exact: true }), + ).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + await expect( + page + .getByRole('navigation', { name: 'breadcrumbs' }) + .getByText(hostSetName), + ).toBeVisible(); + + await page.getByRole('link', { name: 'Hosts' }).click(); + await expect( + page + .getByRole('navigation', { name: 'breadcrumbs' }) + .getByText('Hosts'), + ).toBeVisible(); + + // Check number of hosts in host set + const expectedHosts = JSON.parse(awsHostSetIps); + await expect(async () => { + const rowCount = await page + .getByRole('table') + .getByRole('row') + .filter({ hasNot: page.getByRole('columnheader') }) + .count(); + + if (rowCount !== expectedHosts.length) { + await page.reload(); + await page + .getByRole('navigation', { name: 'breadcrumbs' }) + .getByText(hostSetName) + .waitFor(); + } + expect(rowCount).toBe(expectedHosts.length); + }).toPass(); + + // Navigate to each host in the host set + for (const row of await page + .getByRole('table') + .getByRole('rowgroup') + .nth(1) + .getByRole('row') + .all()) { + const host = row.getByRole('link'); + let hostName = await host.innerText(); + await host.click(); + await expect( + page + .getByRole('navigation', { name: 'breadcrumbs' }) + .getByText(hostName), + ).toBeVisible(); + + await page + .getByRole('navigation', { name: 'breadcrumbs' }) + .getByText(hostSetName) + .click(); + await page.getByRole('link', { name: 'Hosts' }).click(); + } + + // Create a target and add DHC host set as a host source + const targetsPage = new TargetsPage(page); + const targetName = await targetsPage.createSshTargetEnt(targetPort); + await targetsPage.addHostSourceToTarget(hostSetName); + + // Add another host source + const hostCatalogsPage = new HostCatalogsPage(page); + await hostCatalogsPage.createHostCatalog(); + const newHostSetName = await hostCatalogsPage.createHostSet(); + await page + .getByRole('navigation', { name: 'Resources' }) + .getByRole('link', { name: 'Targets' }) + .click(); + await page.getByRole('link', { name: targetName }).click(); + await targetsPage.addHostSourceToTarget(newHostSetName); + + // Remove the host source from the target + await page + .getByRole('link', { name: newHostSetName }) + .locator('..') + .locator('..') + .getByRole('button', { name: 'Manage' }) + .click(); + await page.getByRole('button', { name: 'Remove' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect( + page.getByRole('alert').getByText('Success', { exact: true }), + ).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + + // Set up target credentials + const credentialStoresPage = new CredentialStoresPage(page); + await credentialStoresPage.createStaticCredentialStore(); + const credentialName = + await credentialStoresPage.createStaticCredentialKeyPair( + sshUser, + sshKeyPath, + ); + await targetsPage.addInjectedCredentialsToTarget( + targetName, + credentialName, + ); + + // Connect to target + await boundaryCli.authenticateBoundary( + baseURL, + adminAuthMethodId, + adminLoginName, + adminPassword, + ); + const orgId = await boundaryCli.getOrgIdFromName(orgName); + const projectId = await boundaryCli.getProjectIdFromName( + orgId, + projectName, + ); + const targetId = await boundaryCli.getTargetIdFromName( + projectId, + targetName, + ); + connect = await boundaryCli.connectSshToTarget(targetId); + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); + } finally { + if (connect) { + connect.kill('SIGTERM'); + } + + await boundaryCli.authenticateBoundary( + baseURL, + adminAuthMethodId, + adminLoginName, + adminPassword, + ); + + if (orgName) { + const orgId = await boundaryCli.getOrgIdFromName(orgName); + if (orgId) { + await boundaryCli.deleteScope(orgId); + } + } + } + }); +}); diff --git a/e2e-tests/admin/tests/dynamic-host-catalog-aws.spec.js b/e2e-tests/admin/tests/dynamic-host-catalog-aws.spec.js index a3a9c0a00..bce71d342 100644 --- a/e2e-tests/admin/tests/dynamic-host-catalog-aws.spec.js +++ b/e2e-tests/admin/tests/dynamic-host-catalog-aws.spec.js @@ -3,24 +3,24 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; import { nanoid } from 'nanoid'; import * as boundaryCli from '../../helpers/boundary-cli'; +import { CredentialStoresPage } from '../pages/credential-stores.js'; import { HostCatalogsPage } from '../pages/host-catalogs.js'; import { OrgsPage } from '../pages/orgs.js'; import { ProjectsPage } from '../pages/projects.js'; +import { SessionsPage } from '../pages/sessions.js'; import { TargetsPage } from '../pages/targets.js'; -test.use({ storageState: authenticatedState }); - test.beforeEach(async ({ page }) => { await page.goto('/'); }); test.describe('AWS', async () => { - test('Create an AWS Dynamic Host Catalog and set up Host Sets @ce @ent @aws', async ({ + test('Create an AWS Dynamic Host Catalog and set up Host Sets @ce @aws', async ({ page, baseURL, adminAuthMethodId, @@ -32,13 +32,16 @@ test.describe('AWS', async () => { awsRegion, awsSecretAccessKey, targetPort, + sshUser, + sshKeyPath, }) => { let orgName; + let connect; try { const orgsPage = new OrgsPage(page); orgName = await orgsPage.createOrg(); const projectsPage = new ProjectsPage(page); - await projectsPage.createProject(); + const projectName = await projectsPage.createProject(); // Create host catalog const hostCatalogName = 'Host Catalog ' + nanoid(); @@ -107,37 +110,32 @@ test.describe('AWS', async () => { ).toBeVisible(); // Check number of hosts in host set - let i = 0; - let rowCount = 0; - let expectedHosts = JSON.parse(awsHostSetIps); - do { - i = i + 1; - // Getting the number of rows in the second rowgroup (the first rowgroup is the header row) - rowCount = await page + const expectedHosts = JSON.parse(awsHostSetIps); + await expect(async () => { + const rowCount = await page .getByRole('table') - .getByRole('rowgroup') - .nth(1) .getByRole('row') + .filter({ hasNot: page.getByRole('columnheader') }) .count(); - await page.reload(); - await page - .getByRole('navigation', { name: 'breadcrumbs' }) - .getByText(hostSetName) - .waitFor(); - } while (i < 5); - expect(rowCount).toBe(expectedHosts.length); + if (rowCount !== expectedHosts.length) { + await page.reload(); + await page + .getByRole('navigation', { name: 'breadcrumbs' }) + .getByText(hostSetName) + .waitFor(); + } + expect(rowCount).toBe(expectedHosts.length); + }).toPass(); // Navigate to each host in the host set - for (let i = 0; i < expectedHosts.length; i++) { - const host = await page - .getByRole('table') - .getByRole('rowgroup') - .nth(1) - .getByRole('row') - .nth(i) - .getByRole('link'); - + for (const row of await page + .getByRole('table') + .getByRole('rowgroup') + .nth(1) + .getByRole('row') + .all()) { + const host = row.getByRole('link'); let hostName = await host.innerText(); await host.click(); await expect( @@ -183,9 +181,43 @@ test.describe('AWS', async () => { ).toBeVisible(); await page.getByRole('button', { name: 'Dismiss' }).click(); - // Add the host source back - await targetsPage.addHostSourceToTarget(newHostSetName); + // Set up target credentials + const credentialStoresPage = new CredentialStoresPage(page); + await credentialStoresPage.createStaticCredentialStore(); + const credentialName = + await credentialStoresPage.createStaticCredentialKeyPair( + sshUser, + sshKeyPath, + ); + await targetsPage.addBrokeredCredentialsToTarget( + targetName, + credentialName, + ); + + // Connect to target + await boundaryCli.authenticateBoundary( + baseURL, + adminAuthMethodId, + adminLoginName, + adminPassword, + ); + const orgId = await boundaryCli.getOrgIdFromName(orgName); + const projectId = await boundaryCli.getProjectIdFromName( + orgId, + projectName, + ); + const targetId = await boundaryCli.getTargetIdFromName( + projectId, + targetName, + ); + connect = await boundaryCli.connectSshToTarget(targetId); + const sessionsPage = new SessionsPage(page); + await sessionsPage.waitForSessionToBeVisible(targetName); } finally { + if (connect) { + connect.kill('SIGTERM'); + } + await boundaryCli.authenticateBoundary( baseURL, adminAuthMethodId, diff --git a/e2e-tests/admin/tests/global-settings.spec.js b/e2e-tests/admin/tests/global-settings.spec.js index b920af96a..670e3a1cc 100644 --- a/e2e-tests/admin/tests/global-settings.spec.js +++ b/e2e-tests/admin/tests/global-settings.spec.js @@ -3,15 +3,13 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; import * as boundaryCli from '../../helpers/boundary-cli'; import { OrgsPage } from '../pages/orgs.js'; import { StoragePoliciesPage } from '../pages/storage-policies.js'; -test.use({ storageState: authenticatedState }); - test('Global Settings @ent @aws @docker', async ({ page, baseURL, diff --git a/e2e-tests/admin/tests/group-role.spec.js b/e2e-tests/admin/tests/group-role.spec.js index 3a7a5b592..f135dcb64 100644 --- a/e2e-tests/admin/tests/group-role.spec.js +++ b/e2e-tests/admin/tests/group-role.spec.js @@ -3,15 +3,13 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import * as boundaryCli from '../../helpers/boundary-cli'; import { GroupsPage } from '../pages/groups.js'; import { OrgsPage } from '../pages/orgs.js'; import { RolesPage } from '../pages/roles.js'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); }); diff --git a/e2e-tests/admin/tests/login.spec.js b/e2e-tests/admin/tests/login.spec.js index 0e4e19f63..16dd760ed 100644 --- a/e2e-tests/admin/tests/login.spec.js +++ b/e2e-tests/admin/tests/login.spec.js @@ -8,6 +8,9 @@ import { expect } from '@playwright/test'; import { LoginPage } from '../pages/login.js'; +// Reset storage state for this file to avoid being authenticated +test.use({ storageState: { cookies: [], origins: [] } }); + test('Log in, log out, and then log back in @ce @ent @aws @docker', async ({ page, adminLoginName, diff --git a/e2e-tests/admin/tests/pagination.spec.js b/e2e-tests/admin/tests/pagination.spec.js index b796574bf..4fa270f03 100644 --- a/e2e-tests/admin/tests/pagination.spec.js +++ b/e2e-tests/admin/tests/pagination.spec.js @@ -3,13 +3,11 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; import * as boundaryHttp from '../../helpers/boundary-http.js'; -test.use({ storageState: authenticatedState }); - test('Search and Pagination (Targets) @ce @ent @aws @docker', async ({ page, request, diff --git a/e2e-tests/admin/tests/session-recording-aws-ent.spec.js b/e2e-tests/admin/tests/session-recording-aws-ent.spec.js index 1b2cd8bf9..1119cc326 100644 --- a/e2e-tests/admin/tests/session-recording-aws-ent.spec.js +++ b/e2e-tests/admin/tests/session-recording-aws-ent.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; import { execSync } from 'child_process'; @@ -17,8 +17,6 @@ import { StorageBucketsPage } from '../pages/storage-buckets.js'; import { StoragePoliciesPage } from '../pages/storage-policies.js'; import { TargetsPage } from '../pages/targets.js'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); }); @@ -227,6 +225,11 @@ test('Session Recording Test (AWS) @ent @aws', async ({ await page.getByRole('link', { name: targetName }).click(); await targetsPage.detachStorageBucket(); } finally { + // End `boundary connect` process + if (connect) { + connect.kill('SIGTERM'); + } + if (policyName) { const storagePolicyId = await boundaryCli.getPolicyIdFromName( orgId, @@ -240,9 +243,5 @@ test('Session Recording Test (AWS) @ent @aws', async ({ if (orgId) { await boundaryCli.deleteScope(orgId); } - // End `boundary connect` process - if (connect) { - connect.kill('SIGTERM'); - } } }); diff --git a/e2e-tests/admin/tests/session-recording-minio-ent.spec.js b/e2e-tests/admin/tests/session-recording-minio-ent.spec.js index db2090275..4fc636113 100644 --- a/e2e-tests/admin/tests/session-recording-minio-ent.spec.js +++ b/e2e-tests/admin/tests/session-recording-minio-ent.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; import { execSync } from 'child_process'; @@ -17,8 +17,6 @@ import { StorageBucketsPage } from '../pages/storage-buckets.js'; import { StoragePoliciesPage } from '../pages/storage-policies.js'; import { TargetsPage } from '../pages/targets.js'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); }); @@ -234,6 +232,11 @@ test('Session Recording Test (MinIO) @ent @docker', async ({ await page.getByRole('link', { name: targetName }).click(); await targetsPage.detachStorageBucket(); } finally { + // End `boundary connect` process + if (connect) { + connect.kill('SIGTERM'); + } + if (policyName) { const storagePolicyId = await boundaryCli.getPolicyIdFromName( orgId, @@ -247,9 +250,5 @@ test('Session Recording Test (MinIO) @ent @docker', async ({ if (orgId) { await boundaryCli.deleteScope(orgId); } - // End `boundary connect` process - if (connect) { - connect.kill('SIGTERM'); - } } }); diff --git a/e2e-tests/admin/tests/ssh-certificate-injection-vault-ent.spec.js b/e2e-tests/admin/tests/ssh-certificate-injection-vault-ent.spec.js index 9a53e6e8c..45b77a253 100644 --- a/e2e-tests/admin/tests/ssh-certificate-injection-vault-ent.spec.js +++ b/e2e-tests/admin/tests/ssh-certificate-injection-vault-ent.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { execSync } from 'child_process'; import * as boundaryCli from '../../helpers/boundary-cli'; @@ -21,8 +21,6 @@ const secretsPath = 'e2e_secrets'; // This must match the secret name in ssh-policy.hcl const secretName = 'boundary-client'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); await vaultCli.checkVaultCli(); @@ -61,8 +59,8 @@ test('SSH Certificate Injection @ent @docker', async ({ `vault write ${secretsPath}/roles/${secretName} @./admin/tests/fixtures/ssh-certificate-injection-role.json`, ); - const private_key = atob(sshCaKey); - const public_key = atob(sshCaKeyPublic); + const private_key = Buffer.from(sshCaKey, 'base64'); + const public_key = Buffer.from(sshCaKeyPublic, 'base64'); execSync( `vault write ${secretsPath}/config/ca` + @@ -136,20 +134,16 @@ test('SSH Certificate Injection @ent @docker', async ({ connect = await boundaryCli.connectSshToTarget(targetId); const sessionsPage = new SessionsPage(page); await sessionsPage.waitForSessionToBeVisible(targetName); - await page - .getByRole('cell', { name: targetName }) - .locator('..') - .getByRole('button', { name: 'Cancel' }) - .click(); } finally { - if (orgId) { - await boundaryCli.deleteScope(orgId); - } // End `boundary connect` process if (connect) { connect.kill('SIGTERM'); } + if (orgId) { + await boundaryCli.deleteScope(orgId); + } + execSync(`vault secrets disable ${secretsPath}`); execSync(`vault policy delete ${sshPolicyName}`); execSync(`vault policy delete ${boundaryPolicyName}`); diff --git a/e2e-tests/admin/tests/ssh-credential-injection-vault-ent.spec.js b/e2e-tests/admin/tests/ssh-credential-injection-vault-ent.spec.js index 779f3ad7c..34023017c 100644 --- a/e2e-tests/admin/tests/ssh-credential-injection-vault-ent.spec.js +++ b/e2e-tests/admin/tests/ssh-credential-injection-vault-ent.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { execSync } from 'child_process'; import * as boundaryCli from '../../helpers/boundary-cli'; @@ -19,8 +19,6 @@ const secretName = 'cred'; const secretPolicyName = 'kv-policy'; const boundaryPolicyName = 'boundary-controller'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); await vaultCli.checkVaultCli(); @@ -126,18 +124,14 @@ test('SSH Credential Injection (Vault User & Key Pair) @ent @docker', async ({ connect = await boundaryCli.connectSshToTarget(targetId); const sessionsPage = new SessionsPage(page); await sessionsPage.waitForSessionToBeVisible(targetName); - await page - .getByRole('cell', { name: targetName }) - .locator('..') - .getByRole('button', { name: 'Cancel' }) - .click(); } finally { - if (orgId) { - await boundaryCli.deleteScope(orgId); - } // End `boundary connect` process if (connect) { connect.kill('SIGTERM'); } + + if (orgId) { + await boundaryCli.deleteScope(orgId); + } } }); diff --git a/e2e-tests/admin/tests/target-ent.spec.js b/e2e-tests/admin/tests/target-ent.spec.js index 7db10081a..d9e6f177f 100644 --- a/e2e-tests/admin/tests/target-ent.spec.js +++ b/e2e-tests/admin/tests/target-ent.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; import * as boundaryCli from '../../helpers/boundary-cli'; @@ -14,8 +14,6 @@ import { ProjectsPage } from '../pages/projects.js'; import { SessionsPage } from '../pages/sessions.js'; import { TargetsPage } from '../pages/targets.js'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); }); diff --git a/e2e-tests/admin/tests/target.spec.js b/e2e-tests/admin/tests/target.spec.js index 565e56766..e6f59c89d 100644 --- a/e2e-tests/admin/tests/target.spec.js +++ b/e2e-tests/admin/tests/target.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; import * as boundaryCli from '../../helpers/boundary-cli'; @@ -14,8 +14,6 @@ import { ProjectsPage } from '../pages/projects.js'; import { SessionsPage } from '../pages/sessions.js'; import { TargetsPage } from '../pages/targets.js'; -test.use({ storageState: authenticatedState }); - test.beforeAll(async () => { await boundaryCli.checkBoundaryCli(); }); diff --git a/e2e-tests/admin/tests/worker-ent.spec.js b/e2e-tests/admin/tests/worker-ent.spec.js index fcfacb0e4..9faec160b 100644 --- a/e2e-tests/admin/tests/worker-ent.spec.js +++ b/e2e-tests/admin/tests/worker-ent.spec.js @@ -3,11 +3,9 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; -test.use({ storageState: authenticatedState }); - test('Create a worker (enterprise) @ent @docker @aws', async ({ page, browserName, diff --git a/e2e-tests/admin/tests/worker-tags.spec.js b/e2e-tests/admin/tests/worker-tags.spec.js index ca7eb7653..414e62271 100644 --- a/e2e-tests/admin/tests/worker-tags.spec.js +++ b/e2e-tests/admin/tests/worker-tags.spec.js @@ -3,14 +3,12 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; import { customAlphabet } from 'nanoid'; import { WorkersPage } from '../pages/workers.js'; -test.use({ storageState: authenticatedState }); - test('Worker Tags @ce @ent @aws @docker', async ({ page }) => { const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10); diff --git a/e2e-tests/admin/tests/worker.spec.js b/e2e-tests/admin/tests/worker.spec.js index 45c512411..05601cf62 100644 --- a/e2e-tests/admin/tests/worker.spec.js +++ b/e2e-tests/admin/tests/worker.spec.js @@ -3,11 +3,9 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { test, authenticatedState } from '../../global-setup.js'; +import { test } from '../../global-setup.js'; import { expect } from '@playwright/test'; -test.use({ storageState: authenticatedState }); - test('Create a worker @ce @docker @aws', async ({ page, browserName }) => { test.skip(browserName === 'webkit', 'Bug in worker form on Safari'); diff --git a/e2e-tests/helpers/boundary-cli/connect.js b/e2e-tests/helpers/boundary-cli/connect.js index 4a22bfdcd..063210599 100644 --- a/e2e-tests/helpers/boundary-cli/connect.js +++ b/e2e-tests/helpers/boundary-cli/connect.js @@ -3,7 +3,50 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { exec } from 'node:child_process'; +import { spawn } from 'node:child_process'; + +const remoteCommand = + 'for i in {1..15}; do echo helloworld\\$i; sleep 1s; done'; + +/** + * Spawns a child process for a Boundary connection and returns a promise. + * Resolves on any output from stdout or stderr. + * @param {string} command + * @param {Array} args + */ +async function spawnConnection(command, args) { + return new Promise((resolve, reject) => { + const childProcess = spawn(command, args, { + shell: true, + }); + childProcess.stdout.on('data', (data) => { + if (data.toString().includes('helloworld3')) { + resolve({ childProcess, stdout: data.toString() }); + } + }); + childProcess.stderr.on('data', (data) => { + if (!data.toString().includes('Warning: Permanently added')) { + reject(data.toString()); + } + }); + childProcess.on('error', (err) => { + reject(err); + }); + + // In case the process has no stdio and didn't error out, resolve a + // promise once the child process closes so we're not waiting forever. + // Windows doesn't seem to return any error nor any output from stderr when + // a timeout occurs so this guarantees we return a response to the caller. + // Otherwise this should not get hit as we should be returning a response + // from one of the handlers above. + childProcess.on('close', () => { + resolve({ + childProcess, + stderr: JSON.stringify({ error: 'Process was closed.' }), + }); + }); + }); +} /** * Connects to the specified target @@ -14,23 +57,61 @@ import { exec } from 'node:child_process'; * @returns ChildProcess representing the result of the command execution */ export async function connectToTarget(targetId, sshUser, sshKeyPath) { - let connect; try { - connect = exec( - `boundary connect -target-id=${targetId}` + - ' -exec /usr/bin/ssh --' + - ` -l ${sshUser}` + - ` -i ${sshKeyPath}` + - ' -o UserKnownHostsFile=/dev/null' + - ' -o StrictHostKeyChecking=no' + - ' -o IdentitiesOnly=yes' + // forces the use of the provided key - ' -p {{boundary.port}}' + - ' {{boundary.ip}}', - ); + const { childProcess } = await spawnConnection('boundary', [ + 'connect', + `-target-id=${targetId}`, + '-exec', + '/usr/bin/ssh', + '--', + '-T', // forces tty allocation + `-l ${sshUser}`, + `-i ${sshKeyPath}`, + '-o UserKnownHostsFile=/dev/null', + '-o StrictHostKeyChecking=no', + '-o IdentitiesOnly=yes', // forces the use of the provided key + '-p {{boundary.port}}', + '{{boundary.ip}}', + `"${remoteCommand}"`, + ]); + + return childProcess; } catch (e) { - console.log(`${e.stderr}`); + throw new Error(`Failed to connect to the target: ${e}`); + } +} + +/** + * Connects to the specified alias + * exec is used here to keep the session open + * @param {string} alias alias of the target to be connected to + * @param {string} sshUser User to be used for the ssh connection + * @param {string} sshKeyPath Path to the ssh key to be used for the ssh connection + * @returns ChildProcess representing the result of the command execution + */ +export async function connectToAlias(alias, sshUser, sshKeyPath) { + try { + const { childProcess } = await spawnConnection('boundary', [ + 'connect', + `${alias}`, + '-exec', + '/usr/bin/ssh', + '--', + '-T', // forces tty allocation + `-l ${sshUser}`, + `-i ${sshKeyPath}`, + '-o UserKnownHostsFile=/dev/null', + '-o StrictHostKeyChecking=no', + '-o IdentitiesOnly=yes', // forces the use of the provided key + '-p {{boundary.port}}', + '{{boundary.ip}}', + `"${remoteCommand}"`, + ]); + + return childProcess; + } catch (e) { + throw new Error(`Failed to connect to the target: ${e}`); } - return connect; } /** @@ -40,18 +121,47 @@ export async function connectToTarget(targetId, sshUser, sshKeyPath) { * @returns ChildProcess representing the result of the command execution */ export async function connectSshToTarget(targetId) { - let connect; try { - connect = exec( - 'boundary connect ssh' + - ` -target-id=${targetId}` + - ' --' + - ' -o UserKnownHostsFile=/dev/null' + - ' -o StrictHostKeyChecking=no' + - ' -o IdentitiesOnly=yes', // forces the use of the provided key - ); + const { childProcess } = await spawnConnection('boundary', [ + 'connect', + 'ssh', + `-target-id=${targetId}`, + `-remote-command="${remoteCommand}"`, + '--', + '-T', // forces tty allocation + '-o UserKnownHostsFile=/dev/null', + '-o StrictHostKeyChecking=no', + '-o IdentitiesOnly=yes', // forces the use of the provided key + ]); + + return childProcess; } catch (e) { - console.log(`${e.stderr}`); + throw new Error(`Failed to connect to the target: ${e}`); + } +} + +/** + * Connects via ssh to the specified alias + * exec is used here to keep the session open + * @param {string} alias alias of the target to be connected to + * @returns ChildProcess representing the result of the command execution + */ +export async function connectSshToAlias(alias) { + try { + const { childProcess } = await spawnConnection('boundary', [ + 'connect', + `ssh`, + `${alias}`, + `-remote-command="${remoteCommand}"`, + '--', + '-T', // forces tty allocation + '-o UserKnownHostsFile=/dev/null', + '-o StrictHostKeyChecking=no', + '-o IdentitiesOnly=yes', // forces the use of the provided key + ]); + + return childProcess; + } catch (e) { + throw new Error(`Failed to connect to the target: ${e}`); } - return connect; }