diff --git a/addons/api/addon/serializers/credential.js b/addons/api/addon/serializers/credential.js
index 92915d775..c3e84f577 100644
--- a/addons/api/addon/serializers/credential.js
+++ b/addons/api/addon/serializers/credential.js
@@ -26,15 +26,19 @@ export default class CredentialSerializer extends ApplicationSerializer {
serializeUsernamePassword() {
const serialized = super.serialize(...arguments);
// Remove non-username_password type attributes
- delete serialized['attributes']['private_key'];
- delete serialized['attributes']['passphrase'];
+ delete serialized.attributes.private_key;
+ delete serialized.attributes.passphrase;
return serialized;
}
serializeSSHPrivateKey() {
const serialized = super.serialize(...arguments);
// Remove non-ssh_private_key type attributes
- delete serialized['attributes']['password'];
+ delete serialized.attributes.password;
+ if (!serialized?.attributes?.passphrase)
+ delete serialized.attributes.passphrase;
+ if (!serialized?.attributes?.private_key)
+ delete serialized.attributes.private_key;
return serialized;
}
@@ -42,7 +46,9 @@ export default class CredentialSerializer extends ApplicationSerializer {
const normalizedHash = copy(hash, true);
const normalized = super.normalize(typeClass, normalizedHash, ...rest);
// Remove passphrase as we don't track it after being created/updated
- delete normalized['data']['attributes']['passphrase'];
+ normalized.data.attributes.passphrase = '';
+ normalized.data.attributes.password = '';
+ normalized.data.attributes.private_key = '';
return normalized;
}
}
diff --git a/addons/api/tests/unit/serializers/credential-test.js b/addons/api/tests/unit/serializers/credential-test.js
index 7daf66aeb..e856add16 100644
--- a/addons/api/tests/unit/serializers/credential-test.js
+++ b/addons/api/tests/unit/serializers/credential-test.js
@@ -86,6 +86,9 @@ module('Unit | Serializer | credential', function (hooks) {
type: 'username_password',
username: 'username',
version: 1,
+ passphrase: '',
+ private_key: '',
+ password: '',
},
type: 'credential',
id: 'credup_123',
@@ -117,6 +120,9 @@ module('Unit | Serializer | credential', function (hooks) {
type: 'ssh_private_key',
username: 'username',
version: 1,
+ passphrase: '',
+ private_key: '',
+ password: '',
},
type: 'credential',
id: 'credspk_123',
diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml
index a4f89d0a5..d8dca572b 100644
--- a/addons/core/translations/resources/en-us.yaml
+++ b/addons/core/translations/resources/en-us.yaml
@@ -396,6 +396,20 @@ credential:
ssh_private_key: Username & Key Pair
help:
username_password: This host requires a username and password to connect.
+ ssh_private_key: This host requires a username, public key, and private key to connect.
+ form:
+ username:
+ label: Username
+ help: The username to use when connecting with this credential store.
+ password:
+ label: Password
+ help: The password to use when signing in with this credential store.
+ private_key:
+ label: SSH Private Key
+ help: The private key to use when connecting with this credential store.
+ passphrase:
+ label: Pass phrase
+ help: Optional.
scope:
title: Scope
title_plural: Scopes
diff --git a/ui/admin/app/components/form/credential/index.hbs b/ui/admin/app/components/form/credential/index.hbs
index f501d63b2..a27830b5c 100644
--- a/ui/admin/app/components/form/credential/index.hbs
+++ b/ui/admin/app/components/form/credential/index.hbs
@@ -1,118 +1,9 @@
-
-
-
- {{#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}}
-
-
- <:title>{{t 'form.type.label'}}
- <:body>
-
-
-
-
-
-
-
- {{#if @model.errors.username}}
-
- {{#each @model.errors.username as |error|}}
- {{error.message}}
- {{/each}}
-
- {{/if}}
-
-
- {{#if (or @model.isNew form.isEditable)}}
-
- {{#if @model.errors.password}}
-
- {{#each @model.errors.password as |error|}}
- {{error.message}}
- {{/each}}
-
- {{/if}}
-
- {{/if}}
-
- {{#if (can 'save model' @model)}}
-
- {{/if}}
-
-
\ No newline at end of file
+{{#if @model.type}}
+ {{component
+ (concat 'form/credential/' @model.type)
+ model=@model
+ submit=@submit
+ cancel=@cancel
+ changeType=@changeType
+ }}
+{{/if}}
\ No newline at end of file
diff --git a/ui/admin/app/components/form/credential/ssh_private_key/index.hbs b/ui/admin/app/components/form/credential/ssh_private_key/index.hbs
new file mode 100644
index 000000000..36fb1a761
--- /dev/null
+++ b/ui/admin/app/components/form/credential/ssh_private_key/index.hbs
@@ -0,0 +1,154 @@
+
+
+
+ {{#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}}
+
+
+ <:title>{{t 'form.type.label'}}
+ <:body>
+ {{#if @model.isNew}}
+
+
+
+
+ {{else}}
+
+
+
+ {{/if}}
+
+
+
+
+ {{#if @model.errors.username}}
+
+ {{#each @model.errors.username as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+ {{#if (or @model.isNew form.isEditable)}}
+
+ {{#if @model.errors.private_key}}
+
+ {{#each @model.errors.private_key as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+ {{#if @model.errors.passphrase}}
+
+ {{#each @model.errors.passphrase as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+ {{/if}}
+
+ {{#if (can 'save model' @model)}}
+
+ {{/if}}
+
+
\ No newline at end of file
diff --git a/ui/admin/app/components/form/credential/username_password/index.hbs b/ui/admin/app/components/form/credential/username_password/index.hbs
new file mode 100644
index 000000000..0bf027844
--- /dev/null
+++ b/ui/admin/app/components/form/credential/username_password/index.hbs
@@ -0,0 +1,136 @@
+
+
+
+ {{#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}}
+
+
+ <:title>{{t 'form.type.label'}}
+ <:body>
+ {{#if @model.isNew}}
+
+
+
+
+ {{else}}
+
+
+
+ {{/if}}
+
+
+
+
+ {{#if @model.errors.username}}
+
+ {{#each @model.errors.username as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+ {{#if (or @model.isNew form.isEditable)}}
+
+ {{#if @model.errors.password}}
+
+ {{#each @model.errors.password as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+ {{/if}}
+
+ {{#if (can 'save model' @model)}}
+
+ {{/if}}
+
+
\ No newline at end of file
diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials.js b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials.js
index 6a20de805..070a9a40a 100644
--- a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials.js
+++ b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials.js
@@ -58,9 +58,6 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialsRoute
isNew ? 'notifications.create-success' : 'notifications.save-success'
)
async save(credential) {
- //hardcoding the type to its subtype
- //as the api expects the subtype `username_password`
- credential.type = 'username_password';
await credential.save();
if (this.can.can('read model', credential)) {
await this.router.transitionTo(
diff --git a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials/new.js b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials/new.js
index 1331d410d..185534339 100644
--- a/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials/new.js
+++ b/ui/admin/app/routes/scopes/scope/credential-stores/credential-store/credentials/new.js
@@ -19,17 +19,27 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialsNewRou
/**
* Creates a new unsaved credential in current credential store.
+ * Also rollback/destroy any new, unsaved instances from this route before
+ * creating another, but reuse name/description if available.
* @return {CredentialModel}
*/
model({ type = 'username_password' }) {
const { id: credential_store_id } = this.modelFor(
'scopes.scope.credential-stores.credential-store'
);
+ let name, description;
+ if (this.currentModel?.isNew) {
+ ({ name, description } = this.currentModel);
+ this.currentModel.rollbackAttributes();
+ }
return this.store.createRecord('credential', {
type,
credential_store_id,
+ name,
+ description,
});
}
+
/**
* Update type of credential
* @param {string} type
diff --git a/ui/admin/tests/acceptance/credential-store/credentials/create-test.js b/ui/admin/tests/acceptance/credential-store/credentials/create-test.js
index 5b7fe2d7d..298ee7c1e 100644
--- a/ui/admin/tests/acceptance/credential-store/credentials/create-test.js
+++ b/ui/admin/tests/acceptance/credential-store/credentials/create-test.js
@@ -12,12 +12,15 @@ module(
setupMirage(hooks);
let getCredentialsCount;
+ let getUsernamePasswordCredentialCount;
+ let getUsernameKeyPairCredentialCount;
const instances = {
scopes: {
org: null,
project: null,
},
+ staticCredentialStore: null,
};
const urls = {
@@ -52,19 +55,50 @@ module(
getCredentialsCount = () => {
return this.server.schema.credentials.all().models.length;
};
+ getUsernamePasswordCredentialCount = () => {
+ return this.server.schema.credentials.where({
+ type: 'username_password',
+ }).length;
+ };
+ getUsernameKeyPairCredentialCount = () => {
+ return this.server.schema.credentials.where({ type: 'ssh_private_key' })
+ .length;
+ };
authenticateSession({});
});
- test('Users can create a new credential', async function (assert) {
- assert.expect(1);
+ test('Users can create a new username & password credential', async function (assert) {
+ assert.expect(2);
const credentialsCount = getCredentialsCount();
+ const usernamePasswordCredentialCount =
+ getUsernamePasswordCredentialCount();
await visit(urls.newCredential);
await fillIn('[name="name"]', 'random string');
await click('[type="submit"]');
assert.strictEqual(getCredentialsCount(), credentialsCount + 1);
+ assert.strictEqual(
+ getUsernamePasswordCredentialCount(),
+ usernamePasswordCredentialCount + 1
+ );
});
- test('Users can cancel create new credential', async function (assert) {
+ test('Users can create a new username & key pair credential', async function (assert) {
+ assert.expect(2);
+ const credentialsCount = getCredentialsCount();
+ const usernameKeyPairCredentialCount =
+ getUsernameKeyPairCredentialCount();
+ await visit(urls.newCredential);
+ await fillIn('[name="name"]', 'random string');
+ await click('[value="ssh_private_key"]');
+ await click('[type="submit"]');
+ assert.strictEqual(getCredentialsCount(), credentialsCount + 1);
+ assert.strictEqual(
+ getUsernameKeyPairCredentialCount(),
+ usernameKeyPairCredentialCount + 1
+ );
+ });
+
+ test('Users can cancel create new username & password credential', async function (assert) {
assert.expect(2);
const credentialsCount = getCredentialsCount();
await visit(urls.newCredential);
@@ -74,6 +108,17 @@ module(
assert.strictEqual(getCredentialsCount(), credentialsCount);
});
+ test('Users can cancel create new username & key pair credential', async function (assert) {
+ assert.expect(2);
+ const credentialsCount = getCredentialsCount();
+ await visit(urls.newCredential);
+ await fillIn('[name="name"]', 'random string');
+ await click('[value="ssh_private_key"]');
+ await click('.rose-form-actions [type="button"]');
+ assert.strictEqual(currentURL(), urls.credentials);
+ assert.strictEqual(getCredentialsCount(), credentialsCount);
+ });
+
test('Users cannot navigate to new credential route without proper authorization', async function (assert) {
assert.expect(2);
instances.staticCredentialStore.authorized_collection_actions.credentials =
@@ -87,7 +132,7 @@ module(
assert.notOk(find(`[href="${urls.newCredential}"]`));
});
- test('saving a new credential with invalid fields displays error messages', async function (assert) {
+ test('saving a new username & password credential with invalid fields displays error messages', async function (assert) {
assert.expect(2);
this.server.post('/credentials', () => {
return new Response(
@@ -120,5 +165,40 @@ module(
'Field required for creating a username-password credential.'
);
});
+
+ test('saving a new username & key pair credential with invalid fields displays error messages', async function (assert) {
+ assert.expect(2);
+ this.server.post('/credentials', () => {
+ return new Response(
+ 400,
+ {},
+ {
+ status: 400,
+ code: 'invalid_argument',
+ message: 'Error in provided request.',
+ details: {
+ request_fields: [
+ {
+ name: 'attributes.private_key',
+ description:
+ 'Field required for creating a username-key-pair credential.',
+ },
+ ],
+ },
+ }
+ );
+ });
+ await visit(urls.newCredential);
+ await click('[value="ssh_private_key"]');
+ await click('[type="submit"]');
+ assert.ok(
+ find('[role="alert"]').textContent.trim(),
+ 'Error in provided request.'
+ );
+ assert.ok(
+ find('.rose-form-error-message').textContent.trim(),
+ 'Field required for creating a username-key-pair credential.'
+ );
+ });
}
);