diff --git a/addons/core/public/app-icons/credentials.svg b/addons/core/public/app-icons/credentials.svg index 72c15d48a..30c8e75eb 100644 --- a/addons/core/public/app-icons/credentials.svg +++ b/addons/core/public/app-icons/credentials.svg @@ -1,9 +1,5 @@ - - - - - - - - + + + + diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 727a34a5a..0867adc93 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -254,6 +254,7 @@ target: create: New Target delete: Delete Target add-host-sets: Add Host Sets + add-credential-libraries: Add Credential Libraries types: tcp: TCP host-set: @@ -267,6 +268,17 @@ target: add: title: Add Host Sets description: Select host sets to assign to this target. + credential-library: + messages: + welcome: + title: Welcome to Credential Libraries + description: No credential libraries in this target. + none: + title: No Credential Libraries Available + description: No credential libraries available to add to this target. + add: + title: Add Credential Libraries + description: Select credential libraries to assign to this target. credential-store: title: Credential Store title_plural: Credential Stores diff --git a/ui/admin/app/components/form/target/add-credential-libraries/index.hbs b/ui/admin/app/components/form/target/add-credential-libraries/index.hbs new file mode 100644 index 000000000..eb51f5129 --- /dev/null +++ b/ui/admin/app/components/form/target/add-credential-libraries/index.hbs @@ -0,0 +1,55 @@ +{{#if this.hasAvailableCredentialLibraries}} + + + + + + + {{t 'form.id.label'}} + {{t 'form.name.label'}} + + + + {{#each this.filteredCredentialLibraries as |credentialLibrary|}} + + + + + {{credentialLibrary.name}} + + {{/each}} + + + +{{/if}} + +{{#unless this.hasAvailableCredentialLibraries}} + + + + {{t 'resources.target.credential-library.messages.none.description'}} + + + + {{t 'actions.back'}} + + + +{{/unless}} \ No newline at end of file diff --git a/ui/admin/app/components/form/target/add-credential-libraries/index.js b/ui/admin/app/components/form/target/add-credential-libraries/index.js new file mode 100644 index 000000000..21ce6250a --- /dev/null +++ b/ui/admin/app/components/form/target/add-credential-libraries/index.js @@ -0,0 +1,65 @@ +import Component from '@glimmer/component'; +import { computed, action } from '@ember/object'; +import { A } from '@ember/array'; + +export default class FormTargetAddCredentialLibrariesIndexComponent extends Component { + // =properties + + /** + * Array of selected credential library IDs. + * @type {EmberArray} + */ + selectedCredentialLibraryIDs = A(); + + /** + * Checks for unassigned credential libraries. + * @param {[CredentialLibraryModel]} filteredCredentialLibraries + * @type {boolean} + */ + @computed('filteredCredentialLibraries.length') + get hasAvailableCredentialLibraries() { + return this.filteredCredentialLibraries.length > 0; + } + + /** + * Filter out credential libraries not already added to the target. + * @type {[CredentialLibraryModel]} + */ + @computed( + 'args.{credentialLibraries.[],model.application_credential_library_ids.[]}' + ) + get filteredCredentialLibraries() { + // Get IDs for credential libraries already added to the current target + const currentCredentialLibraryIDs = + this.args.model.application_credential_library_ids.map( + (credentialLibrary) => credentialLibrary.value + ); + const notAddedCredentialLibraries = this.args.credentialLibraries.filter( + ({ id }) => !currentCredentialLibraryIDs.includes(id) + ); + return notAddedCredentialLibraries; + } + + // =actions + + /** + * Add/Remove credential library to current selection + * @param CredentialLibraryModel + */ + @action + toggleCredentialLibrary(credentialLibrary) { + if (!this.selectedCredentialLibraryIDs.includes(credentialLibrary.id)) { + this.selectedCredentialLibraryIDs.addObject(credentialLibrary.id); + } else { + this.selectedCredentialLibraryIDs.removeObject(credentialLibrary.id); + } + } + + /** + * Submit selected credential library ids + */ + @action + submit(fn) { + fn(this.selectedCredentialLibraryIDs); + } +} diff --git a/ui/admin/app/controllers/scopes/scope/targets/target/credential-libraries.js b/ui/admin/app/controllers/scopes/scope/targets/target/credential-libraries.js new file mode 100644 index 000000000..ff0f8c558 --- /dev/null +++ b/ui/admin/app/controllers/scopes/scope/targets/target/credential-libraries.js @@ -0,0 +1,18 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class ScopesScopeTargetsTargetCredentialLibrariesController extends Controller { + // =services + + @service intl; + + // =attributes + + /** + * Translated credential library breadcrumb + * @type {string} + */ + get breadCrumb() { + return this.intl.t('resources.credential-library.title_plural'); + } +} diff --git a/ui/admin/app/router.js b/ui/admin/app/router.js index a1bfa1e18..6b70bd999 100644 --- a/ui/admin/app/router.js +++ b/ui/admin/app/router.js @@ -65,6 +65,8 @@ Router.map(function () { this.route('target', { path: ':target_id' }, function () { this.route('host-sets', function () {}); this.route('add-host-sets'); + this.route('credential-libraries'); + this.route('add-credential-libraries'); }); this.route('new'); }); diff --git a/ui/admin/app/routes/scopes/scope/targets/target/add-credential-libraries.js b/ui/admin/app/routes/scopes/scope/targets/target/add-credential-libraries.js new file mode 100644 index 000000000..f2cb1eca2 --- /dev/null +++ b/ui/admin/app/routes/scopes/scope/targets/target/add-credential-libraries.js @@ -0,0 +1,94 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { all } from 'rsvp'; +import { action } from '@ember/object'; +import loading from 'ember-loading/decorator'; +import { notifySuccess, notifyError } from 'core/decorators/notify'; + +export default class ScopesScopeTargetsTargetAddCredentialLibrariesRoute extends Route { + // =services + + @service intl; + @service notify; + + // =methods + + /** + * Empty out any previously loaded credential libraries. + */ + beforeModel() { + this.store.unloadAll('credential-library'); + } + + /** + * Returns the current target and all credential libraries. + * @return {{target: TargetModel, credentialLibraries: [CredentialLibraryModel]}} + */ + async model() { + const target = this.modelFor('scopes.scope.targets.target'); + const { id: scope_id } = this.modelFor('scopes.scope'); + const credentialStores = await this.store.query('credential-store', { + scope_id, + }); + await all( + credentialStores.map(({ id: credential_store_id }) => + this.store.query('credential-library', { credential_store_id }) + ) + ); + const credentialLibraries = this.store.peekAll('credential-library'); + return { + target, + credentialLibraries, + }; + } + + /** + * Renders credential libraries specific header template. + * @override + */ + renderTemplate() { + super.renderTemplate(...arguments); + + this.render( + 'scopes/scope/targets/target/add-credential-libraries/-header', + { + into: 'scopes/scope/targets/target', + outlet: 'header', + } + ); + + this.render('-empty', { + into: 'scopes/scope/targets/target', + outlet: 'navigation', + }); + + this.render('-empty', { + into: 'scopes/scope/targets/target', + outlet: 'actions', + }); + } + + // =actions + + /** + * Add credential libraries to current target + * @param {TargetModel} target + * @param {[string]} credentialLibraryIDs + */ + @action + @loading + @notifyError(({ message }) => message, { catch: true }) + @notifySuccess('notifications.add-success') + async save(target, credentialLibraryIDs) { + await target.addCredentialLibraries(credentialLibraryIDs); + this.replaceWith('scopes.scope.targets.target.credential-libraries'); + } + + /** + * Redirect to target credential libraries as if nothing ever happened. + */ + @action + cancel() { + this.replaceWith('scopes.scope.targets.target.credential-libraries'); + } +} diff --git a/ui/admin/app/routes/scopes/scope/targets/target/credential-libraries.js b/ui/admin/app/routes/scopes/scope/targets/target/credential-libraries.js new file mode 100644 index 000000000..17e3e2692 --- /dev/null +++ b/ui/admin/app/routes/scopes/scope/targets/target/credential-libraries.js @@ -0,0 +1,34 @@ +import Route from '@ember/routing/route'; +import { hash, all } from 'rsvp'; + +export default class ScopesScopeTargetsTargetCredentialLibrariesRoute extends Route { + // =methods + + /** + * Loads all credential libraries under the current target. + * @return {Promise{[CredentialLibraryModel]}} + */ + beforeModel() { + const { application_credential_library_ids: credential_libraries } = + this.modelFor('scopes.scope.targets.target'); + return hash({ + credentialLibraries: all( + credential_libraries.map((credential_library) => + this.store.findRecord( + 'credential-library', + credential_library.value, + { reload: true } + ) + ) + ), + }); + } + + /** + * Returns the previously loaded target instance. + * @return {TargetModel} + */ + model() { + return this.modelFor('scopes.scope.targets.target'); + } +} diff --git a/ui/admin/app/templates/scopes/scope.hbs b/ui/admin/app/templates/scopes/scope.hbs index b0040f3e7..65e456c51 100644 --- a/ui/admin/app/templates/scopes/scope.hbs +++ b/ui/admin/app/templates/scopes/scope.hbs @@ -85,7 +85,7 @@ {{#if (feature-flag 'credential-store')}} - + {{t 'resources.credential-store.title_plural'}} {{/if}} diff --git a/ui/admin/app/templates/scopes/scope/targets/target/-actions.hbs b/ui/admin/app/templates/scopes/scope/targets/target/-actions.hbs index 9b6b207e3..9c95b8fa2 100644 --- a/ui/admin/app/templates/scopes/scope/targets/target/-actions.hbs +++ b/ui/admin/app/templates/scopes/scope/targets/target/-actions.hbs @@ -6,6 +6,13 @@ {{t 'resources.target.actions.add-host-sets'}} + {{#if (feature-flag 'credential-store')}} + + {{t 'resources.target.actions.add-credential-libraries'}} + + {{/if}} {{t 'resources.host-set.title_plural'}} + {{#if (feature-flag 'credential-store')}} + + {{t 'resources.credential-library.title_plural'}} + + {{/if}} \ No newline at end of file diff --git a/ui/admin/app/templates/scopes/scope/targets/target/add-credential-libraries.hbs b/ui/admin/app/templates/scopes/scope/targets/target/add-credential-libraries.hbs new file mode 100644 index 000000000..e4573894c --- /dev/null +++ b/ui/admin/app/templates/scopes/scope/targets/target/add-credential-libraries.hbs @@ -0,0 +1,27 @@ +{{page-title (t 'resources.target.actions.add-credential-libraries')}} + +{{#if @model.credentialLibraries}} + +{{/if}} + +{{#unless @model.credentialLibraries}} + + + + {{t 'resources.target.credential-library.messages.none.description'}} + + + + {{t 'actions.back'}} + + + +{{/unless}} \ No newline at end of file diff --git a/ui/admin/app/templates/scopes/scope/targets/target/add-credential-libraries/-header.hbs b/ui/admin/app/templates/scopes/scope/targets/target/add-credential-libraries/-header.hbs new file mode 100644 index 000000000..4ae5bad96 --- /dev/null +++ b/ui/admin/app/templates/scopes/scope/targets/target/add-credential-libraries/-header.hbs @@ -0,0 +1,4 @@ +

+ {{t 'resources.target.credential-library.messages.add.title'}} +

+

{{t 'resources.target.credential-library.messages.add.description'}}

\ No newline at end of file diff --git a/ui/admin/app/templates/scopes/scope/targets/target/credential-libraries.hbs b/ui/admin/app/templates/scopes/scope/targets/target/credential-libraries.hbs new file mode 100644 index 000000000..ebba89bff --- /dev/null +++ b/ui/admin/app/templates/scopes/scope/targets/target/credential-libraries.hbs @@ -0,0 +1,54 @@ +{{page-title (t 'resources.credential-library.title_plural')}} + +{{#if @model.credentialLibraries}} + + + + + {{t 'form.name.label'}} + {{t 'form.id.label'}} + + + + {{#each @model.credentialLibraries as |credentialLibrary|}} + + + + {{credentialLibrary.displayName}} + +

{{credentialLibrary.description}}

+
+ + + {{credentialLibrary.id}} + + +
+ {{/each}} +
+
+ +{{else}} + + + + + {{t 'resources.target.credential-library.messages.welcome.description'}} + + + + +{{/if}} \ No newline at end of file diff --git a/ui/admin/tests/acceptance/credential-library/credential-library-test.js b/ui/admin/tests/acceptance/credential-library/credential-library-test.js index dfab8c002..cd515e8f0 100644 --- a/ui/admin/tests/acceptance/credential-library/credential-library-test.js +++ b/ui/admin/tests/acceptance/credential-library/credential-library-test.js @@ -1,8 +1,9 @@ import { module, test } from 'qunit'; -import { visit, click, find } from '@ember/test-helpers'; +import { visit, click, fillIn, currentURL, find } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { authenticateSession } from 'ember-simple-auth/test-support'; +import { Response } from 'miragejs'; import { resolve, reject } from 'rsvp'; import sinon from 'sinon'; @@ -56,12 +57,65 @@ module('Acceptance | credential-library', function (hooks) { urls.credentialStore = `${urls.credentialStores}/${instances.credentialStore.id}`; urls.credentialLibraries = `${urls.credentialStore}/credential-libraries`; urls.credentialLibrary = `${urls.credentialLibraries}/${instances.credentialLibrary.id}`; + urls.newCredentialLibrary = `${urls.credentialLibraries}/new`; // Generate resource couner getCredentialLibraryCount = () => this.server.schema.credentialLibraries.all().models.length; authenticateSession({}); }); + test('can create a credential library', async function (assert) { + assert.expect(1); + const count = getCredentialLibraryCount(); + await visit(urls.newCredentialLibrary); + await fillIn('[name="name"]', 'random string'); + await click('[type="submit"]'); + assert.equal(getCredentialLibraryCount(), count + 1); + }); + + test('can cancel create a new credential library', async function (assert) { + assert.expect(2); + const count = getCredentialLibraryCount(); + await visit(urls.newCredentialLibrary); + await fillIn('[name="name"]', 'random string'); + await click('.rose-form-actions [type="button"]'); + assert.equal(currentURL(), urls.credentialLibraries); + assert.equal(getCredentialLibraryCount(), count); + }); + + test('saving a new credential library with invalid fields displays error messasges', async function (assert) { + assert.expect(2); + this.server.post('/credential-libraries', () => { + return new Response( + 400, + {}, + { + status: 400, + code: 'invalid_argument', + message: 'The request was invalid', + details: { + request_fields: [ + { + name: 'name', + description: 'Name is required.', + }, + ], + }, + } + ); + }); + await visit(urls.newCredentialLibrary); + await click('[type="submit"]'); + assert.ok( + find('[role="alert"]').textContent.trim(), + 'The request was invalid.' + ); + assert.ok( + find('.rose-form-error-message').textContent.trim(), + 'Name is required.' + ); + }); + test('can delete credential library', async function (assert) { assert.expect(1); const count = getCredentialLibraryCount(); diff --git a/ui/admin/tests/unit/controllers/scopes/scope/targets/target/credential-libraries-test.js b/ui/admin/tests/unit/controllers/scopes/scope/targets/target/credential-libraries-test.js new file mode 100644 index 000000000..76a26ff69 --- /dev/null +++ b/ui/admin/tests/unit/controllers/scopes/scope/targets/target/credential-libraries-test.js @@ -0,0 +1,17 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module( + 'Unit | Controller | scopes/scope/targets/target/credential-libraries', + function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let controller = this.owner.lookup( + 'controller:scopes/scope/targets/target/credential-libraries' + ); + assert.ok(controller); + }); + } +); diff --git a/ui/admin/tests/unit/routes/scopes/scope/targets/target/add-credential-libraries-test.js b/ui/admin/tests/unit/routes/scopes/scope/targets/target/add-credential-libraries-test.js new file mode 100644 index 000000000..e6279d5b3 --- /dev/null +++ b/ui/admin/tests/unit/routes/scopes/scope/targets/target/add-credential-libraries-test.js @@ -0,0 +1,16 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module( + 'Unit | Route | scopes/scope/targets/target/add-credential-libraries', + function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let route = this.owner.lookup( + 'route:scopes/scope/targets/target/add-credential-libraries' + ); + assert.ok(route); + }); + } +); diff --git a/ui/admin/tests/unit/routes/scopes/scope/targets/target/credential-libraries-test.js b/ui/admin/tests/unit/routes/scopes/scope/targets/target/credential-libraries-test.js new file mode 100644 index 000000000..894e3add0 --- /dev/null +++ b/ui/admin/tests/unit/routes/scopes/scope/targets/target/credential-libraries-test.js @@ -0,0 +1,16 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module( + 'Unit | Route | scopes/scope/targets/target/credential-libraries', + function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let route = this.owner.lookup( + 'route:scopes/scope/targets/target/credential-libraries' + ); + assert.ok(route); + }); + } +);