diff --git a/ui/admin/tests/e2e/helpers/boundary-ui.js b/ui/admin/tests/e2e/helpers/boundary-ui.js index a0aa35253..c3644c138 100644 --- a/ui/admin/tests/e2e/helpers/boundary-ui.js +++ b/ui/admin/tests/e2e/helpers/boundary-ui.js @@ -138,7 +138,7 @@ exports.createNewHostInHostSet = async (page) => { }; /** - * Uses the UI to create a credential store. Assumes you have selected the desired project. + * Uses the UI to create a static credential store. Assumes you have selected the desired project. * @param {Page} page Playwright page object * @returns Name of the credential store */ @@ -166,6 +166,40 @@ exports.createStaticCredentialStore = async (page) => { return credentialStoreName; }; +/** + * Uses the UI to create a vault credential store. Assumes you have selected the desired project. + * @param {Page} page Playwright page object + * @param {string} clientToken vault token to connect to boundary + * @returns Name of the credential store + */ +exports.createVaultCredentialStore = async (page, clientToken) => { + const credentialStoreName = 'Credential Store ' + nanoid(); + await page + .getByRole('navigation', { name: 'Resources' }) + .getByRole('link', { name: 'Credential Stores' }) + .click(); + await page.getByRole('link', { name: 'New', exact: true }).click(); + await page.getByLabel('Name', { exact: true }).fill(credentialStoreName); + await page.getByLabel('Description').fill('This is an automated test'); + await page.getByRole('group', { name: 'Type' }).getByLabel('Vault').click(); + await page + .getByLabel('Address', { exact: true }) + .fill(process.env.E2E_VAULT_ADDR); + await page.getByLabel('Token', { exact: true }).fill(clientToken); + 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(credentialStoreName), + ).toBeVisible(); + + return credentialStoreName; +}; + /** * Uses the UI to create a static key pair credential . Assumes you have selected the desired project. * @param {Page} page Playwright page object @@ -202,6 +236,43 @@ exports.createStaticCredentialKeyPair = async (page) => { return credentialName; }; +/** + * Uses the UI to create a vault credential library. Assumes you have selected + * the desired credential store. + * @param {Page} page Playwright page object + * @param {string} vaultPath path to secret in vault + * @param {string} credentialType type of credential for credential injection + * @returns Name of the credential library + */ +exports.createVaultCredentialLibrary = async ( + page, + vaultPath, + credentialType, +) => { + const credentialLibraryName = 'Credential Library ' + nanoid(); + await page.getByRole('link', { name: 'Credential Libraries' }).click(); + await page.getByRole('link', { name: 'New', exact: true }).click(); + await page + .getByLabel('Name (Optional)', { exact: true }) + .fill(credentialLibraryName); + await page + .getByLabel('Description (Optional)') + .fill('This is an automated test'); + await page.getByLabel('Vault Path').fill(vaultPath); + + await page + .getByRole('combobox', { name: 'Credential Type' }) + .selectOption(credentialType); + + await page.getByRole('button', { name: 'Save' }).click(); + await expect( + page.getByRole('alert').getByText('Success', { exact: true }), + ).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + + return credentialLibraryName; +}; + /** * Uses the UI to create a new target. Assumes you have selected the desired project. * @param {Page} page Playwright page object diff --git a/ui/admin/tests/e2e/tests/credential-store-vault.spec.js b/ui/admin/tests/e2e/tests/credential-store-vault.spec.js index 59209c2c1..4ee0e4b4a 100644 --- a/ui/admin/tests/e2e/tests/credential-store-vault.spec.js +++ b/ui/admin/tests/e2e/tests/credential-store-vault.spec.js @@ -16,11 +16,13 @@ const { checkVaultCli } = require('../helpers/vault-cli'); const { createNewOrg, createNewProject, + createVaultCredentialStore, createNewHostCatalog, createNewHostSet, createNewHostInHostSet, createNewTarget, addHostSourceToTarget, + addBrokeredCredentialsToTarget, } = require('../helpers/boundary-ui'); const { readFile } = require('fs/promises'); @@ -56,141 +58,107 @@ test.beforeEach(async ({ page }) => { test('Vault Credential Store (User & Key Pair) @ce @aws @docker', async ({ page, }) => { - execSync( - `vault policy write ${boundaryPolicyName} ./tests/e2e/tests/fixtures/boundary-controller-policy.hcl`, - ); - execSync(`vault secrets enable -path=${secretsPath} kv-v2`); - execSync( - `vault kv put -mount ${secretsPath} ${secretName} ` + - ` username=${process.env.E2E_SSH_USER}` + - ` private_key=@${process.env.E2E_SSH_KEY_PATH}`, - ); - execSync( - `vault policy write ${secretPolicyName} ./tests/e2e/tests/fixtures/kv-policy.hcl`, - ); - const vaultToken = JSON.parse( + let org; + try { 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; - - const orgName = await createNewOrg(page); - const projectName = await createNewProject(page); - await createNewHostCatalog(page); - const hostSetName = await createNewHostSet(page); - await createNewHostInHostSet(page); - const targetName = await createNewTarget(page); - await addHostSourceToTarget(page, hostSetName); - - const credentialStoreName = 'Credential Store ' + nanoid(); - await page - .getByRole('navigation', { name: 'Resources' }) - .getByRole('link', { name: 'Credential Stores' }) - .click(); - await page.getByRole('link', { name: 'New', exact: true }).click(); - await page.getByLabel('Name', { exact: true }).fill(credentialStoreName); - await page.getByLabel('Description').fill('This is an automated test'); - await page.getByRole('group', { name: 'Type' }).getByLabel('Vault').click(); - await page - .getByLabel('Address', { exact: true }) - .fill(process.env.E2E_VAULT_ADDR); - await page.getByLabel('Token', { exact: true }).fill(clientToken); - 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(credentialStoreName), - ).toBeVisible(); - - const credentialLibraryName = 'Credential Library ' + nanoid(); - await page.getByRole('link', { name: 'Credential Libraries' }).click(); - await page.getByRole('link', { name: 'New', exact: true }).click(); - await page - .getByLabel('Name (Optional)', { exact: true }) - .fill(credentialLibraryName); - await page - .getByLabel('Description (Optional)') - .fill('This is an automated test'); - await page.getByLabel('Vault Path').fill(`${secretsPath}/data/${secretName}`); - 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 page - .getByRole('navigation', { name: 'Resources' }) - .getByRole('link', { name: 'Targets' }) - .click(); - await page.getByRole('link', { name: targetName }).click(); - await page - .getByRole('link', { name: 'Brokered Credentials', exact: true }) - .click(); - await page - .getByRole('article') - .getByRole('link', { name: 'Add Brokered Credentials', exact: true }) - .click(); - await page - .getByRole('cell', { name: credentialLibraryName }) - .locator('..') - .getByRole('checkbox') - .click({ force: true }); - await page - .getByRole('button', { name: 'Add Brokered Credentials', exact: true }) - .click(); - await expect( - page.getByRole('alert').getByText('Success', { exact: true }), - ).toBeVisible(); - await page.getByRole('button', { name: 'Dismiss' }).click(); - await expect( - page.getByRole('link', { name: credentialLibraryName }), - ).toBeVisible(); - - await authenticateBoundaryCli(); - const orgs = JSON.parse(execSync('boundary scopes list -format json')); - const org = orgs.items.filter((obj) => obj.name == orgName)[0]; - const projects = JSON.parse( - execSync(`boundary scopes list -format json -scope-id ${org.id}`), - ); - const project = projects.items.filter((obj) => obj.name == projectName)[0]; - const targets = JSON.parse( - execSync(`boundary targets list -format json -scope-id ${project.id}`), - ); - const target = targets.items.filter((obj) => obj.name == targetName)[0]; - - const session = JSON.parse( - execSync( - `boundary targets authorize-session -id ${target.id} -format json`, - ), - ); - const retrievedUser = - session.item.credentials[0].secret.decoded.data.username; - const retrievedKey = - session.item.credentials[0].secret.decoded.data.private_key; - if (process.env.E2E_SSH_USER != retrievedUser) { - throw new Error( - 'Stored User does not match. EXPECTED: ' + - process.env.E2E_SSH_USER + - ', ACTUAL: ' + - retrievedUser, + `vault policy write ${boundaryPolicyName} ./tests/e2e/tests/fixtures/boundary-controller-policy.hcl`, ); - } - const keyData = await readFile(process.env.E2E_SSH_KEY_PATH, { - encoding: 'utf-8', - }); - if (keyData != retrievedKey) { - throw new Error('Stored Key does not match'); + execSync(`vault secrets enable -path=${secretsPath} kv-v2`); + execSync( + `vault kv put -mount ${secretsPath} ${secretName} ` + + ` username=${process.env.E2E_SSH_USER}` + + ` private_key=@${process.env.E2E_SSH_KEY_PATH}`, + ); + execSync( + `vault policy write ${secretPolicyName} ./tests/e2e/tests/fixtures/kv-policy.hcl`, + ); + 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; + + const orgName = await createNewOrg(page); + await authenticateBoundaryCli(); + const orgs = JSON.parse(execSync('boundary scopes list -format json')); + org = orgs.items.filter((obj) => obj.name == orgName)[0]; + + const projectName = await createNewProject(page); + const projects = JSON.parse( + execSync(`boundary scopes list -format json -scope-id ${org.id}`), + ); + const project = projects.items.filter((obj) => obj.name == projectName)[0]; + + await createNewHostCatalog(page); + const hostSetName = await createNewHostSet(page); + await createNewHostInHostSet(page); + const targetName = await createNewTarget(page); + await addHostSourceToTarget(page, hostSetName); + const targets = JSON.parse( + execSync(`boundary targets list -format json -scope-id ${project.id}`), + ); + const target = targets.items.filter((obj) => obj.name == targetName)[0]; + + await createVaultCredentialStore(page, clientToken); + + const credentialLibraryName = 'Credential Library ' + nanoid(); + await page.getByRole('link', { name: 'Credential Libraries' }).click(); + await page.getByRole('link', { name: 'New', exact: true }).click(); + await page + .getByLabel('Name (Optional)', { exact: true }) + .fill(credentialLibraryName); + await page + .getByLabel('Description (Optional)') + .fill('This is an automated test'); + await page + .getByLabel('Vault Path') + .fill(`${secretsPath}/data/${secretName}`); + 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 addBrokeredCredentialsToTarget( + page, + targetName, + credentialLibraryName, + ); + + const session = JSON.parse( + execSync( + `boundary targets authorize-session -id ${target.id} -format json`, + ), + ); + const retrievedUser = + session.item.credentials[0].secret.decoded.data.username; + const retrievedKey = + session.item.credentials[0].secret.decoded.data.private_key; + if (process.env.E2E_SSH_USER != retrievedUser) { + throw new Error( + 'Stored User does not match. EXPECTED: ' + + process.env.E2E_SSH_USER + + ', ACTUAL: ' + + retrievedUser, + ); + } + const keyData = await readFile(process.env.E2E_SSH_KEY_PATH, { + encoding: 'utf-8', + }); + if (keyData != retrievedKey) { + throw new Error('Stored Key does not match'); + } + } finally { + if (org) { + await deleteOrgCli(org.id); + } } }); diff --git a/ui/admin/tests/e2e/tests/ssh-credential-injection-vault-ent.spec.js b/ui/admin/tests/e2e/tests/ssh-credential-injection-vault-ent.spec.js new file mode 100644 index 000000000..c4d88d19e --- /dev/null +++ b/ui/admin/tests/e2e/tests/ssh-credential-injection-vault-ent.spec.js @@ -0,0 +1,139 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +/* eslint-disable no-undef */ +const { test } = require('@playwright/test'); +const { execSync } = require('child_process'); +const { checkEnv, authenticatedState } = require('../helpers/general'); +const { + authenticateBoundaryCli, + checkBoundaryCli, + connectSshToTarget, + deleteOrgCli, +} = require('../helpers/boundary-cli'); +const { checkVaultCli } = require('../helpers/vault-cli'); +const { + createNewOrg, + createNewProject, + createSshTargetWithAddressEnt, + createVaultCredentialStore, + createVaultCredentialLibrary, + addInjectedCredentialsToTarget, + waitForSessionToBeVisible, +} = require('../helpers/boundary-ui'); + +const secretsPath = 'e2e_secrets'; +const secretName = 'cred'; +const secretPolicyName = 'kv-policy'; +const boundaryPolicyName = 'boundary-controller'; + +test.use({ storageState: authenticatedState }); + +test.beforeAll(async () => { + await checkEnv([ + 'VAULT_ADDR', // Address used by Vault CLI + 'VAULT_TOKEN', + 'E2E_VAULT_ADDR', // Address used by Boundary Server (could be different from VAULT_ADDR depending on network config (i.e. docker network)) + 'E2E_TARGET_ADDRESS', + 'E2E_SSH_USER', + 'E2E_SSH_KEY_PATH', + ]); + + await checkBoundaryCli(); + await checkVaultCli(); +}); + +test.beforeEach(async ({ page }) => { + execSync(`vault policy delete ${secretPolicyName}`); + execSync(`vault policy delete ${boundaryPolicyName}`); + execSync(`vault secrets disable ${secretsPath}`); + + await page.goto('/'); +}); + +test('SSH Credential Injection (Vault User & Key Pair) @ent @docker', async ({ + page, +}) => { + let org; + let connect; + try { + // Set up vault + execSync( + `vault policy write ${boundaryPolicyName} ./tests/e2e/tests/fixtures/boundary-controller-policy.hcl`, + ); + execSync(`vault secrets enable -path=${secretsPath} kv-v2`); + execSync( + `vault kv put -mount ${secretsPath} ${secretName} ` + + ` username=${process.env.E2E_SSH_USER}` + + ` private_key=@${process.env.E2E_SSH_KEY_PATH}`, + ); + execSync( + `vault policy write ${secretPolicyName} ./tests/e2e/tests/fixtures/kv-policy.hcl`, + ); + 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 org + const orgName = await createNewOrg(page); + await authenticateBoundaryCli(); + const orgs = JSON.parse(execSync('boundary scopes list -format json')); + org = orgs.items.filter((obj) => obj.name == orgName)[0]; + + // Create project + const projectName = await createNewProject(page); + const projects = JSON.parse( + execSync('boundary scopes list -format json -scope-id ' + org.id), + ); + const project = projects.items.filter((obj) => obj.name == projectName)[0]; + + // Create target + const targetName = await createSshTargetWithAddressEnt(page); + const targets = JSON.parse( + execSync('boundary targets list -format json -scope-id ' + project.id), + ); + const target = targets.items.filter((obj) => obj.name == targetName)[0]; + + // Create credentials + await createVaultCredentialStore(page, clientToken); + const credentialLibraryName = await createVaultCredentialLibrary( + page, + `${secretsPath}/data/${secretName}`, + 'SSH Private Key', + ); + await addInjectedCredentialsToTarget( + page, + targetName, + credentialLibraryName, + ); + + // Connect to target + connect = await connectSshToTarget(target.id); + await waitForSessionToBeVisible(page, targetName); + await page + .getByRole('cell', { name: targetName }) + .locator('..') + .getByRole('button', { name: 'Cancel' }) + .click(); + } finally { + if (org) { + await deleteOrgCli(org.id); + } + // End `boundary connect` process + if (connect) { + connect.kill('SIGTERM'); + } + } +});