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.' + ); + }); } );