diff --git a/addons/core/translations/actions/en-us.yaml b/addons/core/translations/actions/en-us.yaml index 125ea3f51..36cb5dffe 100644 --- a/addons/core/translations/actions/en-us.yaml +++ b/addons/core/translations/actions/en-us.yaml @@ -45,3 +45,5 @@ learn-more: Learn more refresh: Refresh remove-resources: Remove resources and save do-this-later: Do this later +toggle-color-theme: Toggle Color Theme +toggle-feature-edition: Toggle Feature Edition diff --git a/addons/core/translations/en-us.yaml b/addons/core/translations/en-us.yaml index 39b20edcd..277e0dd3b 100644 --- a/addons/core/translations/en-us.yaml +++ b/addons/core/translations/en-us.yaml @@ -28,7 +28,6 @@ titles: actions: Actions documentation: Documentation abandon-confirm: Discard unsaved changes? - toggle-color-theme: Toggle Color Theme welcome-to-boundary: Welcome to Boundary label: Label value: Value @@ -38,6 +37,12 @@ titles: success: Success create-resources: Create Resources choose-an-option: Choose an option + feature-edition: 'Edition: {edition}' +editions: + oss: OSS + ent: Enterprise + enterprise: Enterprise + hcp: HCP descriptions: empty-set: There are no items to display yet. You may be able to add items or try back later. cluster-url-initialization: To get started, please enter your cluster URL diff --git a/ui/admin/app/abilities/credential-library.js b/ui/admin/app/abilities/credential-library.js index 923bcdd86..f2859dd18 100644 --- a/ui/admin/app/abilities/credential-library.js +++ b/ui/admin/app/abilities/credential-library.js @@ -12,13 +12,12 @@ export default class OverrideCredentialLibraryAbility extends CredentialLibraryA /** * This override ensures that vault ssh cert may be read only if the - * credential-library-vault-ssh-certificate feature flag is enabled. + * ssh-target feature flag is enabled. * All other types are subject to the standard logic found in the api addon. */ get canRead() { - return this.features.isEnabled( - 'credential-library-vault-ssh-certificate' - ) || !this.model.isVaultSSHCertificate + return this.features.isEnabled('ssh-target') || + !this.model.isVaultSSHCertificate ? super.canRead : false; } diff --git a/ui/admin/app/components/form/credential-library/vault-generic/index.hbs b/ui/admin/app/components/form/credential-library/vault-generic/index.hbs index 418fcee49..3bdd8aee4 100644 --- a/ui/admin/app/components/form/credential-library/vault-generic/index.hbs +++ b/ui/admin/app/components/form/credential-library/vault-generic/index.hbs @@ -50,9 +50,7 @@ {{/if}} - {{#if - (and @model.isNew (feature-flag 'credential-library-vault-ssh-certificate')) - }} + {{#if (and @model.isNew (feature-flag 'ssh-target'))}} {{t 'form.type.label'}} {{#each this.types as |type|}} diff --git a/ui/admin/app/components/form/target/worker-filter/index.hbs b/ui/admin/app/components/form/target/worker-filter/index.hbs index dfc05bc95..71c53e4dc 100644 --- a/ui/admin/app/components/form/target/worker-filter/index.hbs +++ b/ui/admin/app/components/form/target/worker-filter/index.hbs @@ -6,7 +6,7 @@
{ + if (editionFlags[flag]) { + this.features.enable(flag); + } else { + this.features.disable(flag); + } + }); + this.edition = edition; + } +} diff --git a/ui/admin/app/templates/application.hbs b/ui/admin/app/templates/application.hbs index 3db4bb8d3..4c4b34805 100644 --- a/ui/admin/app/templates/application.hbs +++ b/ui/admin/app/templates/application.hbs @@ -33,7 +33,7 @@ - + + + {{#if (feature-flag 'dev-edition-toggle')}} + + + + + {{#each this.featureEdition.editions as |edition|}} + + + + {{/each}} + + + + + {{/if}} + {{/if}} diff --git a/ui/admin/config/environment.js b/ui/admin/config/environment.js index 2f5f63250..72da19afc 100644 --- a/ui/admin/config/environment.js +++ b/ui/admin/config/environment.js @@ -9,28 +9,36 @@ const APP_NAME = process.env.APP_NAME || 'Boundary'; const API_HOST = process.env.API_HOST || ''; const EDITION = process.env.EDITION || 'oss'; // Default edition is OSS -// Object that defines edition features. -const featureEditions = { - oss: { - 'static-credentials': true, - 'json-credentials': true, - byow: true, - 'byow-pki-hcp-cluster-id': false, - 'byow-pki-upstream': true, - 'vault-worker-filter': false, - 'target-worker-filters-v2': true, - 'target-worker-filters-v2-ingress': false, - 'target-worker-filters-v2-hcp': false, - 'target-network-address': true, - 'credential-library-vault-ssh-certificate': false, - }, +// Base edition declares available features, disabled by default. +const baseEdition = { + byow: false, + 'byow-pki-hcp-cluster-id': false, + 'byow-pki-upstream': false, + 'json-credentials': false, + 'ssh-target': false, + 'static-credentials': false, + 'target-worker-filters-v2': false, + 'target-worker-filters-v2-ingress': false, + 'target-worker-filters-v2-hcp': false, + 'target-network-address': false, + 'vault-worker-filter': false, +}; +// Editions maps edition keys to their associated featuresets. +const featureEditions = {}; +featureEditions.oss = { + ...baseEdition, + byow: true, + 'byow-pki-upstream': true, + 'json-credentials': true, + 'static-credentials': true, + 'target-worker-filters-v2': true, + 'target-network-address': true, }; featureEditions.enterprise = { ...featureEditions.oss, 'ssh-target': true, - 'vault-worker-filter': true, 'target-worker-filters-v2-ingress': true, - 'credential-library-vault-ssh-certificate': true, + 'vault-worker-filter': true, }; featureEditions.hcp = { ...featureEditions.enterprise, @@ -127,7 +135,20 @@ module.exports = function (environment) { timeout: 4000, }, - featureFlags: featureEditions[EDITION], + selectedEdition: EDITION, + featureEditions, + featureFlags: {}, + }; + + /** + * Takes a set of features and enables them in all editions. + * For use in development and testing only. + */ + const enableFeaturesInAllEditions = (features = {}) => { + Object.entries(featureEditions).forEach( + ([key, edition]) => + (ENV.featureEditions[key] = { ...features, ...edition }) + ); }; if (environment === 'development') { @@ -151,16 +172,12 @@ module.exports = function (environment) { }, }; - // Enable features in development - ENV.featureFlags['static-credentials'] = true; - ENV.featureFlags['json-credentials'] = true; - ENV.featureFlags['ssh-target'] = true; - ENV.featureFlags['vault-worker-filter'] = true; - ENV.featureFlags['target-worker-filters-v2'] = true; - ENV.featureFlags['target-worker-filters-v2-ingress'] = true; - ENV.featureFlags['target-worker-filters-v2-hcp'] = true; - ENV.featureFlags['target-network-address'] = true; - ENV.featureFlags['credential-library-vault-ssh-certificate'] = true; + // Default edition in development + ENV.selectedEdition = 'enterprise'; + enableFeaturesInAllEditions({ + // Show edition toggle in UI in development + 'dev-edition-toggle': true, + }); } if (environment === 'test') { @@ -179,14 +196,15 @@ module.exports = function (environment) { ENV.enableConfirmService = false; - // Enable tests for development features - ENV.featureFlags['json-credentials'] = true; - ENV.featureFlags['static-credentials'] = true; - ENV.featureFlags['ssh-target'] = true; - ENV.featureFlags['vault-worker-filter'] = true; - ENV.featureFlags['target-worker-filters-v2'] = true; - ENV.featureFlags['target-network-address'] = true; - ENV.featureFlags['credential-library-vault-ssh-certificate'] = true; + /** + * Enable additional features for testing purposes. + * [TODO] Tests should be refactored such that each test case + * explicitly enables the features and/or edition that it needs. + * By default, tests should have no features enabled. This explicit + * approach ensures that test cases are easy to read and understand, + * since their feature requirements are embedded. + */ + ENV.selectedEdition = 'enterprise'; } if (environment === 'production') { diff --git a/ui/admin/tests/acceptance/credential-library/create-test.js b/ui/admin/tests/acceptance/credential-library/create-test.js index c979fd498..7f0343574 100644 --- a/ui/admin/tests/acceptance/credential-library/create-test.js +++ b/ui/admin/tests/acceptance/credential-library/create-test.js @@ -225,7 +225,7 @@ module('Acceptance | credential-libraries | create', function (hooks) { test('cannot select vault ssh cert when feature is disabled', async function (assert) { const featuresService = this.owner.lookup('service:features'); - featuresService.disable('credential-library-vault-ssh-certificate'); + featuresService.disable('ssh-target'); assert.expect(1); await visit(urls.newCredentialLibrary); diff --git a/ui/admin/tests/acceptance/credential-library/read-test.js b/ui/admin/tests/acceptance/credential-library/read-test.js index 1684c24c1..38cf3f406 100644 --- a/ui/admin/tests/acceptance/credential-library/read-test.js +++ b/ui/admin/tests/acceptance/credential-library/read-test.js @@ -95,7 +95,7 @@ module('Acceptance | credential-libraries | read', function (hooks) { `${urls.credentialLibraries}/${instances.credentialLibrary.id}` ); const featuresService = this.owner.lookup('service:features'); - featuresService.disable('credential-library-vault-ssh-certificate'); + featuresService.disable('ssh-target'); await visit(urls.credentialLibraries); assert.dom('.rose-table-body tr:nth-of-type(2) a').doesNotExist(); diff --git a/ui/admin/tests/acceptance/develop-test.js b/ui/admin/tests/acceptance/develop-test.js new file mode 100644 index 000000000..9f6e8ae4a --- /dev/null +++ b/ui/admin/tests/acceptance/develop-test.js @@ -0,0 +1,29 @@ +import { module, test } from 'qunit'; +import { visit, getContext } from '@ember/test-helpers'; +import { setupApplicationTest } from 'admin/tests/helpers'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; + +module('Acceptance | develop', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + let featuresService; + + hooks.beforeEach(function () { + const { owner } = getContext(); + featuresService = owner.lookup('service:features'); + }); + + test('edition toggle is hidden when `dev-edition-toggle` is false', async function (assert) { + await visit('/'); + assert.false(featuresService.isEnabled('dev-edition-toggle')); + assert.dom('data-dev-edition-toggle').doesNotExist(); + }); + + test('edition toggle is visible when `dev-edition-toggle` is true', async function (assert) { + assert.false(featuresService.isEnabled('dev-edition-toggle')); + featuresService.enable('dev-edition-toggle'); + await visit('/'); + assert.dom('data-dev-edition-toggle').doesNotExist(); + }); +}); diff --git a/ui/admin/tests/acceptance/targets/create-test.js b/ui/admin/tests/acceptance/targets/create-test.js index 805d952ff..d5ff06eab 100644 --- a/ui/admin/tests/acceptance/targets/create-test.js +++ b/ui/admin/tests/acceptance/targets/create-test.js @@ -125,7 +125,10 @@ module('Acceptance | targets | create', function (hooks) { await click(`[href="${urls.newTarget}"]`); await click('[value="ssh"]'); await fillIn('[name="name"]', 'random string'); - await click('.hds-form-toggle__control', 'Egress worker filter'); + await click( + '[name="target-worker-filter-toggle-egress_worker_filter"]', + 'Egress toggle' + ); await fillIn('[name="egress_worker_filter"]', 'random filter'); await click('[type="submit"]'); @@ -178,7 +181,10 @@ module('Acceptance | targets | create', function (hooks) { await click(`[href="${urls.newTarget}"]`); await click('[value="tcp"]'); await fillIn('[name="name"]', 'random string'); - await click('.hds-form-toggle__control', 'Egress worker filter'); + await click( + '[name="target-worker-filter-toggle-egress_worker_filter"]', + 'Egress toggle' + ); await fillIn('[name="egress_worker_filter"]', 'random filter'); await click('[type="submit"]'); @@ -301,7 +307,10 @@ module('Acceptance | targets | create', function (hooks) { await click(`[href="${urls.newTarget}"]`); await fillIn('[name="name"]', 'random string'); - await click('.hds-form-toggle__control', 'Egress worker filter'); + await click( + '[name="target-worker-filter-toggle-egress_worker_filter"]', + 'Egress toggle' + ); await fillIn('[name="egress_worker_filter"]', 'random filter'); await click('.rose-form-actions [type="button"]'); @@ -318,7 +327,10 @@ module('Acceptance | targets | create', function (hooks) { await click(`[href="${urls.newTarget}"]`); await fillIn('[name="name"]', 'random string'); await click('[value="ssh"]'); - await click('.hds-form-toggle__control', 'Egress worker filter'); + await click( + '[name="target-worker-filter-toggle-egress_worker_filter"]', + 'Egress toggle' + ); await fillIn('[name="egress_worker_filter"]', 'random filter'); await click('.rose-form-actions [type="button"]'); diff --git a/ui/admin/tests/acceptance/targets/update-test.js b/ui/admin/tests/acceptance/targets/update-test.js index bbb7e64b7..ac7455b66 100644 --- a/ui/admin/tests/acceptance/targets/update-test.js +++ b/ui/admin/tests/acceptance/targets/update-test.js @@ -164,16 +164,21 @@ module('Acceptance | targets | update', function (hooks) { test('hide filter input field when the `egress_worker_filter` toggled is off', async function (assert) { featuresService.enable('target-worker-filters-v2'); + featuresService.enable('target-worker-filters-v2-ingress'); assert.expect(1); await visit(urls.target); await click('.rose-form-actions [type="button"]', 'Activate edit mode'); await click('.hds-button[type="button"]', 'Update Filter'); - await click('.hds-form-toggle__control', 'Egress worker filter'); + await click( + '[name="target-worker-filter-toggle-egress_worker_filter"]', + 'Egress toggle' + ); assert.dom('[name=egress_worker_filter]').isNotVisible(); }); test('clear `egress_worker_field` value from a target when the toggle is off and form is saved', async function (assert) { featuresService.enable('target-worker-filters-v2'); + featuresService.enable('target-worker-filters-v2-ingress'); assert.expect(5); await visit(urls.target); await click('.rose-form-actions [type="button"]', 'Activate edit mode'); @@ -186,7 +191,10 @@ module('Acceptance | targets | update', function (hooks) { 'random filter string' ); await click('.rose-form-actions [type="button"]', 'Activate edit mode'); - await click('.hds-form-toggle__control', 'Egress worker filter'); + await click( + '[name="target-worker-filter-toggle-egress_worker_filter"]', + 'Egress toggle' + ); await click('.rose-form-actions [type="submit"]'); //clear egress_worker_filter when the toggle is off assert.strictEqual(currentURL(), urls.target); diff --git a/ui/admin/tests/e2e/tests/credential-store-static.spec.js b/ui/admin/tests/e2e/tests/credential-store-static.spec.js index ef79ce1ad..a7d3bf72a 100644 --- a/ui/admin/tests/e2e/tests/credential-store-static.spec.js +++ b/ui/admin/tests/e2e/tests/credential-store-static.spec.js @@ -137,9 +137,9 @@ test('Static Credential Store (User & Key Pair)', async ({ page }) => { if (process.env.E2E_SSH_USER != retrievedUser) { throw new Error( 'Stored User does not match. EXPECTED: ' + - process.env.E2E_SSH_USER + - ', ACTUAL: ' + - retrievedUser + process.env.E2E_SSH_USER + + ', ACTUAL: ' + + retrievedUser ); } if (keyData != retrievedKey) { 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 cf4860cab..83f6ee62e 100644 --- a/ui/admin/tests/e2e/tests/credential-store-vault.spec.js +++ b/ui/admin/tests/e2e/tests/credential-store-vault.spec.js @@ -59,8 +59,8 @@ test('Vault Credential Store (User & Key Pair)', async ({ page }) => { 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}` + ` 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` @@ -68,13 +68,13 @@ test('Vault Credential Store (User & Key Pair)', async ({ page }) => { const vaultToken = JSON.parse( execSync( `vault token create` + - ` -no-default-policy=true` + - ` -policy=${boundaryPolicyName}` + - ` -policy=${secretPolicyName}` + - ` -orphan=true` + - ` -period=20m` + - ` -renewable=true` + - ` -format=json` + ` -no-default-policy=true` + + ` -policy=${boundaryPolicyName}` + + ` -policy=${secretPolicyName}` + + ` -orphan=true` + + ` -period=20m` + + ` -renewable=true` + + ` -format=json` ) ); const clientToken = vaultToken.auth.client_token; @@ -175,9 +175,9 @@ test('Vault Credential Store (User & Key Pair)', async ({ page }) => { if (process.env.E2E_SSH_USER != retrievedUser) { throw new Error( 'Stored User does not match. EXPECTED: ' + - process.env.E2E_SSH_USER + - ', ACTUAL: ' + - retrievedUser + process.env.E2E_SSH_USER + + ', ACTUAL: ' + + retrievedUser ); } if (keyData != retrievedKey) { diff --git a/ui/admin/tests/e2e/tests/dynamic-host-catalog.spec.js b/ui/admin/tests/e2e/tests/dynamic-host-catalog.spec.js index 6436da982..127d65efc 100644 --- a/ui/admin/tests/e2e/tests/dynamic-host-catalog.spec.js +++ b/ui/admin/tests/e2e/tests/dynamic-host-catalog.spec.js @@ -128,9 +128,9 @@ test.describe('AWS', async () => { if (!hostsAreVisible) { throw new Error( 'Hosts are not visible. EXPECTED: ' + - expectedHosts.length + - ', ACTUAL: ' + - rowCount + expectedHosts.length + + ', ACTUAL: ' + + rowCount ); } @@ -216,9 +216,9 @@ test.describe('AWS', async () => { if (!hostsAreVisible) { throw new Error( 'Hosts are not visible. EXPECTED: ' + - expectedHosts.length + - ', ACTUAL: ' + - rowCount + expectedHosts.length + + ', ACTUAL: ' + + rowCount ); } }); diff --git a/ui/admin/tests/e2e/tests/target.spec.js b/ui/admin/tests/e2e/tests/target.spec.js index d66e1c47d..cc790df20 100644 --- a/ui/admin/tests/e2e/tests/target.spec.js +++ b/ui/admin/tests/e2e/tests/target.spec.js @@ -61,18 +61,18 @@ test('Verify session created then cancel the session', async ({ page }) => { connect = exec( 'boundary connect' + - ' -target-id=' + - target.id + - ' -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=' + + target.id + + ' -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}}' ); await page diff --git a/ui/admin/tests/unit/abilities/credential-library-test.js b/ui/admin/tests/unit/abilities/credential-library-test.js index c162b8f8d..b1252507f 100644 --- a/ui/admin/tests/unit/abilities/credential-library-test.js +++ b/ui/admin/tests/unit/abilities/credential-library-test.js @@ -17,7 +17,7 @@ module('Unit | Abilities | credential-library', function (hooks) { hooks.beforeEach(function () { features = this.owner.lookup('service:features'); - features.enable('credential-library-vault-ssh-certificate'); + features.enable('ssh-target'); }); test('can read credential library type when authorized and feature is enabled', function (assert) { @@ -51,7 +51,7 @@ module('Unit | Abilities | credential-library', function (hooks) { const canService = this.owner.lookup('service:can'); const store = this.owner.lookup('service:store'); const featuresService = this.owner.lookup('service:features'); - featuresService.disable('credential-library-vault-ssh-certificate'); + featuresService.disable('ssh-target'); const credentialLibrary = store.createRecord('credential-library', { authorized_actions: [], type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE, @@ -66,7 +66,7 @@ module('Unit | Abilities | credential-library', function (hooks) { const canService = this.owner.lookup('service:can'); const store = this.owner.lookup('service:store'); const featuresService = this.owner.lookup('service:features'); - featuresService.disable('credential-library-vault-ssh-certificate'); + featuresService.disable('ssh-target'); const credentialLibrary = store.createRecord('credential-library', { authorized_actions: ['read'], type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE, diff --git a/ui/admin/tests/unit/instance-initializers/feature-edition-test.js b/ui/admin/tests/unit/instance-initializers/feature-edition-test.js new file mode 100644 index 000000000..1a917be63 --- /dev/null +++ b/ui/admin/tests/unit/instance-initializers/feature-edition-test.js @@ -0,0 +1,39 @@ +import Application from '@ember/application'; + +import config from 'admin/config/environment'; +import { initialize } from 'admin/instance-initializers/feature-edition'; +import { module, test } from 'qunit'; +import Resolver from 'ember-resolver'; +import { run } from '@ember/runloop'; + +module('Unit | Instance Initializer | feature-edition', function (hooks) { + hooks.beforeEach(function () { + this.TestApplication = class TestApplication extends Application { + modulePrefix = config.modulePrefix; + podModulePrefix = config.podModulePrefix; + Resolver = Resolver; + }; + + this.TestApplication.instanceInitializer({ + name: 'initializer under test', + initialize, + }); + + this.application = this.TestApplication.create({ + autoboot: false, + }); + + this.instance = this.application.buildInstance(); + }); + hooks.afterEach(function () { + run(this.instance, 'destroy'); + run(this.application, 'destroy'); + }); + + // TODO: Replace this with your real tests. + test('it works', async function (assert) { + await this.instance.boot(); + + assert.ok(true); + }); +}); diff --git a/ui/admin/tests/unit/services/feature-edition-test.js b/ui/admin/tests/unit/services/feature-edition-test.js new file mode 100644 index 000000000..9b922a472 --- /dev/null +++ b/ui/admin/tests/unit/services/feature-edition-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'admin/tests/helpers'; + +module('Unit | Service | features', function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let service = this.owner.lookup('service:features'); + assert.ok(service); + }); +});