diff --git a/ui/admin/tests/e2e/helpers/boundary-ui.js b/ui/admin/tests/e2e/helpers/boundary-ui.js index 3096cd8d7..0ffad625a 100644 --- a/ui/admin/tests/e2e/helpers/boundary-ui.js +++ b/ui/admin/tests/e2e/helpers/boundary-ui.js @@ -930,7 +930,7 @@ exports.addGrantsToGroup = async (page, grants) => { }; /** - * Uses the UI to create a new Storage Bucket. Assumes you have selected the desired scope. + * Uses the UI to create a new AWS Storage Bucket. * @param {Page} page Playwright page object * @param {string} scope Scope of the Storage Bucket * @param {string} bucketName Name of the Storage Bucket @@ -940,7 +940,7 @@ exports.addGrantsToGroup = async (page, grants) => { * @param {string} workerFilter Worker filter for the Storage Bucket * @returns Name of the Storage Bucket */ -exports.createStorageBucket = async ( +exports.createStorageBucketAws = async ( page, scope, bucketName, @@ -956,6 +956,64 @@ exports.createStorageBucket = async ( await page.getByRole('link', { name: 'New Storage Bucket' }).click(); await page.getByLabel(new RegExp('Name*')).fill(storageBucketName); await page.getByLabel('Scope').selectOption({ label: scope }); + await page + .getByRole('group', { name: 'Provider' }) + .getByLabel('Amazon S3') + .click(); + await page.getByLabel('Bucket name').fill(bucketName); + await page.getByLabel('Region').fill(region); + await page.getByLabel('Access key ID').fill(accessKeyId); + await page.getByLabel('Secret access key').fill(secretAccessKey); + await page.getByLabel('Worker filter').fill(workerFilter); + await page.getByLabel('Disable credential rotation').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(storageBucketName), + ).toBeVisible(); + + return storageBucketName; +}; + +/** + * Uses the UI to create a new MinIO Storage Bucket. + * @param {Page} page Playwright page object + * @param {string} scope Scope of the Storage Bucket + * @param {string} endpointUrl Endpoint URL of the Storage Bucket + * @param {string} bucketName Name of the Storage Bucket + * @param {string} region Region of the Storage Bucket + * @param {string} accessKeyId Access Key ID for the Storage Bucket + * @param {string} secretAccessKey Secret Access Key for the Storage Bucket + * @param {string} workerFilter Worker filter for the Storage Bucket + * @returns Name of the Storage Bucket + */ +exports.createStorageBucketMinio = async ( + page, + scope, + endpointUrl, + bucketName, + region, + accessKeyId, + secretAccessKey, + workerFilter, +) => { + const storageBucketName = 'Bucket ' + nanoid(); + await page + .getByRole('link', { name: 'Storage Buckets', exact: true }) + .click(); + await page.getByRole('link', { name: 'New Storage Bucket' }).click(); + await page.getByLabel(new RegExp('Name*')).fill(storageBucketName); + await page.getByLabel('Scope').selectOption({ label: scope }); + await page + .getByRole('group', { name: 'Provider' }) + .getByLabel('MinIO') + .click(); + await page.getByLabel('Endpoint URL').fill(endpointUrl); await page.getByLabel('Bucket name').fill(bucketName); await page.getByLabel('Region').fill(region); await page.getByLabel('Access key ID').fill(accessKeyId); diff --git a/ui/admin/tests/e2e/tests/session-recording-ent.spec.js b/ui/admin/tests/e2e/tests/session-recording-aws-ent.spec.js similarity index 98% rename from ui/admin/tests/e2e/tests/session-recording-ent.spec.js rename to ui/admin/tests/e2e/tests/session-recording-aws-ent.spec.js index 871ba1d7a..8b0010e6c 100644 --- a/ui/admin/tests/e2e/tests/session-recording-ent.spec.js +++ b/ui/admin/tests/e2e/tests/session-recording-aws-ent.spec.js @@ -19,7 +19,7 @@ const { const { createSshTargetWithAddressAndWorkerFilterEnt, waitForSessionToBeVisible, - createStorageBucket, + createStorageBucketAws, enableSessionRecording, createStoragePolicy, attachStoragePolicy, @@ -48,7 +48,7 @@ test.beforeAll(async () => { await checkBoundaryCli(); }); -test('Verify session recording can be deleted @ent @aws', async ({ page }) => { +test('Session Recording Test (AWS) @ent @aws', async ({ page }) => { await page.goto('/'); let orgId; let storagePolicy; @@ -77,7 +77,7 @@ test('Verify session recording can be deleted @ent @aws', async ({ page }) => { // Create storage bucket await page.getByRole('link', { name: 'Orgs', exact: true }).click(); - const storageBucketName = await createStorageBucket( + const storageBucketName = await createStorageBucketAws( page, orgName, process.env.E2E_AWS_BUCKET_NAME, diff --git a/ui/admin/tests/e2e/tests/session-recording-minio-ent.spec.js b/ui/admin/tests/e2e/tests/session-recording-minio-ent.spec.js new file mode 100644 index 000000000..94eb764ab --- /dev/null +++ b/ui/admin/tests/e2e/tests/session-recording-minio-ent.spec.js @@ -0,0 +1,248 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +/* eslint-disable no-undef */ +const { test, expect } = require('@playwright/test'); +const { execSync } = require('child_process'); +const { checkEnv, authenticatedState } = require('../helpers/general'); +const { + authenticateBoundaryCli, + checkBoundaryCli, + connectSshToTarget, + deleteOrgCli, + waitForSessionRecordingCli, + deleteStorageBucketCli, + deletePolicyCli, +} = require('../helpers/boundary-cli'); +const { + createSshTargetWithAddressAndWorkerFilterEnt, + waitForSessionToBeVisible, + createStorageBucketMinio, + enableSessionRecording, + createStoragePolicy, + attachStoragePolicy, + createOrg, + createProject, + createStaticCredentialStore, + createStaticCredentialKeyPair, + addInjectedCredentialsToTarget, +} = require('../helpers/boundary-ui'); + +test.use({ storageState: authenticatedState }); + +test.beforeAll(async () => { + await checkEnv([ + 'E2E_SSH_USER', + 'E2E_SSH_KEY_PATH', + 'E2E_TARGET_ADDRESS', + 'E2E_TARGET_PORT', + 'E2E_BUCKET_NAME', + 'E2E_BUCKET_ENDPOINT_URL', + 'E2E_BUCKET_ACCESS_KEY_ID', + 'E2E_BUCKET_SECRET_ACCESS_KEY', + 'E2E_REGION', + ]); + + await checkBoundaryCli(); +}); + +test('Session Recording Test (MinIO) @ent @docker', async ({ page }) => { + await page.goto('/'); + + let orgId; + let storagePolicy; + let storageBucket; + let connect; + try { + // Create org + const orgName = await createOrg(page); + await authenticateBoundaryCli( + process.env.BOUNDARY_ADDR, + process.env.E2E_PASSWORD_AUTH_METHOD_ID, + process.env.E2E_PASSWORD_ADMIN_LOGIN_NAME, + process.env.E2E_PASSWORD_ADMIN_PASSWORD, + ); + const orgs = JSON.parse(execSync('boundary scopes list -format json')); + const org = orgs.items.filter((obj) => obj.name == orgName)[0]; + orgId = org.id; + + // Create project + const projectName = await createProject(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]; + const projectId = project.id; + + // Create storage bucket + await page.getByRole('link', { name: 'Orgs', exact: true }).click(); + const storageBucketName = await createStorageBucketMinio( + page, + orgName, + process.env.E2E_BUCKET_ENDPOINT_URL, + process.env.E2E_BUCKET_NAME, + process.env.E2E_REGION, + process.env.E2E_BUCKET_ACCESS_KEY_ID, + process.env.E2E_BUCKET_SECRET_ACCESS_KEY, + `"${process.env.E2E_WORKER_TAG_EGRESS}" in "/tags/type"`, + ); + const storageBuckets = JSON.parse( + execSync('boundary storage-buckets list --recursive -format json'), + ); + storageBucket = storageBuckets.items.filter( + (obj) => obj.name == storageBucketName, + )[0]; + + // Create target + await page.getByRole('link', { name: 'Orgs', exact: true }).click(); + await page.getByRole('link', { name: orgName }).click(); + await page.getByRole('link', { name: projectName }).click(); + const targetName = await createSshTargetWithAddressAndWorkerFilterEnt( + page, + process.env.E2E_TARGET_ADDRESS, + process.env.E2E_TARGET_PORT, + `"${process.env.E2E_WORKER_TAG_EGRESS}" in "/tags/type"`, + ); + const targets = JSON.parse( + execSync('boundary targets list -format json -scope-id ' + projectId), + ); + const target = targets.items.filter((obj) => obj.name == targetName)[0]; + await createStaticCredentialStore(page); + const credentialName = await createStaticCredentialKeyPair( + page, + process.env.E2E_SSH_USER, + process.env.E2E_SSH_KEY_PATH, + ); + await addInjectedCredentialsToTarget(page, targetName, credentialName); + await page.getByRole('link', { name: targetName }).click(); + await enableSessionRecording(page, storageBucketName); + + // Create storage policy in org scope: keep session recordings forever + await page.getByRole('link', { name: 'Orgs', exact: true }).click(); + await page.getByRole('link', { name: orgName }).click(); + const policyName = await createStoragePolicy(page); + await attachStoragePolicy(page, policyName); + + // Establish connection to target and cancel it + connect = await connectSshToTarget(target.id); + await page.getByRole('link', { name: 'Projects', exact: true }).click(); + await page.getByRole('link', { name: projectName }).click(); + await waitForSessionToBeVisible(page, targetName); + await page + .getByRole('cell', { name: targetName }) + .locator('..') + .getByRole('button', { name: 'Cancel' }) + .click(); + await expect( + page.getByRole('alert').getByText('Success', { exact: true }), + ).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss', exact: true }).click(); + await waitForSessionRecordingCli(storageBucket.id); + + // Play back session recording + await page.getByRole('link', { name: 'Orgs', exact: true }).click(); + await page + .getByRole('navigation', { name: 'General' }) + .getByRole('link', { name: 'Session Recordings', exact: true }) + .click(); + await page + .getByRole('row', { name: targetName }) + .getByRole('link', { name: 'View' }) + .click(); + await page + .getByRole('cell', { name: 'Channel 1' }) + .locator('..') + .getByRole('link', { name: 'Play' }) + .click(); + await page.locator('div.session-recording-player').hover(); + await page.locator('.ap-playback-button').click(); + + // Try to delete session recording: expect failure + await page.getByRole('link', { name: 'Orgs', exact: true }).click(); + await page + .getByRole('navigation', { name: 'General' }) + .getByRole('link', { name: 'Session Recordings', exact: true }) + .click(); + await page + .getByRole('row', { name: targetName }) + .getByRole('link', { name: 'View' }) + .click(); + await page.getByText('Manage').click(); + await page.getByRole('button', { name: 'Delete recording' }).click(); + await page.getByRole('button', { name: 'OK', exact: true }).click(); + await expect( + page.getByRole('alert').getByText('Error', { exact: true }), + ).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + + // Edit storage policy: do not protect from deletion + const storagePolicies = JSON.parse( + execSync('boundary policies list -format json -scope-id ' + orgId), + ); + storagePolicy = storagePolicies.items.filter( + (obj) => obj.name == policyName, + )[0]; + await page.getByRole('link', { name: 'Orgs', exact: true }).click(); + await page.getByRole('link', { name: orgName }).click(); + await page + .getByRole('link', { name: 'Storage Policies', exact: true }) + .click(); + await page.getByRole('link', { name: policyName }).click(); + await page.getByRole('button', { name: 'Edit form' }).click(); + await page + .getByLabel('Retention Policy') + .selectOption({ label: 'Do not protect, allow deletion any time' }); + await page.getByLabel('Deletion Policy').selectOption({ label: 'Custom' }); + await page.getByRole('button', { name: 'Save' }).click(); + await expect( + page.getByRole('alert').getByText('Success', { exact: true }), + ).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + + // BUG? After the error trying to delete the session recording, subsequent + // actions result in an error. Reloading the page fixes the issue. This is a + // temporary workaround. + await page.reload(); + + // Re-apply storage policy to the session recording + await page.getByRole('link', { name: 'Orgs', exact: true }).click(); + await page + .getByRole('link', { name: 'Session Recordings', exact: true }) + .click(); + await page + .getByRole('row', { name: targetName }) + .getByRole('link', { name: 'View' }) + .click(); + await page.getByText('Manage').click(); + await page.getByRole('button', { name: 'Re-apply storage policy' }).click(); + await expect( + page.getByRole('alert').getByText('Success', { exact: true }), + ).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + + // Delete session recording + await page.getByText('Manage').click(); + await page.getByRole('button', { name: 'Delete recording' }).click(); + await page.getByRole('button', { name: 'OK' }).click(); + await expect( + page.getByRole('alert').getByText('Success', { exact: true }), + ).toBeVisible(); + await page.getByRole('button', { name: 'Dismiss' }).click(); + } finally { + if (storagePolicy) { + await deletePolicyCli(storagePolicy.id); + } + if (orgId) { + await deleteOrgCli(orgId); + } + if (storageBucket) { + await deleteStorageBucketCli(storageBucket.id); + } + // End `boundary connect` process + if (connect) { + connect.kill('SIGTERM'); + } + } +});