Merge pull request #130 from hashicorp/oss/merge-main-acd719140dcb4aac4700316c6267bdc1961c39bc

[main] OSS to ENT merge of (acd7191)
This commit is contained in:
hc-github-team-secure-boundary authored and GitHub committed 2023-03-17 17:47:59 -04:00
commit 03ec3856aa
24 files changed
+303 -91

No files matched your search

@@ -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
+6 -1
View File
@@ -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
+3 -4
View File
@@ -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;
}
@@ -50,9 +50,7 @@
{{/if}}
</Hds::Form::Textarea::Field>
{{#if
(and @model.isNew (feature-flag 'credential-library-vault-ssh-certificate'))
}}
{{#if (and @model.isNew (feature-flag 'ssh-target'))}}
<Hds::Form::RadioCard::Group @name='type' @alignment='center' as |G|>
<G.Legend>{{t 'form.type.label'}}</G.Legend>
{{#each this.types as |type|}}
@@ -6,7 +6,7 @@
<div class='target-worker-filter' data-test-filter={{@name}}>
<Hds::Form::Toggle::Field
name='target-worker-filter-toggle'
name={{concat 'target-worker-filter-toggle-' @name}}
checked={{@toggleEnabled}}
disabled={{@disabled}}
@isOptional={{true}}
+1
View File
@@ -11,4 +11,5 @@ export default class ApplicationController extends Controller {
@service session;
@service flashMessages;
@service featureEdition;
}
@@ -0,0 +1,8 @@
export function initialize(owner) {
const service = owner.lookup('service:feature-edition');
service.initialize();
}
export default {
initialize,
};
+6
View File
@@ -20,6 +20,7 @@ export default class ApplicationRoute extends Route {
@service confirm;
@service router;
@service intl;
@service featureEdition;
// =attributes
@@ -126,4 +127,9 @@ export default class ApplicationRoute extends Route {
rootEl.classList.remove('rose-theme-light');
}
}
@action
toggleEdition(edition) {
this.featureEdition.setEdition(edition);
}
}
@@ -36,7 +36,7 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
// Set the type to generic vault if feature flag isn't enabled in cases where
// user sets the query parameter manually
if (!this.features.isEnabled('credential-library-vault-ssh-certificate')) {
if (!this.features.isEnabled('ssh-target')) {
type = TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC;
}
+42
View File
@@ -0,0 +1,42 @@
import Service from '@ember/service';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
import config from 'admin/config/environment';
const { selectedEdition, featureEditions } = config;
export default class FeatureEditionService extends Service {
// =services
@service features;
// =attributes
@tracked edition;
// array of edition strings
get editions() {
return Object.keys(featureEditions);
}
// =methods
/**
* Initializes the selected edition from the config.
*/
initialize() {
this.setEdition(selectedEdition);
}
setEdition(edition) {
const editionFlags = featureEditions[edition];
Object.keys(editionFlags).forEach((flag) => {
if (editionFlags[flag]) {
this.features.enable(flag);
} else {
this.features.disable(flag);
}
});
this.edition = edition;
}
}
+34 -1
View File
@@ -33,7 +33,7 @@
<dropdown.separator />
<dropdown.section @title={{t 'titles.toggle-color-theme'}}>
<dropdown.section @title={{t 'actions.toggle-color-theme'}}>
<Rose::Form as |form|>
<form.radioGroup
@name='theme'
@@ -67,6 +67,39 @@
</dropdown.section>
</utilities.dropdown>
{{#if (feature-flag 'dev-edition-toggle')}}
<utilities.dropdown
data-test-dev-edition-toggle
@text={{t
'titles.feature-edition'
edition=(t (concat 'editions.' this.featureEdition.edition))
}}
@icon='flight-icons/svg/layers-16'
as |dropdown|
>
<dropdown.section @title={{t 'actions.toggle-feature-edition'}}>
<Rose::Form as |form|>
<form.radioGroup
@name='edition'
@selectedValue={{this.featureEdition.edition}}
@changed={{route-action 'toggleEdition'}}
as |radioGroup|
>
{{#each this.featureEdition.editions as |edition|}}
<dropdown.item>
<radioGroup.radio
@label={{t (concat 'editions.' edition)}}
@value={{edition}}
/>
</dropdown.item>
{{/each}}
</form.radioGroup>
</Rose::Form>
</dropdown.section>
</utilities.dropdown>
{{/if}}
</header.utilities>
{{/if}}
+54 -36
View File
@@ -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') {
@@ -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);
@@ -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();
+29
View File
@@ -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();
});
});
@@ -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"]');
@@ -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);
@@ -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) {
@@ -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) {
@@ -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
);
}
});
+12 -12
View File
@@ -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
@@ -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,
@@ -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);
});
});
@@ -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);
});
});