diff --git a/addons/api/addon/models/credential-library.js b/addons/api/addon/models/credential-library.js
index c95b9f73e..a77e605ba 100644
--- a/addons/api/addon/models/credential-library.js
+++ b/addons/api/addon/models/credential-library.js
@@ -2,6 +2,13 @@ import GeneratedCredentialLibraryModel from '../generated/models/credential-libr
import { attr } from '@ember-data/model';
import { fragment } from 'ember-data-model-fragments/attributes';
+/**
+ * Enum options for credential library.
+ */
+export const options = {
+ http_method: ['GET', 'POST'],
+};
+
export default class CredentialLibraryModel extends GeneratedCredentialLibraryModel {
// =error attributes
// These attributes exist solely to capture errors on nested fields.
diff --git a/addons/api/addon/models/fragment-credential-library-attributes.js b/addons/api/addon/models/fragment-credential-library-attributes.js
index dd6138199..582922601 100644
--- a/addons/api/addon/models/fragment-credential-library-attributes.js
+++ b/addons/api/addon/models/fragment-credential-library-attributes.js
@@ -1,5 +1,6 @@
import Fragment from 'ember-data-model-fragments/fragment';
import { attr } from '@ember-data/model';
+import { options } from './credential-library';
export default class FragmentCredentialLibraryAttributesModel extends Fragment {
//= attributes (vault)
@@ -7,6 +8,7 @@ export default class FragmentCredentialLibraryAttributesModel extends Fragment {
@attr('string', {
description:
'The HTTP method the library uses when requesting credentials from Vault.',
+ defaultValue: () => options.http_method[0],
})
http_method;
diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js
index 03a767a36..c1b535a41 100644
--- a/addons/api/mirage/config.js
+++ b/addons/api/mirage/config.js
@@ -497,7 +497,15 @@ export default function () {
) => credentialLibraries.where({ credentialStoreId })
);
this.get('/credential-libraries/:id');
- this.post('/credential-libraries/:id');
+ this.post(
+ '/credential-libraries',
+ function ({ credentialStores, credentialLibraries }) {
+ const attrs = this.normalizedRequestAttrs();
+ const credentialStore = credentialStores.find(attrs.credentialStoreId);
+ attrs.scopeId = credentialStore.scope.id;
+ return credentialLibraries.create(attrs);
+ }
+ );
this.del('/credential-libraries/:id');
this.patch('/credential-libraries/:id');
diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml
index 9d8564288..8094abf9e 100644
--- a/addons/core/translations/resources/en-us.yaml
+++ b/addons/core/translations/resources/en-us.yaml
@@ -277,9 +277,19 @@ credential-library:
title: Credential Library
title_plural: Credential Libraries
description: A credential library is a resource that provides credentials.
+ actions:
+ create: New Credential Library
messages:
none:
title: No Credential Libraries Available
description: No Credential Libraries available in this credential store.
- actions:
- create: New Credential Library
+ form:
+ vault_path:
+ label: Vault Path
+ help: The path the library uses when requesting credentials from Vault.
+ http_method:
+ label: HTTP Method
+ help: The HTTP method the library uses when requesting credentials from Vault.
+ http_request_body:
+ label: HTTP Method POST Request Body
+ help: The body of the HTTP request the library sends to Vault when requesting credentials. Only allowed when HTTP method is set to POST.
diff --git a/addons/rose/addon/components/rose/form/index.hbs b/addons/rose/addon/components/rose/form/index.hbs
index 8c545ec22..e23e52c0b 100644
--- a/addons/rose/addon/components/rose/form/index.hbs
+++ b/addons/rose/addon/components/rose/form/index.hbs
@@ -20,6 +20,7 @@
input=(component 'rose/form/input' disabled=this.disabled)
select=(component 'rose/form/select' disabled=this.disabled)
textarea=(component 'rose/form/textarea' disabled=this.disabled)
+ fieldset=(component 'rose/form/fieldset')
actions=(component
(if this.showEditToggleButton
'rose/form/actions/edit-toggle'
diff --git a/addons/rose/addon/styles/rose/components/form/_fieldset.scss b/addons/rose/addon/styles/rose/components/form/_fieldset.scss
index f35d3755c..c01a77a60 100644
--- a/addons/rose/addon/styles/rose/components/form/_fieldset.scss
+++ b/addons/rose/addon/styles/rose/components/form/_fieldset.scss
@@ -5,7 +5,7 @@
margin-bottom: sizing.rems(m);
.rose-form-fieldset-title {
- @include type.type(s, bold);
+ @include type.type(s, semibold);
}
.rose-form-fieldset-description {
diff --git a/ui/admin/app/components/form/credential-library/index.hbs b/ui/admin/app/components/form/credential-library/index.hbs
new file mode 100644
index 000000000..beef7e8c6
--- /dev/null
+++ b/ui/admin/app/components/form/credential-library/index.hbs
@@ -0,0 +1,116 @@
+
+
+ {{#if @model.errors.name}}
+
+ {{#each @model.errors.name as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+
+ {{#if @model.errors.description}}
+
+ {{#each @model.errors.description as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+
+ {{#if @model.errors.attributes_path}}
+
+ {{#each @model.errors.attributes_path as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+
+ <:title>{{t 'resources.credential-library.form.http_method.label'}}
+ <:description>{{t
+ 'resources.credential-library.form.http_method.help'
+ }}
+ <:body>
+
+ {{#each this.httpMethodOptions as |httpMethod|}}
+
+ {{/each}}
+
+
+
+
+ {{#if this.isHttpRequestBodyAllowed}}
+
+ {{#if @model.errors.attributes_http_request_body}}
+
+ {{#each @model.errors.attributes_http_request_body as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+ {{/if}}
+
+
+
\ No newline at end of file
diff --git a/ui/admin/app/components/form/credential-library/index.js b/ui/admin/app/components/form/credential-library/index.js
new file mode 100644
index 000000000..b46e8d803
--- /dev/null
+++ b/ui/admin/app/components/form/credential-library/index.js
@@ -0,0 +1,20 @@
+import Component from '@glimmer/component';
+import { options } from 'api/models/credential-library';
+import { computed } from '@ember/object';
+
+export default class FormCredentialLibraryIndexComponent extends Component {
+ // =properties
+ /**
+ * @type {object}
+ */
+ httpMethodOptions = options.http_method;
+
+ /**
+ * Only allow HTTP request body field if http_method is set to POST.
+ * @type {boolean}
+ */
+ @computed('args.model.attributes.http_method')
+ get isHttpRequestBodyAllowed() {
+ return this.args.model.attributes.http_method?.match(/post/i);
+ }
+}
diff --git a/ui/admin/app/controllers/scopes/scope/credential-stores/credential-store/credential-libraries.js b/ui/admin/app/controllers/scopes/scope/credential-stores/credential-store/credential-libraries.js
new file mode 100644
index 000000000..fe9084ea7
--- /dev/null
+++ b/ui/admin/app/controllers/scopes/scope/credential-stores/credential-store/credential-libraries.js
@@ -0,0 +1,18 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+
+export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesController extends Controller {
+ // =services
+
+ @service intl;
+
+ // =attributes
+
+ /**
+ * Translated credential libraries breadcrumb
+ * @type {string}
+ */
+ get breadCrumb() {
+ return this.intl.t('resources.credential-library.title_plural');
+ }
+}
diff --git a/ui/admin/app/controllers/scopes/scope/credential-stores/credential-store/credential-libraries/new.js b/ui/admin/app/controllers/scopes/scope/credential-stores/credential-store/credential-libraries/new.js
new file mode 100644
index 000000000..ff4053f92
--- /dev/null
+++ b/ui/admin/app/controllers/scopes/scope/credential-stores/credential-store/credential-libraries/new.js
@@ -0,0 +1,18 @@
+import Controller from '@ember/controller';
+import { inject as service } from '@ember/service';
+
+export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesNewController extends Controller {
+ // =services
+
+ @service intl;
+
+ // =attributes
+
+ /**
+ * Translated new credential libraries breadcrumb
+ * @type {string}
+ */
+ get breadCrumb() {
+ return this.intl.t('titles.new');
+ }
+}
diff --git a/ui/admin/app/router.js b/ui/admin/app/router.js
index 4012791b2..3c359e268 100644
--- a/ui/admin/app/router.js
+++ b/ui/admin/app/router.js
@@ -91,7 +91,9 @@ Router.map(function () {
'credential-store',
{ path: ':credential_store_id' },
function () {
- this.route('credential-libraries', function () {});
+ this.route('credential-libraries', function () {
+ this.route('new');
+ });
}
);
this.route('new');
diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries.js b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries.js
index 7c8ade755..fa54e9691 100644
--- a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries.js
+++ b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries.js
@@ -1,4 +1,7 @@
import Route from '@ember/routing/route';
+import { action } from '@ember/object';
+import loading from 'ember-loading/decorator';
+import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesRoute extends Route {
// =methods
@@ -13,4 +16,38 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
);
return this.store.query('credential-library', { credential_store_id });
}
+
+ // =actions
+
+ /**
+ * Rollback changes on a credential library.
+ * @param {CredentialLibraryModel} credentialLibrary
+ */
+ @action
+ cancel(credentialLibrary) {
+ const { isNew } = credentialLibrary;
+ credentialLibrary.rollbackAttributes();
+ if (isNew)
+ this.transitionTo(
+ 'scopes.scope.credential-stores.credential-store.credential-libraries'
+ );
+ }
+
+ /**
+ * Handle save of a credential library
+ * @param {CredentialLibraryModel} credentialLibrary
+ */
+ @action
+ @loading
+ @notifyError(({ message }) => message)
+ @notifySuccess(({ isNew }) =>
+ isNew ? 'notifications.create-success' : 'notifications.save-success'
+ )
+ async save(credentialLibrary) {
+ await credentialLibrary.save();
+ await this.transitionTo(
+ 'scopes.scope.credential-stores.credential-store.credential-libraries'
+ );
+ this.refresh();
+ }
}
diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new.js b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new.js
new file mode 100644
index 000000000..d83109dda
--- /dev/null
+++ b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new.js
@@ -0,0 +1,45 @@
+import Route from '@ember/routing/route';
+
+export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesNewRoute extends Route {
+ // =methods
+
+ /**
+ * Creates a new unsaved credential library in current credential store.
+ * @return {CredentialLibraryModel}
+ */
+ model() {
+ const { id: credential_store_id } = this.modelFor(
+ 'scopes.scope.credential-stores.credential-store'
+ );
+ return this.store.createRecord('credential-library', {
+ credential_store_id,
+ });
+ }
+
+ /**
+ * Renders new credential library specific templates for header, navigation,
+ * and action page sections.
+ * @override
+ */
+ renderTemplate() {
+ super.renderTemplate(...arguments);
+
+ this.render(
+ 'scopes/scope/credential-stores/credential-store/credential-libraries/new/-header',
+ {
+ into: 'scopes/scope/credential-stores/credential-store',
+ outlet: 'header',
+ }
+ );
+
+ this.render('_empty', {
+ into: 'scopes/scope/credential-stores/credential-store',
+ outlet: 'navigation',
+ });
+
+ this.render('_empty', {
+ into: 'scopes/scope/credential-stores/credential-store',
+ outlet: 'actions',
+ });
+ }
+}
diff --git a/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/-actions.hbs b/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/-actions.hbs
index e19af1ada..c08355da6 100644
--- a/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/-actions.hbs
+++ b/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/-actions.hbs
@@ -3,10 +3,13 @@
@dropdownRight={{true}}
as |dropdown|
>
- {{!--
+
{{t 'resources.credential-library.actions.create'}}
- --}}
+
{{t 'resources.credential-library.messages.none.description'}}
+
+
+ {{t 'titles.new'}}
+
diff --git a/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/credential-libraries/new.hbs b/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/credential-libraries/new.hbs
new file mode 100644
index 000000000..5d03e5b9f
--- /dev/null
+++ b/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/credential-libraries/new.hbs
@@ -0,0 +1,7 @@
+{{page-title (t 'titles.new')}}
+
+
\ No newline at end of file
diff --git a/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/credential-libraries/new/-header.hbs b/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/credential-libraries/new/-header.hbs
new file mode 100644
index 000000000..51755b136
--- /dev/null
+++ b/ui/admin/app/templates/scopes/scope/credential-stores/credential-store/credential-libraries/new/-header.hbs
@@ -0,0 +1,3 @@
+
+ {{t 'titles.new'}}
+
\ No newline at end of file
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/credential-stores/credential-store/credential-libraries-test.js b/ui/admin/tests/unit/controllers/scopes/scope/credential-stores/credential-store/credential-libraries-test.js
new file mode 100644
index 000000000..944329525
--- /dev/null
+++ b/ui/admin/tests/unit/controllers/scopes/scope/credential-stores/credential-store/credential-libraries-test.js
@@ -0,0 +1,17 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Controller | scopes/scope/credential-stores/credential-store/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/credential-stores/credential-store/credential-libraries'
+ );
+ assert.ok(controller);
+ });
+ }
+);
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/credential-stores/credential-store/credential-libraries/new-test.js b/ui/admin/tests/unit/controllers/scopes/scope/credential-stores/credential-store/credential-libraries/new-test.js
new file mode 100644
index 000000000..2dce4303a
--- /dev/null
+++ b/ui/admin/tests/unit/controllers/scopes/scope/credential-stores/credential-store/credential-libraries/new-test.js
@@ -0,0 +1,17 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Controller | scopes/scope/credential-stores/credential-store/credential-libraries/new',
+ function (hooks) {
+ setupTest(hooks);
+
+ // TODO: Replace this with your real tests.
+ test('it exists', function (assert) {
+ let controller = this.owner.lookup(
+ 'controller:scopes/scope/credential-stores/credential-store/credential-libraries/new'
+ );
+ assert.ok(controller);
+ });
+ }
+);
diff --git a/ui/admin/tests/unit/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new-test.js b/ui/admin/tests/unit/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new-test.js
new file mode 100644
index 000000000..3c4efdd66
--- /dev/null
+++ b/ui/admin/tests/unit/routes/scopes/scope/credential-stores/credential-store/credential-libraries/new-test.js
@@ -0,0 +1,16 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Route | scopes/scope/credential-stores/credential-store/credential-libraries/new',
+ function (hooks) {
+ setupTest(hooks);
+
+ test('it exists', function (assert) {
+ let route = this.owner.lookup(
+ 'route:scopes/scope/credential-stores/credential-store/credential-libraries/new'
+ );
+ assert.ok(route);
+ });
+ }
+);