diff --git a/e2e-tests/admin/pages/credential-stores.js b/e2e-tests/admin/pages/credential-stores.js
index 9e56239a4..f11c88a6f 100644
--- a/e2e-tests/admin/pages/credential-stores.js
+++ b/e2e-tests/admin/pages/credential-stores.js
@@ -147,7 +147,7 @@ export class CredentialStoresPage extends BaseResourcePage {
await this.page.getByRole('link', { name: 'New Credential' }).click();
}
- await this.page.getByLabel('Name (Optional)').fill(credentialName);
+ await this.page.getByLabel('Name', { exact: true }).fill(credentialName);
await this.page.getByLabel('Description').fill('This is an automated test');
await this.page.getByRole('combobox', { name: 'Type' }).click();
await this.page
@@ -207,7 +207,7 @@ export class CredentialStoresPage extends BaseResourcePage {
}
const credentialName = 'Credential ' + nanoid();
- await this.page.getByLabel('Name (Optional)').fill(credentialName);
+ await this.page.getByLabel('Name', { exact: true }).fill(credentialName);
await this.page.getByLabel('Description').fill('This is an automated test');
await this.page.getByRole('combobox', { name: 'Type' }).click();
await this.page
@@ -271,7 +271,7 @@ export class CredentialStoresPage extends BaseResourcePage {
}
const credentialName = 'Credential ' + nanoid();
- await this.page.getByLabel('Name (Optional)').fill(credentialName);
+ await this.page.getByLabel('Name', { exact: true }).fill(credentialName);
await this.page.getByLabel('Description').fill('This is an automated test');
await this.page.getByRole('combobox', { name: 'Type' }).click();
await this.page
diff --git a/e2e-tests/admin/tests/credential-store-static.spec.js b/e2e-tests/admin/tests/credential-store-static.spec.js
index 7bbc251a5..0a8e78fce 100644
--- a/e2e-tests/admin/tests/credential-store-static.spec.js
+++ b/e2e-tests/admin/tests/credential-store-static.spec.js
@@ -191,7 +191,7 @@ test(
.getByRole('link', { name: 'Credentials', exact: true })
.click();
await page.getByRole('link', { name: 'New', exact: true }).click();
- await page.getByLabel('Name (Optional)').fill(credentialName);
+ await page.getByLabel('Name', { exact: true }).fill(credentialName);
await page.getByLabel('Description').fill('This is an automated test');
await page.getByRole('combobox', { name: 'Type' }).click();
await page.getByRole('option', { name: 'JSON' }).click();
diff --git a/ui/admin/app/components/form/credential/json/index.hbs b/ui/admin/app/components/form/credential/json/index.hbs
index bc97a8ab6..90f11f567 100644
--- a/ui/admin/app/components/form/credential/json/index.hbs
+++ b/ui/admin/app/components/form/credential/json/index.hbs
@@ -15,7 +15,6 @@
name='name'
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
- @isOptional={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
@@ -37,7 +36,6 @@
name='description'
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
- @isOptional={{true}}
disabled={{form.disabled}}
as |F|
>
diff --git a/ui/admin/app/components/form/credential/password/index.hbs b/ui/admin/app/components/form/credential/password/index.hbs
index e76257cef..205ffa68a 100644
--- a/ui/admin/app/components/form/credential/password/index.hbs
+++ b/ui/admin/app/components/form/credential/password/index.hbs
@@ -1,4 +1,108 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
-}}
\ No newline at end of file
+}}
+
+
+
+
+ {{t 'form.name.label'}}
+ {{t 'form.name.help'}}
+ {{#if @model.errors.name}}
+
+ {{#each @model.errors.name as |error|}}
+
+ {{error.message}}
+
+ {{/each}}
+
+ {{/if}}
+
+
+
+ {{t 'form.description.label'}}
+ {{t 'form.description.help'}}
+ {{#if @model.errors.description}}
+
+ {{#each @model.errors.description as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+ {{#if @model.isNew}}
+
+ {{else}}
+
+ {{t 'form.type.label'}}
+ {{t (concat 'resources.credential.help.' @model.type)}}
+
+
+ {{/if}}
+
+ {{#if (or @model.isNew form.isEditable)}}
+
+ {{t 'resources.credential.form.password.label'}}
+ {{t
+ 'resources.credential.form.password.help'
+ }}
+ {{#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/components/form/credential/select/index.hbs b/ui/admin/app/components/form/credential/select/index.hbs
index b04ecad38..76e536a5e 100644
--- a/ui/admin/app/components/form/credential/select/index.hbs
+++ b/ui/admin/app/components/form/credential/select/index.hbs
@@ -5,6 +5,7 @@
diff --git a/ui/admin/app/components/form/credential/username_password/index.hbs b/ui/admin/app/components/form/credential/username_password/index.hbs
index ef1ff305d..42812771e 100644
--- a/ui/admin/app/components/form/credential/username_password/index.hbs
+++ b/ui/admin/app/components/form/credential/username_password/index.hbs
@@ -15,7 +15,6 @@
name='name'
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
- @isOptional={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
@@ -37,7 +36,6 @@
name='description'
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
- @isOptional={{true}}
disabled={{form.disabled}}
as |F|
>
diff --git a/ui/admin/app/components/form/credential/username_password_domain/index.hbs b/ui/admin/app/components/form/credential/username_password_domain/index.hbs
index 7e53a17c6..9089e3ad7 100644
--- a/ui/admin/app/components/form/credential/username_password_domain/index.hbs
+++ b/ui/admin/app/components/form/credential/username_password_domain/index.hbs
@@ -15,7 +15,6 @@
name='name'
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
- @isOptional={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
@@ -37,7 +36,6 @@
name='description'
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
- @isOptional={{true}}
disabled={{form.disabled}}
as |F|
>
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 0c3c9e0c8..3fce76109 100644
--- a/ui/admin/tests/acceptance/credential-store/credentials/create-test.js
+++ b/ui/admin/tests/acceptance/credential-store/credentials/create-test.js
@@ -11,6 +11,13 @@ import { Response } from 'miragejs';
import * as selectors from './selectors';
import * as commonSelectors from 'admin/tests/helpers/selectors';
import { setRunOptions } from 'ember-a11y-testing/test-support';
+import {
+ TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
+ TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
+ TYPE_CREDENTIAL_USERNAME_PASSWORD,
+ TYPE_CREDENTIAL_JSON,
+ TYPE_CREDENTIAL_PASSWORD,
+} from 'api/models/credential';
module(
'Acceptance | credential-stores | credentials | create',
@@ -23,6 +30,7 @@ module(
let getUsernameKeyPairCredentialCount;
let getUsernamePasswordDomainCredentialCount;
let getJsonCredentialCount;
+ let getPasswordCredentialCount;
let featuresService;
const instances = {
@@ -68,19 +76,27 @@ module(
};
getUsernamePasswordCredentialCount = () => {
return this.server.schema.credentials.where({
- type: 'username_password',
+ type: TYPE_CREDENTIAL_USERNAME_PASSWORD,
}).length;
};
getUsernameKeyPairCredentialCount = () => {
- return this.server.schema.credentials.where({ type: 'ssh_private_key' })
- .length;
+ return this.server.schema.credentials.where({
+ type: TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
+ }).length;
};
getJsonCredentialCount = () => {
- return this.server.schema.credentials.where({ type: 'json' }).length;
+ return this.server.schema.credentials.where({
+ type: TYPE_CREDENTIAL_JSON,
+ }).length;
};
getUsernamePasswordDomainCredentialCount = () => {
return this.server.schema.credentials.where({
- type: 'username_password_domain',
+ type: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
+ }).length;
+ };
+ getPasswordCredentialCount = () => {
+ return this.server.schema.credentials.where({
+ type: TYPE_CREDENTIAL_PASSWORD,
}).length;
};
});
@@ -253,6 +269,36 @@ module(
assert.strictEqual(getJsonCredentialCount(), jsonCredentialCount + 1);
});
+ test('users can create a new password credential', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ const credentialsCount = getCredentialsCount();
+ const passwordCredentialCount = getPasswordCredentialCount();
+ await visit(urls.credentials);
+
+ await click(commonSelectors.HREF(urls.newCredential));
+ await fillIn(
+ commonSelectors.FIELD_NAME,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+ await click(selectors.TYPE_SELECT);
+ await click(selectors.FIELD_TYPE_PASSWORD);
+ await click(commonSelectors.SAVE_BTN);
+
+ assert.strictEqual(getCredentialsCount(), credentialsCount + 1);
+ assert.strictEqual(
+ getPasswordCredentialCount(),
+ passwordCredentialCount + 1,
+ );
+ });
+
test('users can cancel create new username & password credential', async function (assert) {
setRunOptions({
rules: {
@@ -391,6 +437,32 @@ module(
assert.dom(selectors.EDITOR).includesText('{}');
});
+ test('users can cancel creation of new password credential', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ const credentialsCount = getCredentialsCount();
+ await visit(urls.credentials);
+
+ await click(commonSelectors.HREF(urls.newCredential));
+ await fillIn(
+ commonSelectors.FIELD_NAME,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+ await click(selectors.TYPE_SELECT);
+ await click(selectors.FIELD_TYPE_PASSWORD);
+ await click(commonSelectors.CANCEL_BTN);
+
+ assert.strictEqual(currentURL(), urls.credentials);
+ assert.strictEqual(getCredentialsCount(), credentialsCount);
+ });
+
test('users cannot navigate to new credential route without proper authorization', async function (assert) {
setRunOptions({
rules: {
@@ -598,6 +670,49 @@ module(
assert.dom(commonSelectors.ALERT_TOAST_BODY).hasText(errorMessage);
});
+ test('saving a new password credential with invalid fields displays error messages', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ const errorMessage = 'Error in provided request.';
+ const errorDescription =
+ 'Field required for creating a password credential.';
+ await visit(urls.credentials);
+ this.server.post('/credentials', () => {
+ return new Response(
+ 400,
+ {},
+ {
+ status: 400,
+ code: 'invalid_argument',
+ message: errorMessage,
+ details: {
+ request_fields: [
+ {
+ name: 'attributes.password',
+ description: errorDescription,
+ },
+ ],
+ },
+ },
+ );
+ });
+
+ await click(commonSelectors.HREF(urls.newCredential));
+ await click(selectors.TYPE_SELECT);
+ await click(selectors.FIELD_TYPE_PASSWORD);
+ await click(commonSelectors.SAVE_BTN);
+
+ assert.dom(commonSelectors.ALERT_TOAST_BODY).hasText(errorMessage);
+ assert.dom(selectors.FIELD_PASSWORD_ERROR).hasText(errorDescription);
+ });
+
test('users cannot create a new credential without proper authorization', async function (assert) {
setRunOptions({
rules: {
diff --git a/ui/admin/tests/acceptance/credential-store/credentials/delete-test.js b/ui/admin/tests/acceptance/credential-store/credentials/delete-test.js
index edb7369a4..cb500edfe 100644
--- a/ui/admin/tests/acceptance/credential-store/credentials/delete-test.js
+++ b/ui/admin/tests/acceptance/credential-store/credentials/delete-test.js
@@ -15,6 +15,8 @@ import {
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
TYPE_CREDENTIAL_USERNAME_PASSWORD,
+ TYPE_CREDENTIAL_JSON,
+ TYPE_CREDENTIAL_PASSWORD,
} from 'api/models/credential';
module(
@@ -27,6 +29,7 @@ module(
let getUsernameKeyPairCredentialCount;
let getJSONCredentialCount;
let getUsernamePasswordDomainCredentialCount;
+ let getPasswordCredentialCount;
const mockResponseMessage = 'Oops.';
const mockResponse = () => {
@@ -57,6 +60,7 @@ module(
usernameKeyPairCredential: null,
jsonCredential: null,
usernamePasswordDomainCredential: null,
+ passwordCredential: null,
};
hooks.beforeEach(async function () {
@@ -94,7 +98,12 @@ module(
instances.jsonCredential = this.server.create('credential', {
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
- type: 'json',
+ type: TYPE_CREDENTIAL_JSON,
+ });
+ instances.passwordCredential = this.server.create('credential', {
+ scope: instances.scopes.project,
+ credentialStore: instances.staticCredentialStore,
+ type: TYPE_CREDENTIAL_PASSWORD,
});
// Generate route URLs for resources
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
@@ -105,6 +114,7 @@ module(
urls.usernameKeyPairCredential = `${urls.credentials}/${instances.usernameKeyPairCredential.id}`;
urls.usernamePasswordDomainCredential = `${urls.credentials}/${instances.usernamePasswordDomainCredential.id}`;
urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`;
+ urls.passwordCredential = `${urls.credentials}/${instances.passwordCredential.id}`;
// Generate resource counter
getUsernamePasswordCredentialCount = () => {
return this.server.schema.credentials.where({
@@ -124,6 +134,11 @@ module(
type: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
}).length;
};
+ getPasswordCredentialCount = () => {
+ return this.server.schema.credentials.where({
+ type: TYPE_CREDENTIAL_PASSWORD,
+ }).length;
+ };
});
test('can delete username & password credential', async function (assert) {
@@ -219,6 +234,29 @@ module(
);
});
+ test('can delete password credential', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ const passwordCredentialCount = getPasswordCredentialCount();
+ await visit(urls.passwordCredential);
+
+ await click(selectors.MANAGE_DROPDOWN);
+ await click(selectors.MANAGE_DROPDOWN_DELETE);
+
+ assert.strictEqual(currentURL(), urls.credentials);
+ assert.strictEqual(
+ getPasswordCredentialCount(),
+ passwordCredentialCount - 1,
+ );
+ });
+
test('cannot delete a username & password credential without proper authorization', async function (assert) {
const usernamePasswordCredentialCount =
getUsernamePasswordCredentialCount();
@@ -283,6 +321,30 @@ module(
);
});
+ test('cannot delete a password credential without proper authorization', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ const passwordCredentialCount = getPasswordCredentialCount();
+ instances.passwordCredential.authorized_actions =
+ instances.passwordCredential.authorized_actions.filter(
+ (item) => item !== 'delete',
+ );
+ await visit(urls.credentials);
+
+ await click(commonSelectors.HREF(urls.passwordCredential));
+
+ assert.strictEqual(currentURL(), urls.passwordCredential);
+ assert.dom(selectors.MANAGE_DROPDOWN).doesNotExist();
+ assert.strictEqual(getPasswordCredentialCount(), passwordCredentialCount);
+ });
+
test('can accept delete username & password credential via dialog', async function (assert) {
setRunOptions({
rules: {
@@ -359,6 +421,33 @@ module(
assert.strictEqual(getJSONCredentialCount(), jsonCredentialCount - 1);
});
+ test('can accept delete password credential via dialog', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+ const passwordCredentialCount = getPasswordCredentialCount();
+ await visit(urls.credentials);
+
+ await click(commonSelectors.HREF(urls.passwordCredential));
+ await click(selectors.MANAGE_DROPDOWN);
+ await click(selectors.MANAGE_DROPDOWN_DELETE);
+ await click(commonSelectors.MODAL_WARNING_CONFIRM_BTN);
+
+ assert.strictEqual(currentURL(), urls.credentials);
+ assert.strictEqual(
+ getPasswordCredentialCount(),
+ passwordCredentialCount - 1,
+ );
+ });
+
test('can cancel delete username & password credential via dialog', async function (assert) {
setRunOptions({
rules: {
@@ -463,6 +552,30 @@ module(
assert.strictEqual(getJSONCredentialCount(), jsonCredentialCount);
});
+ test('can cancel delete password credential via dialog', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+ const passwordCredentialCount = getPasswordCredentialCount();
+ await visit(urls.credentials);
+
+ await click(commonSelectors.HREF(urls.passwordCredential));
+ await click(selectors.MANAGE_DROPDOWN);
+ await click(selectors.MANAGE_DROPDOWN_DELETE);
+ await click(commonSelectors.MODAL_WARNING_CANCEL_BTN);
+
+ assert.strictEqual(currentURL(), urls.passwordCredential);
+ assert.strictEqual(getPasswordCredentialCount(), passwordCredentialCount);
+ });
+
test('deleting a username & password credential which errors displays error message', async function (assert) {
setRunOptions({
rules: {
@@ -548,5 +661,24 @@ module(
assert.dom(commonSelectors.ALERT_TOAST_BODY).hasText('Oops.');
});
+
+ test('deleting a password credential which errors displays error message', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ this.server.del('/credentials/:id', mockResponse);
+ await visit(urls.passwordCredential);
+
+ await click(selectors.MANAGE_DROPDOWN);
+ await click(selectors.MANAGE_DROPDOWN_DELETE);
+
+ assert.dom(commonSelectors.ALERT_TOAST_BODY).hasText(mockResponseMessage);
+ });
},
);
diff --git a/ui/admin/tests/acceptance/credential-store/credentials/read-test.js b/ui/admin/tests/acceptance/credential-store/credentials/read-test.js
index 99cb31c57..ac0746ee3 100644
--- a/ui/admin/tests/acceptance/credential-store/credentials/read-test.js
+++ b/ui/admin/tests/acceptance/credential-store/credentials/read-test.js
@@ -9,6 +9,13 @@ import { setupApplicationTest } from 'admin/tests/helpers';
import { setupSqlite } from 'api/test-support/helpers/sqlite';
import * as commonSelectors from 'admin/tests/helpers/selectors';
import { setRunOptions } from 'ember-a11y-testing/test-support';
+import {
+ TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
+ TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
+ TYPE_CREDENTIAL_USERNAME_PASSWORD,
+ TYPE_CREDENTIAL_JSON,
+ TYPE_CREDENTIAL_PASSWORD,
+} from 'api/models/credential';
module('Acceptance | credential-stores | credentials | read', function (hooks) {
setupApplicationTest(hooks);
@@ -24,6 +31,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
usernameKeyPairCredential: null,
jsonCredential: null,
usernamePasswordDomainCredential: null,
+ passwordCredential: null,
};
const urls = {
@@ -36,6 +44,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
jsonCredential: null,
unknownCredential: null,
usernamePasswordDomainCredential: null,
+ passwordCredential: null,
};
hooks.beforeEach(async function () {
@@ -55,26 +64,31 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
instances.usernamePasswordCredential = this.server.create('credential', {
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
- type: 'username_password',
+ type: TYPE_CREDENTIAL_USERNAME_PASSWORD,
});
instances.usernameKeyPairCredential = this.server.create('credential', {
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
- type: 'ssh_private_key',
+ type: TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
});
instances.jsonCredential = this.server.create('credential', {
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
- type: 'json',
+ type: TYPE_CREDENTIAL_JSON,
});
instances.usernamePasswordDomainCredential = this.server.create(
'credential',
{
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
- type: 'username_password_domain',
+ type: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
},
);
+ instances.passwordCredential = this.server.create('credential', {
+ scope: instances.scopes.project,
+ credentialStore: instances.staticCredentialStore,
+ type: TYPE_CREDENTIAL_PASSWORD,
+ });
// Generate route URLs for resources
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
@@ -85,6 +99,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
urls.usernameKeyPairCredential = `${urls.credentials}/${instances.usernameKeyPairCredential.id}`;
urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`;
urls.usernamePasswordDomainCredential = `${urls.credentials}/${instances.usernamePasswordDomainCredential.id}`;
+ urls.passwordCredential = `${urls.credentials}/${instances.passwordCredential.id}`;
urls.unknownCredential = `${urls.credentials}/foo`;
});
@@ -169,6 +184,26 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
assert.strictEqual(currentURL(), urls.usernamePasswordDomainCredential);
});
+ test('visiting password credential', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ await visit(urls.staticCredentialStore);
+ await click(commonSelectors.HREF(urls.credentials));
+
+ assert.strictEqual(currentURL(), urls.credentials);
+
+ await click(commonSelectors.HREF(urls.passwordCredential));
+
+ assert.strictEqual(currentURL(), urls.passwordCredential);
+ });
+
test('cannot navigate to a username & password credential form without proper authorization', async function (assert) {
setRunOptions({
rules: {
@@ -266,6 +301,30 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
.doesNotExist();
});
+ test('cannot navigate to a password credential form without proper authorization', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ instances.passwordCredential.authorized_actions =
+ instances.passwordCredential.authorized_actions.filter(
+ (item) => item != 'read',
+ );
+ await visit(urls.credentials);
+
+ assert
+ .dom(commonSelectors.TABLE_RESOURCE_LINK(urls.usernamePasswordCredential))
+ .isVisible();
+ assert
+ .dom(commonSelectors.TABLE_RESOURCE_LINK(urls.passwordCredential))
+ .doesNotExist();
+ });
+
test('visiting an unknown credential displays 404 message', async function (assert) {
setRunOptions({
rules: {
diff --git a/ui/admin/tests/acceptance/credential-store/credentials/selectors.js b/ui/admin/tests/acceptance/credential-store/credentials/selectors.js
index 3804a31aa..591f22de3 100644
--- a/ui/admin/tests/acceptance/credential-store/credentials/selectors.js
+++ b/ui/admin/tests/acceptance/credential-store/credentials/selectors.js
@@ -6,10 +6,11 @@
export const FIELD_TYPE_SSH = '[data-option-index="1"]';
export const FIELD_TYPE_USERNAME_PASSWORD = '[data-option-index="0"]';
export const FIELD_TYPE_USERNAME_PASSWORD_DOMAIN = '[data-option-index="2"]';
+export const FIELD_TYPE_JSON = '[data-option-index="3"]';
+export const FIELD_TYPE_PASSWORD = '[data-option-index="4"]';
export const FIELD_DOMAIN = '[name=domain]';
export const FIELD_PASSWORD = '[name=password]';
export const FIELD_USERNAME = '[name=username]';
-export const FIELD_TYPE_JSON = '[data-option-index="3"]';
export const FIELD_DOMAIN_VALUE = 'g.com';
export const FIELD_USERNAME_VALUE = 'username123';
export const FIELD_PASSWORD_VALUE = 'password123';
diff --git a/ui/admin/tests/acceptance/credential-store/credentials/update-test.js b/ui/admin/tests/acceptance/credential-store/credentials/update-test.js
index 5fe15719a..c869df66b 100644
--- a/ui/admin/tests/acceptance/credential-store/credentials/update-test.js
+++ b/ui/admin/tests/acceptance/credential-store/credentials/update-test.js
@@ -18,6 +18,13 @@ import { Response } from 'miragejs';
import * as selectors from './selectors';
import * as commonSelectors from 'admin/tests/helpers/selectors';
import { setRunOptions } from 'ember-a11y-testing/test-support';
+import {
+ TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
+ TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
+ TYPE_CREDENTIAL_USERNAME_PASSWORD,
+ TYPE_CREDENTIAL_JSON,
+ TYPE_CREDENTIAL_PASSWORD,
+} from 'api/models/credential';
module(
'Acceptance | credential-stores | credentials | update',
@@ -35,6 +42,7 @@ module(
usernameKeyPairCredential: null,
jsonCredential: null,
usernamePasswordDomainCredential: null,
+ passwordCredential: null,
};
const urls = {
@@ -46,6 +54,7 @@ module(
usernameKeyPairCredential: null,
jsonCredential: null,
usernamePasswordDomainCredential: null,
+ passwordCredential: null,
};
const mockResponseMessage = 'Error in provided request.';
@@ -87,26 +96,31 @@ module(
instances.usernamePasswordCredential = this.server.create('credential', {
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
- type: 'username_password',
+ type: TYPE_CREDENTIAL_USERNAME_PASSWORD,
});
instances.usernameKeyPairCredential = this.server.create('credential', {
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
- type: 'ssh_private_key',
+ type: TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
});
instances.jsonCredential = this.server.create('credential', {
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
- type: 'json',
+ type: TYPE_CREDENTIAL_JSON,
});
instances.usernamePasswordDomainCredential = this.server.create(
'credential',
{
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
- type: 'username_password_domain',
+ type: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
},
);
+ instances.passwordCredential = this.server.create('credential', {
+ scope: instances.scopes.project,
+ credentialStore: instances.staticCredentialStore,
+ type: TYPE_CREDENTIAL_PASSWORD,
+ });
// Generate route URLs for resources
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
@@ -117,6 +131,7 @@ module(
urls.usernameKeyPairCredential = `${urls.credentials}/${instances.usernameKeyPairCredential.id}`;
urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`;
urls.usernamePasswordDomainCredential = `${urls.credentials}/${instances.usernamePasswordDomainCredential.id}`;
+ urls.passwordCredential = `${urls.credentials}/${instances.passwordCredential.id}`;
});
test('can save changes to existing username & password credential', async function (assert) {
@@ -231,6 +246,37 @@ module(
assert.strictEqual(credential.attributes.domain, 'g.com');
});
+ test('can save changes to existing password credential', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ assert.notEqual(
+ instances.passwordCredential.name,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+ await visit(urls.passwordCredential);
+
+ await click(commonSelectors.EDIT_BTN);
+ await fillIn(
+ commonSelectors.FIELD_NAME,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+ await click(commonSelectors.SAVE_BTN);
+
+ assert.strictEqual(currentURL(), urls.passwordCredential);
+ assert.strictEqual(
+ this.server.schema.credentials.where({ type: TYPE_CREDENTIAL_PASSWORD })
+ .models[0].name,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+ });
+
test('cannot make changes to an existing username & password credential without proper authorization', async function (assert) {
instances.usernamePasswordCredential.authorized_actions =
instances.usernamePasswordCredential.authorized_actions.filter(
@@ -262,6 +308,27 @@ module(
assert.dom(commonSelectors.EDIT_BTN).doesNotExist();
});
+ test('cannot make changes to an existing password credential without proper authorization', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ instances.passwordCredential.authorized_actions =
+ instances.passwordCredential.authorized_actions.filter(
+ (item) => item !== 'update',
+ );
+ await visit(urls.credentials);
+
+ await click(commonSelectors.HREF(urls.passwordCredential));
+
+ assert.dom(commonSelectors.EDIT_BTN).doesNotExist();
+ });
+
test('can cancel changes to existing username & password credential', async function (assert) {
setRunOptions({
rules: {
@@ -336,6 +403,33 @@ module(
.hasValue(instances.jsonCredential.name);
});
+ test('can cancel changes to existing password credential', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ await visit(urls.passwordCredential);
+ await click(commonSelectors.EDIT_BTN, 'Activate edit mode');
+ await fillIn(
+ commonSelectors.FIELD_NAME,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+ await click(commonSelectors.CANCEL_BTN);
+
+ assert.notEqual(
+ instances.passwordCredential.name,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+ assert
+ .dom(commonSelectors.FIELD_NAME)
+ .hasValue(instances.passwordCredential.name);
+ });
+
test('saving an existing username & password credential with invalid fields displays error message', async function (assert) {
setRunOptions({
rules: {
@@ -405,6 +499,32 @@ module(
.hasText(mockResponseDescription);
});
+ test('saving an existing password credential with invalid fields displays error message', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ this.server.patch('/credentials/:id', mockResponse);
+ await visit(urls.passwordCredential);
+
+ await click(commonSelectors.EDIT_BTN, 'Activate edit mode');
+ await fillIn(
+ commonSelectors.FIELD_NAME,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+ await click(commonSelectors.SAVE_BTN);
+
+ assert.dom(commonSelectors.ALERT_TOAST_BODY).hasText(mockResponseMessage);
+ assert
+ .dom(commonSelectors.FIELD_NAME_ERROR)
+ .hasText(mockResponseDescription);
+ });
+
test('can discard unsaved username & password credential changes via dialog', async function (assert) {
setRunOptions({
rules: {
@@ -530,6 +650,48 @@ module(
}
});
+ test('can discard unsaved password credential changes via dialog', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ assert.expect(5);
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+ assert.notEqual(
+ instances.passwordCredential.name,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+ await visit(urls.passwordCredential);
+
+ await click(commonSelectors.EDIT_BTN, 'Activate edit mode');
+ await fillIn(
+ commonSelectors.FIELD_NAME,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+ assert.strictEqual(currentURL(), urls.passwordCredential);
+ try {
+ await visit(urls.credentials);
+ } catch (e) {
+ assert.dom(commonSelectors.MODAL_WARNING).isVisible();
+
+ await click(commonSelectors.MODAL_WARNING_CONFIRM_BTN, 'Click Discard');
+
+ assert.strictEqual(currentURL(), urls.credentials);
+ assert.notEqual(
+ this.server.schema.credentials.where({
+ type: TYPE_CREDENTIAL_PASSWORD,
+ }).models[0].name,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+ }
+ });
+
test('can cancel discard unsaved username & password credential changes via dialog', async function (assert) {
setRunOptions({
rules: {
@@ -679,6 +841,54 @@ module(
}
});
+ test('can cancel discard unsaved password credential changes via dialog', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ assert.expect(6);
+ const confirmService = this.owner.lookup('service:confirm');
+ confirmService.enabled = true;
+ assert.notEqual(
+ instances.passwordCredential.name,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+ await visit(urls.passwordCredential);
+
+ await click(commonSelectors.EDIT_BTN);
+ const credentialName = find(commonSelectors.FIELD_NAME).value;
+ await fillIn(
+ commonSelectors.FIELD_NAME,
+ commonSelectors.FIELD_NAME_VALUE,
+ );
+
+ assert.strictEqual(currentURL(), urls.passwordCredential);
+
+ try {
+ await visit(urls.credentials);
+ } catch (e) {
+ assert.dom(commonSelectors.MODAL_WARNING).isVisible();
+
+ await click(commonSelectors.MODAL_WARNING_CANCEL_BTN, 'Click Cancel');
+
+ assert.strictEqual(currentURL(), urls.passwordCredential);
+ assert
+ .dom(commonSelectors.FIELD_NAME)
+ .hasValue(commonSelectors.FIELD_NAME_VALUE);
+ assert.strictEqual(
+ this.server.schema.credentials.where({
+ type: TYPE_CREDENTIAL_PASSWORD,
+ }).models[0].name,
+ credentialName,
+ );
+ }
+ });
+
test('password field renders in edit mode only for a username & password credential', async function (assert) {
setRunOptions({
rules: {
@@ -699,6 +909,26 @@ module(
assert.dom(commonSelectors.FIELD_PASSWORD).isVisible();
});
+ test('password field renders in edit mode only for a password credential', async function (assert) {
+ setRunOptions({
+ rules: {
+ 'color-contrast': {
+ // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-11-06
+ enabled: false,
+ },
+ },
+ });
+
+ await visit(urls.passwordCredential);
+
+ assert.dom(commonSelectors.FIELD_PASSWORD).doesNotExist();
+
+ await click(commonSelectors.EDIT_BTN);
+
+ assert.strictEqual(currentURL(), urls.passwordCredential);
+ assert.dom(commonSelectors.FIELD_PASSWORD).isVisible();
+ });
+
test('private_key and private_key_passphrase fields render in edit mode only for a username & key pair credential', async function (assert) {
setRunOptions({
rules: {
diff --git a/ui/admin/tests/acceptance/targets/brokered-credential-sources-test.js b/ui/admin/tests/acceptance/targets/brokered-credential-sources-test.js
index 1d16a3bb7..22b9c36ff 100644
--- a/ui/admin/tests/acceptance/targets/brokered-credential-sources-test.js
+++ b/ui/admin/tests/acceptance/targets/brokered-credential-sources-test.js
@@ -16,6 +16,7 @@ import {
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_JSON,
+ TYPE_CREDENTIAL_PASSWORD,
} from 'api/models/credential';
import {
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
@@ -56,6 +57,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
credentialLibrary: null,
credential: null,
jsonCredential: null,
+ passwordCredential: null,
addBrokeredCredentialSourcesForTCPTarget: null,
brokeredCredentialSourcesForTCPTarget: null,
addBrokeredCredentialSourcesForRDPTarget: null,
@@ -80,7 +82,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
type: 'static',
scope: instances.scopes.project,
});
- instances.credentials = this.server.createList('credential', 4, {
+ instances.credentials = this.server.createList('credential', 5, {
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
});
@@ -132,6 +134,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
urls.credentialLibrary = `${urls.projectScope}/credential-stores/${instances.credentialLibrary.credentialStoreId}/credential-libraries/${instances.credentialLibrary.id}`;
urls.credential = `${urls.projectScope}/credential-stores/${instances.credential.credentialStoreId}/credentials/${instances.credential.id}`;
urls.jsonCredential = `${urls.projectScope}/credential-stores/${instances.credentials[3].credentialStoreId}/credentials/${instances.credentials[3].id}`;
+ urls.passwordCredential = `${urls.projectScope}/credential-stores/${instances.credentials[4].credentialStoreId}/credentials/${instances.credentials[4].id}`;
urls.addBrokeredCredentialSourcesForTCPTarget = `${urls.tcpTarget}/add-brokered-credential-sources`;
urls.addBrokeredCredentialSourcesForRDPTarget = `${urls.rdpTarget}/add-brokered-credential-sources`;
getCredentialLibraryCount = () =>
@@ -210,6 +213,18 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
link: 'jsonCredential',
expectedUrl: 'jsonCredential',
},
+ 'password credential type for TCP target': {
+ route: 'brokeredCredentialSourcesForTCPTarget',
+ targetName: 'tcpTarget',
+ link: 'passwordCredential',
+ expectedUrl: 'passwordCredential',
+ },
+ 'password credential type for RDP target': {
+ route: 'brokeredCredentialSourcesForRDPTarget',
+ targetName: 'rdpTarget',
+ link: 'passwordCredential',
+ expectedUrl: 'passwordCredential',
+ },
},
async function (assert, input) {
setRunOptions({
@@ -427,6 +442,11 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
action: commonSelectors.SAVE_BTN,
expectedCount: 1,
},
+ 'save password credential': {
+ credentialSources: [TYPE_CREDENTIAL_PASSWORD],
+ action: commonSelectors.SAVE_BTN,
+ expectedCount: 1,
+ },
'save credentials and credential-libraries': {
credentialSources: [
TYPE_CREDENTIAL_USERNAME_PASSWORD,
@@ -530,6 +550,11 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
action: commonSelectors.SAVE_BTN,
expectedCount: 1,
},
+ 'save password credential': {
+ credentialSources: [TYPE_CREDENTIAL_PASSWORD],
+ action: commonSelectors.SAVE_BTN,
+ expectedCount: 1,
+ },
'save credentials and credential-libraries': {
credentialSources: [
TYPE_CREDENTIAL_USERNAME_PASSWORD,
@@ -572,7 +597,11 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
action: commonSelectors.CANCEL_BTN,
expectedCount: 0,
},
-
+ 'cancel password credential': {
+ credentialSources: [TYPE_CREDENTIAL_PASSWORD],
+ action: commonSelectors.CANCEL_BTN,
+ expectedCount: 0,
+ },
'cancel credentials and credential-libraries': {
credentialSources: [
TYPE_CREDENTIAL_USERNAME_PASSWORD,