From 064375d4573bac0e160479213b8a65bd225c4072 Mon Sep 17 00:00:00 2001 From: Dharini Jeeva Date: Thu, 23 Jan 2025 14:30:54 -0800 Subject: [PATCH] =?UTF-8?q?test:=20=F0=9F=92=8D=20add=20e2e=20tests=20for?= =?UTF-8?q?=20credentials=20display=20in=20desktop=20(#2660)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 💍 add e2e tests for credentials display in desktop --- e2e-tests/desktop/tests/credentials.spec.js | 311 ++++++++++++++++++ e2e-tests/helpers/boundary-http.js | 1 + .../boundary-http/credential-libraries.js | 32 ++ .../boundary-http/credential-stores.js | 29 ++ .../helpers/boundary-http/credentials.js | 25 ++ 5 files changed, 398 insertions(+) create mode 100644 e2e-tests/desktop/tests/credentials.spec.js create mode 100644 e2e-tests/helpers/boundary-http/credential-libraries.js diff --git a/e2e-tests/desktop/tests/credentials.spec.js b/e2e-tests/desktop/tests/credentials.spec.js new file mode 100644 index 000000000..cbdd2a826 --- /dev/null +++ b/e2e-tests/desktop/tests/credentials.spec.js @@ -0,0 +1,311 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ +import path from 'path'; +import { expect, test } from '../fixtures/baseTest.js'; +import * as boundaryHttp from '../../helpers/boundary-http.js'; +import { spawnSync, execSync } from 'child_process'; +import * as vaultCli from '../../helpers/vault-cli'; + +let org; +let targetWithBrokeredVaultCredentials; +let targetWithBrokeredStaticCredentials; +let sshPrivateKeyCredential; +let jsonCredential; +let vaultGenericCredentialLibrary; + +// config for vault setup +const secretPolicyName = 'kv-policy'; +const boundaryPolicyName = 'boundary-controller'; +const secretsPath = 'e2e_secrets'; +const secretName = 'cred'; +const __dirname = import.meta.dirname; +const boundaryControllerPath = path.resolve( + __dirname, + '../../admin/tests/fixtures/boundary-controller-policy.hcl', +); +const kvPolicyPath = path.resolve( + __dirname, + '../../admin/tests/fixtures/kv-policy.hcl', +); + +test.beforeAll(async () => { + await vaultCli.checkVaultCli(); +}); + +test.beforeEach( + async ({ + request, + targetAddress, + targetPort, + sshUser, + sshKeyPath, + vaultAddr, + }) => { + execSync(`vault policy delete ${secretPolicyName}`); + execSync(`vault policy delete ${boundaryPolicyName}`); + execSync(`vault secrets disable ${secretsPath}`); + + org = await boundaryHttp.createOrg(request); + const project = await boundaryHttp.createProject(request, org.id); + + // Create host set + const hostCatalog = await boundaryHttp.createStaticHostCatalog( + request, + project.id, + ); + const host = await boundaryHttp.createHost(request, { + hostCatalogId: hostCatalog.id, + address: targetAddress, + }); + + const hostSet = await boundaryHttp.createHostSet(request, hostCatalog.id); + await boundaryHttp.addHostToHostSet(request, { + hostSet, + hostIds: [host.id], + }); + + // Create a static credential store + const credentialStore = await boundaryHttp.createStaticCredentialStore( + request, + project.id, + ); + + // Create a key pair credential + sshPrivateKeyCredential = await boundaryHttp.createStaticCredentialKeyPair( + request, + { + credentialStoreId: credentialStore.id, + username: sshUser, + sshKeyPath, + }, + ); + + // Create a json type credential + jsonCredential = await boundaryHttp.createStaticCredentialJson(request, { + credentialStoreId: credentialStore.id, + data: { + nested: { + key1: String.raw`val\bue1`, + key2: String.raw`val\tue2`, + }, + }, + }); + + // Create Vault token and apply policies + + spawnSync('vault', [ + 'policy', + 'write', + boundaryPolicyName, + boundaryControllerPath, + ]); + execSync(`vault secrets enable -path=${secretsPath} kv-v2`); + spawnSync( + 'vault', + [ + 'kv', + 'put', + '-mount', + secretsPath, + secretName, + `password=${String.raw`pass\word`}`, + ], + { + encoding: 'utf-8', + }, + ); + spawnSync('vault', ['policy', 'write', secretPolicyName, kvPolicyPath]); + const vaultToken = JSON.parse( + execSync( + `vault token create` + + ` -no-default-policy=true` + + ` -policy=${boundaryPolicyName}` + + ` -policy=${secretPolicyName}` + + ` -orphan=true` + + ` -period=20m` + + ` -renewable=true` + + ` -format=json`, + ), + ); + const clientToken = vaultToken.auth.client_token; + + // Create a vault credential store and generic secrets type credential library + const vaultCredentialStore = await boundaryHttp.createVaultCredentialStore( + request, + project.id, + vaultAddr, + clientToken, + ); + vaultGenericCredentialLibrary = + await boundaryHttp.createVaultGenericSecretsCredentialLibrary(request, { + credentialStoreId: vaultCredentialStore.id, + vaultPath: `${secretsPath}/data/${secretName}`, + }); + + // Create a tcp target with vault brokered credential Libraries + targetWithBrokeredVaultCredentials = await boundaryHttp.createTarget( + request, + { + scopeId: project.id, + type: 'tcp', + port: targetPort, + }, + ); + targetWithBrokeredVaultCredentials = await boundaryHttp.addHostSource( + request, + { + target: targetWithBrokeredVaultCredentials, + hostSourceIds: [hostSet.id], + }, + ); + targetWithBrokeredVaultCredentials = + await boundaryHttp.addBrokeredCredentials(request, { + target: targetWithBrokeredVaultCredentials, + credentialIds: [vaultGenericCredentialLibrary.id], + }); + + // Create a tcp target with static brokered credentials + targetWithBrokeredStaticCredentials = await boundaryHttp.createTarget( + request, + { + scopeId: project.id, + type: 'tcp', + port: targetPort, + }, + ); + targetWithBrokeredStaticCredentials = await boundaryHttp.addHostSource( + request, + { + target: targetWithBrokeredStaticCredentials, + hostSourceIds: [hostSet.id], + }, + ); + targetWithBrokeredStaticCredentials = + await boundaryHttp.addBrokeredCredentials(request, { + target: targetWithBrokeredStaticCredentials, + credentialIds: [sshPrivateKeyCredential.id, jsonCredential.id], + }); + }, +); + +test.afterEach(async ({ request }) => { + if (org) { + await boundaryHttp.deleteOrg(request, org.id); + } +}); + +test.describe('Credential Panel tests', async () => { + test('Display Vault brokered Credentials and handle special characters', async ({ + authedPage, + }) => { + await authedPage + .getByRole('link', { name: targetWithBrokeredVaultCredentials.name }) + .click(); + await authedPage.getByRole('button', { name: 'Connect' }).click(); + await expect( + authedPage.getByRole('heading', { name: 'Sessions' }), + ).toBeVisible(); + + await expect( + authedPage.getByText(vaultGenericCredentialLibrary.name), + ).toBeVisible(); + await expect(authedPage.getByText('password')).toBeVisible(); + + await authedPage + .getByRole('listitem') + .filter({ hasText: 'password' }) + .getByLabel('Toggle secret visibility') + .click(); + await expect( + authedPage + .getByRole('listitem') + .filter({ hasText: 'password' }) + .locator('pre'), + ).toHaveText(String.raw`pass\word`); + + // End session as active sessions will show a popup when trying to close the DC + await authedPage.getByRole('button', { name: 'End Session' }).click(); + await expect(authedPage.getByText('Canceled successfully.')).toBeVisible(); + await expect( + authedPage.getByRole('heading', { name: 'Targets' }), + ).toBeVisible(); + }); + + test('Display JSON static credential in a key value format and handle special characters', async ({ + authedPage, + }) => { + await authedPage + .getByRole('link', { name: targetWithBrokeredStaticCredentials.name }) + .click(); + await authedPage.getByRole('button', { name: 'Connect' }).click(); + + await expect( + authedPage.getByRole('heading', { name: 'Sessions' }), + ).toBeVisible(); + + // JSON credential with nested keys + await expect(authedPage.getByText(jsonCredential.name)).toBeVisible(); + await expect(authedPage.getByText('nested.key1')).toBeVisible(); + await expect(authedPage.getByText('nested.key2')).toBeVisible(); + await authedPage + .getByRole('listitem') + .filter({ hasText: 'nested.key1' }) + .getByLabel('Toggle secret visibility') + .click(); + await expect( + authedPage + .getByRole('listitem') + .filter({ hasText: 'nested.key1' }) + .locator('pre'), + ).toHaveText(String.raw`val\bue1`); + await authedPage + .getByRole('listitem') + .filter({ hasText: 'nested.key2' }) + .getByLabel('Toggle secret visibility') + .click(); + await expect( + authedPage + .getByRole('listitem') + .filter({ hasText: 'nested.key2' }) + .locator('pre'), + ).toHaveText(String.raw`val\tue2`); + + // SSH private key credential + await expect( + authedPage.getByText(sshPrivateKeyCredential.name), + ).toBeVisible(); + await expect(authedPage.getByText('private_key')).toBeVisible(); + await expect(authedPage.getByText('username')).toBeVisible(); + await authedPage + .getByRole('listitem') + .filter({ hasText: 'username' }) + .getByLabel('Toggle secret visibility') + .click(); + await expect( + authedPage + .getByRole('listitem') + .filter({ hasText: 'username' }) + .locator('pre'), + ).toHaveText(sshPrivateKeyCredential.attributes.username); + await authedPage + .getByRole('listitem') + .filter({ hasText: 'private_key' }) + .getByLabel('Toggle secret visibility') + .click(); + await expect( + authedPage + .getByRole('listitem') + .filter({ hasText: 'private_key' }) + .locator('pre'), + ).toContainText(/BEGIN (OPENSSH|RSA) PRIVATE KEY/); + + // End session as active sessions will show a popup when trying to close the DC + await authedPage.getByRole('button', { name: 'End Session' }).click(); + await expect(authedPage.getByText('Canceled successfully.')).toBeVisible(); + await expect( + authedPage.getByRole('heading', { name: 'Targets' }), + ).toBeVisible(); + }); +}); diff --git a/e2e-tests/helpers/boundary-http.js b/e2e-tests/helpers/boundary-http.js index cd0f974a8..264a6c531 100644 --- a/e2e-tests/helpers/boundary-http.js +++ b/e2e-tests/helpers/boundary-http.js @@ -10,3 +10,4 @@ export * from './boundary-http/host-sets.js'; export * from './boundary-http/hosts.js'; export * from './boundary-http/credential-stores.js'; export * from './boundary-http/credentials.js'; +export * from './boundary-http/credential-libraries.js'; diff --git a/e2e-tests/helpers/boundary-http/credential-libraries.js b/e2e-tests/helpers/boundary-http/credential-libraries.js new file mode 100644 index 000000000..c707f2a83 --- /dev/null +++ b/e2e-tests/helpers/boundary-http/credential-libraries.js @@ -0,0 +1,32 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { nanoid } from 'nanoid'; +import { checkResponse } from './responseHelper.js'; + +/** + * Creates a vault generic credential library + * @param {import('@playwright/test').APIRequestContext} request + * @param {string} credentialStoreId ID of the credential store where the credential will be created + * @param {string} vaultPath Path to secret in vault + * @returns {Promise} + */ +export async function createVaultGenericSecretsCredentialLibrary( + request, + { credentialStoreId, vaultPath }, +) { + const response = await request.post(`/v1/credential-libraries`, { + data: { + name: `vault-credential-store-${nanoid()}`, + credential_store_id: credentialStoreId, + type: 'vault-generic', + attributes: { + path: vaultPath, + }, + }, + }); + + return checkResponse(response); +} diff --git a/e2e-tests/helpers/boundary-http/credential-stores.js b/e2e-tests/helpers/boundary-http/credential-stores.js index 9b58f6dee..6c524e8a3 100644 --- a/e2e-tests/helpers/boundary-http/credential-stores.js +++ b/e2e-tests/helpers/boundary-http/credential-stores.js @@ -23,3 +23,32 @@ export async function createStaticCredentialStore(request, projectId) { return checkResponse(response); } + +/** + * Creates a new vault credential store + * @param {import('@playwright/test').APIRequestContext} request + * @param {string} projectId ID of the project where the credential store will be created + * @param {string} vaultAddr Address of the vault server + * @param {string} vaultToken Token for the vault server + * @returns {Promise} + */ +export async function createVaultCredentialStore( + request, + projectId, + vaultAddr, + vaultToken, +) { + const response = await request.post(`/v1/credential-stores`, { + data: { + name: `vault-credential-store-${nanoid()}`, + scope_id: projectId, + type: 'vault', + attributes: { + address: vaultAddr, + token: vaultToken, + }, + }, + }); + + return checkResponse(response); +} diff --git a/e2e-tests/helpers/boundary-http/credentials.js b/e2e-tests/helpers/boundary-http/credentials.js index 4f407e0da..0c4411a86 100644 --- a/e2e-tests/helpers/boundary-http/credentials.js +++ b/e2e-tests/helpers/boundary-http/credentials.js @@ -37,3 +37,28 @@ export async function createStaticCredentialKeyPair( return checkResponse(response); } + +/** + * Creates a new json credential + * @param {import('@playwright/test').APIRequestContext} request + * @param {string} credentialStoreId ID of the credential store where the credential will be created + * @param {object} data JSON credential data + * @returns {Promise} + */ +export async function createStaticCredentialJson( + request, + { credentialStoreId, data }, +) { + const response = await request.post(`/v1/credentials`, { + data: { + name: `static-credential-store-${nanoid()}`, + credential_store_id: credentialStoreId, + type: 'json', + attributes: { + object: data, + }, + }, + }); + + return checkResponse(response); +}