test: 💍 Add storage policies e2e-test (#2175)

* test: 💍 Add storage policies e2e-test

✅ Closes: ICU-12883

* chore: Adjust required env for e2e-tests

✅ Closes: ICU-12883

* CR: Refactor helper method to pass in env

---------

Co-authored-by: Michael Li <michael.li@hashicorp.com>
This commit is contained in:
Stan RyzhovandMichael Li authored and GitHub committed 2024-03-15 11:55:13 -04:00
1 parent 0902e42cb0
commit f100190ac1
5 files changed
+441 -27

No files matched your search

+75 -25
View File
@@ -25,13 +25,13 @@ exports.authenticateBoundaryCli = async () => {
try {
execSync(
'boundary authenticate password' +
' -addr=' +
process.env.BOUNDARY_ADDR +
' -auth-method-id=' +
process.env.E2E_PASSWORD_AUTH_METHOD_ID +
' -login-name=' +
process.env.E2E_PASSWORD_ADMIN_LOGIN_NAME +
' -password=env://E2E_PASSWORD_ADMIN_PASSWORD',
' -addr=' +
process.env.BOUNDARY_ADDR +
' -auth-method-id=' +
process.env.E2E_PASSWORD_AUTH_METHOD_ID +
' -login-name=' +
process.env.E2E_PASSWORD_ADMIN_LOGIN_NAME +
' -password=env://E2E_PASSWORD_ADMIN_PASSWORD',
);
} catch (e) {
console.log(`${e.stderr}`);
@@ -48,18 +48,18 @@ exports.connectToTarget = async (targetId) => {
try {
connect = exec(
'boundary connect' +
' -target-id=' +
targetId +
' -exec /usr/bin/ssh --' +
' -l ' +
process.env.E2E_SSH_USER +
' -i ' +
process.env.E2E_SSH_KEY_PATH +
' -o UserKnownHostsFile=/dev/null' +
' -o StrictHostKeyChecking=no' +
' -o IdentitiesOnly=yes' + // forces the use of the provided key
' -p {{boundary.port}}' +
' {{boundary.ip}}',
' -target-id=' +
targetId +
' -exec /usr/bin/ssh --' +
' -l ' +
process.env.E2E_SSH_USER +
' -i ' +
process.env.E2E_SSH_KEY_PATH +
' -o UserKnownHostsFile=/dev/null' +
' -o StrictHostKeyChecking=no' +
' -o IdentitiesOnly=yes' + // forces the use of the provided key
' -p {{boundary.port}}' +
' {{boundary.ip}}',
);
} catch (e) {
console.log(`${e.stderr}`);
@@ -77,12 +77,12 @@ exports.connectSshToTarget = async (targetId) => {
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
' -target-id=' +
targetId +
' --' +
' -o UserKnownHostsFile=/dev/null' +
' -o StrictHostKeyChecking=no' +
' -o IdentitiesOnly=yes', // forces the use of the provided key
);
} catch (e) {
console.log(`${e.stderr}`);
@@ -124,6 +124,30 @@ exports.deleteOrgCli = async (orgId) => {
}
};
/**
* Uses the boundary CLI to delete the specified storage bucket
* @param {string} storageBucketId ID of the storage bucket to be deleted
*/
exports.deleteStorageBucketCli = async (storageBucketId) => {
try {
exec('boundary storage-buckets delete -id=' + storageBucketId);
} catch (e) {
console.log(`${e.stderr}`);
}
};
/**
* Uses the boundary CLI to delete the specified policy
* @param {string} policyId ID of the policy to be deleted
*/
exports.deletePolicyCli = async (policyId) => {
try {
exec('boundary policies delete -id=' + policyId);
} catch (e) {
console.log(`${e.stderr}`);
}
};
/**
* Uses the boundary CLI to create a new project.
* @param {string} orgId ID of the organization under which the project will be created.
@@ -580,3 +604,29 @@ exports.getSessionCli = async (orgName, projectName, targetName) => {
}
return session;
};
/**
* Uses the boundary CLI to wait for a session recording to be available.
* @param {string} storageBucketId ID of storage bucket that the session recording is associated with
* @returns An object representing a session recording
*/
exports.waitForSessionRecordingCli = async (storageBucketId) => {
let i = 0;
let filteredSessionRecording = [];
do {
i = i + 1;
const sessionRecordings = JSON.parse(
execSync('boundary session-recordings list -format json'),
);
filteredSessionRecording = sessionRecordings.items.filter(
(obj) =>
obj.storage_bucket_id == storageBucketId && obj.state == 'available',
);
if (filteredSessionRecording.length > 0) {
break;
}
await new Promise((resolve) => setTimeout(resolve, 1000));
} while (i < 5);
return filteredSessionRecording[0];
};
+153 -2
View File
@@ -443,6 +443,46 @@ exports.createSshTargetWithAddressEnt = async (page) => {
return targetName;
};
/**
* Uses the UI to create a new SSH target with address in boundary-enterprise
* Assumes you have selected the desired project.
* @param {Page} page Playwright page object
* @param {string} address Address of the target
* @param {string} port Port of the target
* @param {string} workerFilterEgress Egress worker filter
* @returns Name of the target
*/
exports.createSshTargetWithAddressAndWorkerFilterEnt = async (
page,
address,
port,
workerFilterEgress,
) => {
const targetName = 'Target ' + nanoid();
await page
.getByRole('navigation', { name: 'Resources' })
.getByRole('link', { name: 'Targets' })
.click();
await page.getByRole('link', { name: 'New', exact: true }).click();
await page.getByLabel('Name').fill(targetName);
await page.getByLabel('Description').fill('This is an automated test');
await page.getByRole('group', { name: 'Type' }).getByLabel('SSH').click();
await page.getByLabel('Target Address').fill(address);
await page.getByLabel('Default Port').fill(port);
await page.getByLabel('Egress worker filter').click();
await page.getByRole('textbox', { name: 'Filter' }).fill(workerFilterEgress);
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(targetName),
).toBeVisible();
return targetName;
};
/**
* Uses the UI to delete a Boundary resource. Assume you have selected the desired resource.
* Note: For a resource to be deleted using this method,
@@ -498,7 +538,8 @@ exports.addHostSourceToTarget = async (page, hostSourceName) => {
};
/**
* Uses the UI to navigate to Sessions and waits for the session to appear.
* Uses the UI to navigate to Sessions and waits for the session to appear
* and be in Active state.
* @param {Page} page Playwright page object
* @param {string} targetName Name of the target associated with the session
*/
@@ -509,12 +550,16 @@ exports.waitForSessionToBeVisible = async (page, targetName) => {
.click();
let i = 0;
let sessionIsVisible = false;
let sessionIsActive = false;
do {
i = i + 1;
sessionIsVisible = await page
.getByRole('cell', { name: targetName })
.isVisible();
if (sessionIsVisible) {
sessionIsActive = await page
.getByRole('cell', { name: 'Active' })
.isVisible();
if (sessionIsVisible && sessionIsActive) {
break;
}
await page.getByRole('button', { name: 'Refresh' }).click();
@@ -858,3 +903,109 @@ exports.addGrantsToGroup = async (page, grants) => {
page.getByRole('textbox', { name: 'Grant', exact: true }),
).toHaveValue(grants);
};
/**
* Uses the UI to create a new Storage Bucket. Assumes you have selected the desired scope.
* @param {Page} page Playwright page object
* @returns Name of the Storage Bucket
*/
exports.createStorageBucket = async (page) => {
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('Bucket name').fill(process.env.E2E_AWS_BUCKET_NAME);
await page.getByLabel('Region').fill(process.env.E2E_AWS_REGION);
await page
.getByLabel('Access key ID')
.fill(process.env.E2E_AWS_ACCESS_KEY_ID);
await page
.getByLabel('Secret access key')
.fill(process.env.E2E_AWS_SECRET_ACCESS_KEY);
const workerTag = `"${process.env.E2E_WORKER_TAG_EGRESS}" in "/tags/type"`;
await page.getByLabel('Worker filter').fill(workerTag);
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 enable session recording for a target. Assumes you have selected the desired target.
* @param {Page} page Playwright page object
* @param {string} storageBucketName name of the Storage Bucket used for session recording
*/
exports.enableSessionRecording = async (page, storageBucketName) => {
await page.getByText('Enable recording').click();
await page.getByLabel('Record sessions for this target').click();
await page
.getByLabel('AWS storage buckets')
.selectOption({ label: storageBucketName });
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('listitem').getByText(storageBucketName),
).toBeVisible();
};
/**
* Uses the UI to create a Storage Policy. Assumes you have selected the desired scope.
* @param {Page} page Playwright page object
* @returns Name of the Storage Policy
*/
exports.createStoragePolicy = async (page) => {
const storagePolicyName = 'Policy ' + nanoid();
await page
.getByRole('link', { name: 'Storage Policies', exact: true })
.click();
await page
.getByRole('link', { name: 'Create a new storage policy', exact: true })
.click();
await page.getByLabel('Name').fill(storagePolicyName);
await page.getByLabel('Retention Policy').selectOption({ label: 'Forever' });
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(storagePolicyName),
).toBeVisible();
return storagePolicyName;
};
/**
* Uses the UI to attach Storage Policy to a scope. Assumes you have selected the desired scope.
* @param {Page} page Playwright page object
* @param {string} policyName name of the Policy to be attached to the scope
*/
exports.attachStoragePolicy = async (page, policyName) => {
await page
.getByRole('link', { name: new RegExp('/*Settings'), exact: true })
.click();
await page.getByText('Add Storage Policy').click();
await page.getByLabel('Storage Policy').selectOption({ label: policyName });
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('listitem').getByText(policyName)).toBeVisible();
};
@@ -0,0 +1,211 @@
/**
* 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,
createStorageBucket,
enableSessionRecording,
createStoragePolicy,
attachStoragePolicy,
createNewOrg,
createNewProject,
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_AWS_BUCKET_NAME',
'E2E_AWS_REGION',
'E2E_AWS_ACCESS_KEY_ID',
'E2E_AWS_SECRET_ACCESS_KEY',
'E2E_WORKER_TAG_EGRESS',
]);
await checkBoundaryCli();
});
test('Verify session recording can be deleted @ent @aws', async ({ page }) => {
await page.goto('/');
let orgId;
let storagePolicy;
let storageBucket;
let connect;
try {
await authenticateBoundaryCli();
// Create org
const orgName = await createNewOrg(page);
await authenticateBoundaryCli();
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 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];
const projectId = project.id;
// Create Storage Bucket
await page.getByRole('link', { name: 'Orgs', exact: true }).click();
const storageBucketName = await createStorageBucket(page);
const storageBuckets = JSON.parse(
execSync('boundary storage-buckets list -format json'),
);
storageBucket = storageBuckets.items.filter(
(obj) => obj.name == storageBucketName,
)[0];
// Create SSH Target
await page.getByRole('link', { name: 'Orgs', exact: true }).click();
await page.getByRole('link', { name: orgId }).click();
await page.getByRole('link', { name: projectId }).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);
await addInjectedCredentialsToTarget(page, targetName, credentialName);
// Enable session recording for Target
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: orgId }).click();
const policyName = await createStoragePolicy(page);
// Attach Storage Policy to org scope
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' }).click();
// Try to delete Session Recording: expect failure
await waitForSessionRecordingCli(storageBucket.id);
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: 'Delete recording' }).click();
await page.getByRole('button', { name: 'OK' }).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: orgId }).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();
// Re-apply Storage Policy to the Session Recording
await page.reload(); // fixes error appearing on re-applying storage policy
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');
}
}
});
@@ -39,6 +39,7 @@ test.beforeAll(async () => {
'E2E_TARGET_ADDRESS',
'E2E_SSH_USER',
'E2E_SSH_KEY_PATH',
'E2E_WORKER_TAG_EGRESS',
]);
await checkBoundaryCli();
@@ -33,6 +33,7 @@ test.beforeAll(async () => {
'E2E_SSH_KEY_PATH',
'E2E_TARGET_ADDRESS',
'E2E_TARGET_PORT',
'E2E_WORKER_TAG_EGRESS',
]);
await checkBoundaryCli();