From 9d9a38510b68027957efa26952d7345c82e4030a Mon Sep 17 00:00:00 2001 From: Michael Li Date: Thu, 7 Mar 2024 17:05:45 -0500 Subject: [PATCH] test(e2e): Add test for SSH certification injection (#2170) --- ui/admin/tests/e2e/helpers/boundary-ui.js | 61 ++++++- .../ssh-certificate-injection-role.json | 10 ++ .../tests/e2e/tests/fixtures/ssh-policy.hcl | 10 ++ ...sh-certificate-injection-vault-ent.spec.js | 151 ++++++++++++++++++ ...ssh-credential-injection-vault-ent.spec.js | 4 +- 5 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 ui/admin/tests/e2e/tests/fixtures/ssh-certificate-injection-role.json create mode 100644 ui/admin/tests/e2e/tests/fixtures/ssh-policy.hcl create mode 100644 ui/admin/tests/e2e/tests/ssh-certificate-injection-vault-ent.spec.js diff --git a/ui/admin/tests/e2e/helpers/boundary-ui.js b/ui/admin/tests/e2e/helpers/boundary-ui.js index c3644c138..76f0d7bd2 100644 --- a/ui/admin/tests/e2e/helpers/boundary-ui.js +++ b/ui/admin/tests/e2e/helpers/boundary-ui.js @@ -237,14 +237,14 @@ exports.createStaticCredentialKeyPair = async (page) => { }; /** - * Uses the UI to create a vault credential library. Assumes you have selected + * Uses the UI to create a vault-generic 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 ( +exports.createVaultGenericCredentialLibrary = async ( page, vaultPath, credentialType, @@ -258,8 +258,11 @@ exports.createVaultCredentialLibrary = async ( await page .getByLabel('Description (Optional)') .fill('This is an automated test'); + await page + .getByRole('group', { name: 'Type' }) + .getByLabel('Generic Secrets') + .click(); await page.getByLabel('Vault Path').fill(vaultPath); - await page .getByRole('combobox', { name: 'Credential Type' }) .selectOption(credentialType); @@ -273,6 +276,58 @@ exports.createVaultCredentialLibrary = async ( return credentialLibraryName; }; +/** + * Uses the UI to create a vault-ssh-certificate credential library. Assumes you have selected + * the desired credential store. + * @param {Page} page Playwright page object + * @param {string} vaultPath path to secret in vault + * @returns Name of the credential library + */ +exports.createVaultSshCertificateCredentialLibrary = async ( + page, + vaultPath, +) => { + const credentialLibraryName = 'Credential Library ' + nanoid(); + + await page.getByRole('link', { name: 'Credential Libraries' }).click(); + await page.getByRole('link', { name: 'New', exact: true }).click(); + + // Temporarily putting the Group selection first due to a bug where + // Name and Description fields get cleared when the Group is selected + await page + .getByRole('group', { name: 'Type' }) + .getByLabel('SSH Certificates') + .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.getByLabel('Username').fill(process.env.E2E_SSH_USER); + await page.getByLabel('Key Type').selectOption('ecdsa'); + await page.getByLabel('Key Bits').fill('521'); + + await page + .getByRole('group', { name: 'Extensions' }) + .getByLabel('Key') + .fill('permity-pty'); + await page + .getByRole('group', { name: 'Extensions' }) + .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(); + + 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/fixtures/ssh-certificate-injection-role.json b/ui/admin/tests/e2e/tests/fixtures/ssh-certificate-injection-role.json new file mode 100644 index 000000000..3cdc50b02 --- /dev/null +++ b/ui/admin/tests/e2e/tests/fixtures/ssh-certificate-injection-role.json @@ -0,0 +1,10 @@ +{ + "key_type": "ca", + "allow_user_certificates": true, + "default_user": "admin", + "default_extensions": { + "permit-pty": "" + }, + "allowed_users": "*", + "allowed_extensions": "*" +} diff --git a/ui/admin/tests/e2e/tests/fixtures/ssh-policy.hcl b/ui/admin/tests/e2e/tests/fixtures/ssh-policy.hcl new file mode 100644 index 000000000..8675ff877 --- /dev/null +++ b/ui/admin/tests/e2e/tests/fixtures/ssh-policy.hcl @@ -0,0 +1,10 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +path "e2e_secrets/issue/boundary-client" { + capabilities = ["create", "update"] +} + +path "e2e_secrets/sign/boundary-client" { + capabilities = ["create", "update"] +} diff --git a/ui/admin/tests/e2e/tests/ssh-certificate-injection-vault-ent.spec.js b/ui/admin/tests/e2e/tests/ssh-certificate-injection-vault-ent.spec.js new file mode 100644 index 000000000..f40f42f1a --- /dev/null +++ b/ui/admin/tests/e2e/tests/ssh-certificate-injection-vault-ent.spec.js @@ -0,0 +1,151 @@ +/** + * 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, + createVaultSshCertificateCredentialLibrary, + addInjectedCredentialsToTarget, + waitForSessionToBeVisible, +} = require('../helpers/boundary-ui'); + +const sshPolicyName = 'ssh-policy'; +const boundaryPolicyName = 'boundary-controller'; +// This must match the secret path in ssh-policy.hcl +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 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_CA_KEY', + 'E2E_SSH_CA_KEY_PUBLIC', + ]); + + await checkBoundaryCli(); + await checkVaultCli(); +}); + +test.beforeEach(async () => { + execSync(`vault secrets disable ${secretsPath}`); +}); + +test('SSH Certificate Injection @ent @docker', async ({ page }) => { + await page.goto('/'); + 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} ssh`); + execSync( + `vault policy write ${sshPolicyName} ./tests/e2e/tests/fixtures/ssh-policy.hcl`, + ); + execSync( + `vault write ${secretsPath}/roles/${secretName} @./tests/e2e/tests/fixtures/ssh-certificate-injection-role.json`, + ); + + const private_key = atob(process.env.E2E_SSH_CA_KEY); + const public_key = atob(process.env.E2E_SSH_CA_KEY_PUBLIC); + + execSync( + `vault write ${secretsPath}/config/ca` + + ` private_key="${private_key}"` + + ` public_key="${public_key}"` + + ` generate_signing_key=false`, + ` format=json`, + ); + + const vaultToken = JSON.parse( + execSync( + `vault token create` + + ` -no-default-policy=true` + + ` -policy=${boundaryPolicyName}` + + ` -policy=${sshPolicyName}` + + ` -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 createVaultSshCertificateCredentialLibrary( + page, + `${secretsPath}/issue/${secretName}`, + ); + 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'); + } + + execSync(`vault secrets disable ${secretsPath}`); + execSync(`vault policy delete ${sshPolicyName}`); + execSync(`vault policy delete ${boundaryPolicyName}`); + } +}); 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 index c4d88d19e..2599b83af 100644 --- 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 @@ -19,7 +19,7 @@ const { createNewProject, createSshTargetWithAddressEnt, createVaultCredentialStore, - createVaultCredentialLibrary, + createVaultGenericCredentialLibrary, addInjectedCredentialsToTarget, waitForSessionToBeVisible, } = require('../helpers/boundary-ui'); @@ -108,7 +108,7 @@ test('SSH Credential Injection (Vault User & Key Pair) @ent @docker', async ({ // Create credentials await createVaultCredentialStore(page, clientToken); - const credentialLibraryName = await createVaultCredentialLibrary( + const credentialLibraryName = await createVaultGenericCredentialLibrary( page, `${secretsPath}/data/${secretName}`, 'SSH Private Key',