diff --git a/addons/api/addon/generated/models/credential-library.js b/addons/api/addon/generated/models/credential-library.js index 3d8ba617ee..9e8d9c0d1a 100644 --- a/addons/api/addon/generated/models/credential-library.js +++ b/addons/api/addon/generated/models/credential-library.js @@ -81,9 +81,10 @@ export default class GeneratedCredentialLibraryModel extends BaseModel { }) credential_type; - @attr('object-as-array', { + @attr('object', { for: 'vault-generic', description: 'It indicates the credential mapping overrides.', + emptyObjectIfMissing: true, }) credential_mapping_overrides; diff --git a/addons/api/addon/generated/models/credential.js b/addons/api/addon/generated/models/credential.js index 9e271a443a..acaace5bda 100644 --- a/addons/api/addon/generated/models/credential.js +++ b/addons/api/addon/generated/models/credential.js @@ -59,9 +59,9 @@ export default class GeneratedCredentialModel extends BaseModel { }) username; - // =attributes (username_password, username_password_domain) + // =attributes (username_password, username_password_domain, password) @attr('string', { - for: ['username_password', 'username_password_domain'], + for: ['username_password', 'username_password_domain', 'password'], isNestedAttribute: true, isSecret: true, description: 'The password for credential.', diff --git a/addons/api/addon/models/credential-library.js b/addons/api/addon/models/credential-library.js index 8bb68bd52c..b66491bbfd 100644 --- a/addons/api/addon/models/credential-library.js +++ b/addons/api/addon/models/credential-library.js @@ -8,6 +8,7 @@ import { TYPE_CREDENTIAL_SSH_PRIVATE_KEY, TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, TYPE_CREDENTIAL_USERNAME_PASSWORD, + TYPE_CREDENTIAL_PASSWORD, } from 'api/models/credential'; /** * Enum options for credential library. @@ -19,6 +20,7 @@ export const options = { TYPE_CREDENTIAL_SSH_PRIVATE_KEY, TYPE_CREDENTIAL_USERNAME_PASSWORD, TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, + TYPE_CREDENTIAL_PASSWORD, ], mapping_overrides: { username_password: ['username_attribute', 'password_attribute'], @@ -32,6 +34,7 @@ export const options = { 'password_attribute', 'domain_attribute', ], + password: ['password_attribute'], }, }; diff --git a/addons/api/addon/models/credential.js b/addons/api/addon/models/credential.js index 146dcbf75d..51cb74fa02 100644 --- a/addons/api/addon/models/credential.js +++ b/addons/api/addon/models/credential.js @@ -12,13 +12,16 @@ import GeneratedCredentialModel from '../generated/models/credential'; export const TYPE_CREDENTIAL_USERNAME_PASSWORD = 'username_password'; export const TYPE_CREDENTIAL_SSH_PRIVATE_KEY = 'ssh_private_key'; export const TYPE_CREDENTIAL_JSON = 'json'; -export const TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN = 'username_password_domain'; +export const TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN = + 'username_password_domain'; +export const TYPE_CREDENTIAL_PASSWORD = 'password'; export const TYPES_CREDENTIAL = Object.freeze([ TYPE_CREDENTIAL_USERNAME_PASSWORD, TYPE_CREDENTIAL_SSH_PRIVATE_KEY, TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, TYPE_CREDENTIAL_JSON, + TYPE_CREDENTIAL_PASSWORD, ]); export default class CredentialModel extends GeneratedCredentialModel { diff --git a/addons/api/addon/serializers/credential-library.js b/addons/api/addon/serializers/credential-library.js index 5e2186eb73..13e155c83c 100644 --- a/addons/api/addon/serializers/credential-library.js +++ b/addons/api/addon/serializers/credential-library.js @@ -68,7 +68,9 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer { */ handleCredentialMappingOverrides(serialized, isNew) { const { credential_type, credential_mapping_overrides } = serialized; - + if (Object.keys(credential_mapping_overrides).length === 0) { + serialized.credential_mapping_overrides = null; + } // API expects to send null to fields if it is undefined or deleted if (credential_mapping_overrides && !isNew) { serialized.credential_mapping_overrides = options.mapping_overrides[ diff --git a/addons/api/mirage/factories/credential-store.js b/addons/api/mirage/factories/credential-store.js index c6941f6885..756b45739a 100644 --- a/addons/api/mirage/factories/credential-store.js +++ b/addons/api/mirage/factories/credential-store.js @@ -65,7 +65,7 @@ export default factory.extend({ break; case 'static': default: - server.createList('credential', 3, { + server.createList('credential', 5, { scope, credentialStore, }); diff --git a/addons/api/mirage/factories/credential.js b/addons/api/mirage/factories/credential.js index c7349fb97e..cfd1000d82 100644 --- a/addons/api/mirage/factories/credential.js +++ b/addons/api/mirage/factories/credential.js @@ -12,6 +12,8 @@ import { TYPE_CREDENTIAL_SSH_PRIVATE_KEY, TYPE_CREDENTIAL_USERNAME_PASSWORD, TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, + TYPE_CREDENTIAL_JSON, + TYPE_CREDENTIAL_PASSWORD, } from 'api/models/credential'; const types = [...TYPES_CREDENTIAL]; @@ -20,14 +22,16 @@ export default factory.extend({ type: (i) => types[i % types.length], id() { switch (this.type) { - case 'ssh_private_key': + case TYPE_CREDENTIAL_SSH_PRIVATE_KEY: return generatedId('credspk_'); - case 'username_password': + case TYPE_CREDENTIAL_USERNAME_PASSWORD: return generatedId('credup_'); - case 'json': + case TYPE_CREDENTIAL_JSON: return generatedId('credjson_'); - case 'username_password_domain': + case TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN: return generatedId('credupd_'); + case TYPE_CREDENTIAL_PASSWORD: + return generatedId('credp_'); } }, authorized_actions: () => diff --git a/addons/api/mirage/factories/target.js b/addons/api/mirage/factories/target.js index c297671327..16331009c3 100644 --- a/addons/api/mirage/factories/target.js +++ b/addons/api/mirage/factories/target.js @@ -17,7 +17,9 @@ import { TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, TYPE_CREDENTIAL_USERNAME_PASSWORD, TYPE_CREDENTIAL_JSON, + TYPE_CREDENTIAL_PASSWORD, } from 'api/models/credential'; +import { TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP } from 'api/models/credential-library'; const randomBoolean = (chance = 0.5) => Math.random() < chance; const randomFilter = () => @@ -111,7 +113,9 @@ export default factory.extend({ server.schema.credentialLibraries, (cred) => cred.scopeId === scope.id && - cred.credential_type !== TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, + cred.credential_type !== TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN && + cred.credential_type !== TYPE_CREDENTIAL_PASSWORD && + cred.type !== TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP, ); const filteredCredentials = selectItems( server.schema.credentials, @@ -119,6 +123,7 @@ export default factory.extend({ cred.scopeId === scope.id && ![ TYPE_CREDENTIAL_JSON, + TYPE_CREDENTIAL_PASSWORD, TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, ].includes(cred.type), ); @@ -127,11 +132,12 @@ export default factory.extend({ const filteredCredentialLibrariesForRDP = selectItems( server.schema.credentialLibraries, (cred) => - cred.scopeId === scope.id && - [ - TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, - TYPE_CREDENTIAL_USERNAME_PASSWORD, - ].includes(cred.credential_type), + (cred.scopeId === scope.id && + [ + TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, + TYPE_CREDENTIAL_USERNAME_PASSWORD, + ].includes(cred.credential_type)) || + cred.type == TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP, ); const filteredCredentialsForRDP = selectItems( server.schema.credentials, diff --git a/addons/api/tests/unit/serializers/credential-library-test.js b/addons/api/tests/unit/serializers/credential-library-test.js index 817ecb42c7..cb90605d6c 100644 --- a/addons/api/tests/unit/serializers/credential-library-test.js +++ b/addons/api/tests/unit/serializers/credential-library-test.js @@ -25,9 +25,7 @@ module('Unit | Serializer | credential library', function (hooks) { http_method: 'GET', version: 1, credential_type: 'ssh_private_key', - credential_mapping_overrides: [ - { key: 'username_attribute', value: 'user' }, - ], + credential_mapping_overrides: { username_attribute: 'user' }, }); const snapshot = record._createSnapshot(); const serializedRecord = serializer.serialize(snapshot); @@ -64,9 +62,7 @@ module('Unit | Serializer | credential library', function (hooks) { http_method: 'GET', version: 1, credential_type: 'ssh_private_key', - credential_mapping_overrides: [ - { key: 'private_key_attribute', value: 'test' }, - ], + credential_mapping_overrides: { private_key_attribute: 'test' }, }, }, }); diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 8eab7e795e..9b91d20b30 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -913,11 +913,12 @@ credential-library: ssh_private_key: SSH Private Key username_password: Username & Password username_password_domain: Username, Password & Domain - username_attribute: Username - password_attribute: Password - private_key_passphrase_attribute: Private Key Passphrase - private_key_attribute: Private Key - domain_attribute: Domain + password: Password + username_attribute: username_attribute + password_attribute: password_attribute + private_key_passphrase_attribute: private_key_passphrase_attribute + private_key_attribute: private_key_attribute + domain_attribute: domain_attribute actions: create: New Credential Library delete: Delete Credential Library @@ -989,12 +990,14 @@ credential: ssh_private_key: Username & Key Pair username_password_domain: Username, Password & Domain json: JSON + password: Password unknown: Unknown help: username_password: Connect using the provided username and password. ssh_private_key: Connect using a username, public key, and private key. json: Connect using a JSON blob containing the credentials. - username_password_domain: Includes a domain field for Active Directory + username_password_domain: Includes a domain field for Active Directory. + password: Connect using only a provided password. form: username: label: Username diff --git a/addons/rose/addon/styles/hds/themes/dark-mode/index.scss b/addons/rose/addon/styles/hds/themes/dark-mode/index.scss index 9aa3916ff5..f42a549b4c 100644 --- a/addons/rose/addon/styles/hds/themes/dark-mode/index.scss +++ b/addons/rose/addon/styles/hds/themes/dark-mode/index.scss @@ -121,14 +121,27 @@ } } +@mixin dark-mode-ember-dropdown-override { + .ember-basic-dropdown-content { + background-color: var(--token-color-palette-neutral-0); + + /* stylelint-disable-next-line selector-class-pattern */ + &.ember-basic-dropdown-content--in-place { + box-shadow: none; + } + } +} + @media (prefers-color-scheme: dark) { .ember-application:not(.rose-theme-light) { @include dark-mode; @include dark-mode-modal-override; + @include dark-mode-ember-dropdown-override; } } .ember-application.rose-theme-dark { @include dark-mode; @include dark-mode-modal-override; + @include dark-mode-ember-dropdown-override; } diff --git a/e2e-tests/admin/pages/credential-stores.js b/e2e-tests/admin/pages/credential-stores.js index 79ef576672..716021ceba 100644 --- a/e2e-tests/admin/pages/credential-stores.js +++ b/e2e-tests/admin/pages/credential-stores.js @@ -147,11 +147,11 @@ 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 - .getByRole('group', { name: 'Type' }) - .getByLabel('Username & Key Pair') + .getByRole('option', { name: 'Username & Key Pair' }) .click(); await this.page .getByLabel('Username Required', { exact: true }) @@ -207,11 +207,11 @@ 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 - .getByRole('group', { name: 'Type' }) - .getByLabel('Username & Password') + .getByRole('option', { name: 'Username & Password' }) .click(); await this.page .getByLabel('Username Required', { exact: true }) @@ -271,11 +271,11 @@ 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 - .getByRole('group', { name: 'Type' }) - .getByLabel('Username, Password & Domain') + .getByRole('option', { name: 'Username, Password & Domain' }) .click(); await this.page .getByLabel('Username Required', { exact: true }) @@ -308,11 +308,9 @@ export class CredentialStoresPage extends BaseResourcePage { await this.page.getByRole('link', { name: 'Credential Libraries' }).click(); await this.page.getByRole('link', { name: 'New', exact: true }).click(); await this.page - .getByLabel('Name (Optional)', { exact: true }) + .getByLabel('Name', { exact: true }) .fill(credentialLibraryName); - await this.page - .getByLabel('Description (Optional)') - .fill('This is an automated test'); + await this.page.getByLabel('Description').fill('This is an automated test'); await this.page .getByRole('group', { name: 'Type' }) .getByLabel('Generic Secrets') @@ -345,11 +343,9 @@ export class CredentialStoresPage extends BaseResourcePage { await this.page.getByRole('link', { name: 'New', exact: true }).click(); await this.page - .getByLabel('Name (Optional)', { exact: true }) + .getByLabel('Name', { exact: true }) .fill(credentialLibraryName); - await this.page - .getByLabel('Description (Optional)') - .fill('This is an automated test'); + await this.page.getByLabel('Description').fill('This is an automated test'); await this.page .getByRole('group', { name: 'Type' }) .getByLabel('SSH Certificates') diff --git a/e2e-tests/admin/tests/credential-store-static.spec.js b/e2e-tests/admin/tests/credential-store-static.spec.js index 1d4ec5452e..0a8e78fce0 100644 --- a/e2e-tests/admin/tests/credential-store-static.spec.js +++ b/e2e-tests/admin/tests/credential-store-static.spec.js @@ -191,12 +191,10 @@ 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('group', { name: 'Type' }) - .getByLabel('JSON') - .click(); + await page.getByRole('combobox', { name: 'Type' }).click(); + await page.getByRole('option', { name: 'JSON' }).click(); await page.getByText('{}').click(); const testName = 'name-json'; const testPassword = 'password-json'; diff --git a/e2e-tests/admin/tests/credential-store-vault.spec.js b/e2e-tests/admin/tests/credential-store-vault.spec.js index fa148d474b..3f4c7f4131 100644 --- a/e2e-tests/admin/tests/credential-store-vault.spec.js +++ b/e2e-tests/admin/tests/credential-store-vault.spec.js @@ -99,11 +99,9 @@ test( await page.getByRole('link', { name: 'Credential Libraries' }).click(); await page.getByRole('link', { name: 'New', exact: true }).click(); await page - .getByLabel('Name (Optional)', { exact: true }) + .getByLabel('Name', { exact: true }) .fill(credentialLibraryName); - await page - .getByLabel('Description (Optional)') - .fill('This is an automated test'); + await page.getByLabel('Description').fill('This is an automated test'); await page .getByLabel('Vault Path') .fill(`${secretsPath}/data/${secretName}`); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7ae14d46f..0d4ee88825 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -545,7 +545,7 @@ importers: version: 7.27.1 '@hashicorp/design-system-components': specifier: ^4.24.1 - version: 4.24.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) + version: 4.24.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.8.0(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) '@hashicorp/design-system-tokens': specifier: ^2.3.0 version: 2.3.0 @@ -839,6 +839,9 @@ importers: ember-auto-import: specifier: ^2.10.0 version: 2.10.0(@glint/template@1.5.2)(webpack@5.99.8(esbuild@0.25.9)) + ember-basic-dropdown: + specifier: ^8.8.0 + version: 8.8.0(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) ember-cli: specifier: ~5.12.0 version: 5.12.0(handlebars@4.7.8)(underscore@1.13.7) @@ -893,6 +896,9 @@ importers: ember-page-title: specifier: ^8.2.3 version: 8.2.4(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) + ember-power-select: + specifier: ^8.11.0 + version: 8.12.0(@babel/core@7.27.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.8.0(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))))(ember-concurrency@4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) ember-qunit: specifier: ^8.1.0 version: 8.1.1(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9)))(qunit@2.24.1) @@ -4627,8 +4633,8 @@ packages: resolution: {integrity: sha512-bcBFDYVTFHyqyq8BNvsj6UO3pE6Uqou/cNmee0WaqBgZ+1nQqFz0UE26usrtnFAT+YaFZSkqF2H36QW84k0/cg==} engines: {node: 12.* || 14.* || >= 16} - ember-basic-dropdown@8.6.1: - resolution: {integrity: sha512-/KFqx88Py8G8Zpbg1ILMImt81bC/jPlQ2N78U8bNF0kcG6PXXdNeW5ufrqBevx/Yl8N5ER0KjJQ3g2BZ0BzJMA==} + ember-basic-dropdown@8.8.0: + resolution: {integrity: sha512-ETcM72Ch3EQ1ClWt2PLS7oD0k6qiA9ofpc/ckYmqkcvkGWCYwh8UEkx8fN9GhnzwbNul5Jr/3FvTc7e4G9p8bQ==} peerDependencies: '@ember/test-helpers': ^2.9.4 || ^3.2.1 || ^4.0.2 || ^5.0.0 '@glimmer/component': ^1.1.2 || ^2.0.0 @@ -4969,6 +4975,14 @@ packages: peerDependencies: ember-source: '>= 3.28.0' + ember-power-select@8.12.0: + resolution: {integrity: sha512-U+raYh8gjXnO4Xg64s4eMU1XO7Y35YtMGbBRFQCZC8ey8CZhXwpjoYtIEee83ECvgvoXVNicGrOmayoY14sUmQ==} + peerDependencies: + '@ember/test-helpers': ^2.9.4 || ^3.2.1 || ^4.0.2 || ^5.0.0 + '@glimmer/component': ^1.1.2 || ^2.0.0 + ember-basic-dropdown: ^8.7.0 + ember-concurrency: ^4.0.4 || ^5.1.0 + ember-power-select@8.7.1: resolution: {integrity: sha512-TbTv+P3QnSZc2Diq74qSO732R0dCo9wtlNUILMpGu6KWvbuTCyIB5CqfL7BpRtf7Iy6gXijYCWvraDBr5xs1aQ==} peerDependencies: @@ -5037,6 +5051,12 @@ packages: '@ember/string': ^3.1.1 || ^4.0.0 ember-source: ^3.28.0 || ^4.0.0 || >=5.0.0 + ember-style-modifier@4.5.1: + resolution: {integrity: sha512-ReVGW9fZmDIsCWsuJGH4joiiHOv9aF9Yv4lUZUjXjQyR9SEAae7RWjZcjPgmEJwpN7yDSyy4PIwdJa0smT2A3g==} + engines: {node: 18.* || >= 20, pnpm: '>= 10.*'} + peerDependencies: + '@ember/string': ^3.1.1 || ^4.0.0 + ember-template-imports@3.4.2: resolution: {integrity: sha512-OS8TUVG2kQYYwP3netunLVfeijPoOKIs1SvPQRTNOQX4Pu8xGGBEZmrv0U1YTnQn12Eg+p6w/0UdGbUnITjyzw==} engines: {node: 12.* || >= 14} @@ -7738,6 +7758,7 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qs@6.13.0: @@ -11093,7 +11114,7 @@ snapshots: '@handlebars/parser@2.0.0': {} - '@hashicorp/design-system-components@4.24.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9)))': + '@hashicorp/design-system-components@4.24.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.8.0(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9)))': dependencies: '@codemirror/commands': 6.8.1 '@codemirror/lang-go': 6.0.1 @@ -11128,7 +11149,7 @@ snapshots: ember-focus-trap: 1.1.1(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) ember-get-config: 2.1.1(@glint/template@1.5.2) ember-modifier: 4.2.2(@babel/core@7.27.1) - ember-power-select: 8.7.1(@babel/core@7.27.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))))(ember-concurrency@4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) + ember-power-select: 8.7.1(@babel/core@7.27.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.8.0(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))))(ember-concurrency@4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) ember-stargate: 0.6.0(@babel/core@7.27.1)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) ember-style-modifier: 4.4.0(@babel/core@7.27.1)(@ember/string@4.0.1)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) ember-truth-helpers: 4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) @@ -13620,7 +13641,7 @@ snapshots: - supports-color - webpack - ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))): + ember-basic-dropdown@8.8.0(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))): dependencies: '@ember/test-helpers': 5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2) '@embroider/addon-shim': 1.10.0 @@ -13629,9 +13650,8 @@ snapshots: '@glimmer/component': 2.0.0 decorator-transforms: 2.3.0(@babel/core@7.27.1) ember-element-helper: 0.8.8 - ember-lifeline: 7.0.0(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2)) ember-modifier: 4.2.2(@babel/core@7.27.1) - ember-style-modifier: 4.4.0(@babel/core@7.27.1)(@ember/string@4.0.1)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) + ember-style-modifier: 4.5.1(@babel/core@7.27.1)(@ember/string@4.0.1) ember-truth-helpers: 4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) transitivePeerDependencies: - '@babel/core' @@ -14475,7 +14495,27 @@ snapshots: transitivePeerDependencies: - supports-color - ember-power-select@8.7.1(@babel/core@7.27.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.6.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))))(ember-concurrency@4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))): + ember-power-select@8.12.0(@babel/core@7.27.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.8.0(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))))(ember-concurrency@4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))): + dependencies: + '@ember/test-helpers': 5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2) + '@embroider/addon-shim': 1.10.0 + '@embroider/util': 1.13.4(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) + '@glimmer/component': 2.0.0 + decorator-transforms: 2.3.0(@babel/core@7.27.1) + ember-assign-helper: 0.5.1 + ember-basic-dropdown: 8.8.0(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) + ember-concurrency: 4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2) + ember-element-helper: 0.8.8 + ember-modifier: 4.2.2(@babel/core@7.27.1) + ember-truth-helpers: 4.0.3(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) + transitivePeerDependencies: + - '@babel/core' + - '@glint/environment-ember-loose' + - '@glint/template' + - ember-source + - supports-color + + ember-power-select@8.7.1(@babel/core@7.27.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-basic-dropdown@8.8.0(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))))(ember-concurrency@4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2))(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))): dependencies: '@ember/test-helpers': 5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2) '@embroider/addon-shim': 1.10.0 @@ -14483,7 +14523,7 @@ snapshots: '@glimmer/component': 2.0.0 decorator-transforms: 2.3.0(@babel/core@7.27.1) ember-assign-helper: 0.5.1 - ember-basic-dropdown: 8.6.1(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) + ember-basic-dropdown: 8.8.0(@babel/core@7.27.1)(@ember/string@4.0.1)(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2))(@glimmer/component@2.0.0)(@glint/template@1.5.2)(ember-source@5.12.0(@glimmer/component@2.0.0)(@glint/template@1.5.2)(rsvp@4.8.5)(webpack@5.99.8(esbuild@0.25.9))) ember-concurrency: 4.0.4(@babel/core@7.27.1)(@glint/template@1.5.2) ember-lifeline: 7.0.0(@ember/test-helpers@5.2.2(@babel/core@7.27.1)(@glint/template@1.5.2)) ember-modifier: 4.2.2(@babel/core@7.27.1) @@ -14635,6 +14675,17 @@ snapshots: - '@babel/core' - supports-color + ember-style-modifier@4.5.1(@babel/core@7.27.1)(@ember/string@4.0.1): + dependencies: + '@ember/string': 4.0.1 + '@embroider/addon-shim': 1.10.0 + csstype: 3.1.3 + decorator-transforms: 2.3.0(@babel/core@7.27.1) + ember-modifier: 4.2.2(@babel/core@7.27.1) + transitivePeerDependencies: + - '@babel/core' + - supports-color + ember-template-imports@3.4.2: dependencies: babel-import-util: 0.2.0 diff --git a/ui/admin/app/abilities/credential.js b/ui/admin/app/abilities/credential.js deleted file mode 100644 index 5485847b16..0000000000 --- a/ui/admin/app/abilities/credential.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import CredentialAbility from 'api/abilities/credential'; -import { service } from '@ember/service'; - -export default class OverrideCredentialAbility extends CredentialAbility { - // =service - @service features; - - /** - * This override ensures that JSON credentials may be read only if the - * json-credentials feature flag is enabled. All other types are subject - * to the standard logic found in the api addon. - */ - get canRead() { - return this.features.isEnabled('json-credentials') || !this.model.isJSON - ? super.canRead - : false; - } -} diff --git a/ui/admin/app/components/form/credential-library/mapping-overrides/index.hbs b/ui/admin/app/components/form/credential-library/mapping-overrides/index.hbs index 894013d876..d22d2697e8 100644 --- a/ui/admin/app/components/form/credential-library/mapping-overrides/index.hbs +++ b/ui/admin/app/components/form/credential-library/mapping-overrides/index.hbs @@ -3,49 +3,36 @@ SPDX-License-Identifier: BUSL-1.1 }} - - <:fieldset as |F|> - - {{t - 'resources.credential-library.form.credential_mapping_overrides.label' - }} - - - {{t - 'resources.credential-library.form.credential_mapping_overrides.help' - }} - - - {{#if @model.errors.credential_mapping_overrides}} - - {{#each @model.errors.credential_mapping_overrides as |error|}} - {{error.message}} - {{/each}} - - {{/if}} - - - <:field as |F|> - - <:newKey as |N|> - - - <:key as |K|> - - - <:value as |V|> - - - - - \ No newline at end of file + + {{t + 'resources.credential-library.form.credential_mapping_overrides.label' + }} + {{t + 'resources.credential-library.form.credential_mapping_overrides.help' + }} + + {{#each this.mappingOverrides as |option|}} + + {{t + (concat 'resources.credential-library.titles.' option) + }} + + {{/each}} + + {{#if @model.errors.credential_mapping_overrides}} + + {{#each @model.errors.credential_mapping_overrides as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + \ No newline at end of file diff --git a/ui/admin/app/components/form/credential-library/mapping-overrides/index.js b/ui/admin/app/components/form/credential-library/mapping-overrides/index.js index 9d50d724a6..cd082393b5 100644 --- a/ui/admin/app/components/form/credential-library/mapping-overrides/index.js +++ b/ui/admin/app/components/form/credential-library/mapping-overrides/index.js @@ -5,43 +5,11 @@ import Component from '@glimmer/component'; import { options } from 'api/models/credential-library'; -import { action } from '@ember/object'; export default class FormCredentialLibraryMappingOverridesComponent extends Component { + // =attributes + get mappingOverrides() { return options.mapping_overrides[this.args.model.credential_type]; } - - get allowedEntries() { - return this.mappingOverrides.length; - } - - /** - * Prevents users from selecting duplicate keys from the select list if the arg is set to true - * @type {array} - */ - get selectOptions() { - const previouslySelectedKeys = - this.args.model.credential_mapping_overrides || []; - if (previouslySelectedKeys.length) { - return this.mappingOverrides.filter((key) => - previouslySelectedKeys.every((obj) => obj.key !== key), - ); - } else { - return this.mappingOverrides; - } - } - - /** - * Determines if we need to show an empty row to the users to enter more key/value pairs based on removeDuplicates arg, - * by default it is true - * @type {object} - */ - @action - showNewRow() { - return ( - this.args.model.credential_mapping_overrides?.length !== - this.allowedEntries - ); - } } diff --git a/ui/admin/app/components/form/credential-library/radio/index.hbs b/ui/admin/app/components/form/credential-library/radio/index.hbs index e2bc8fd96a..fa209f0794 100644 --- a/ui/admin/app/components/form/credential-library/radio/index.hbs +++ b/ui/admin/app/components/form/credential-library/radio/index.hbs @@ -5,6 +5,7 @@ diff --git a/ui/admin/app/components/form/credential-library/vault-generic/index.hbs b/ui/admin/app/components/form/credential-library/vault-generic/index.hbs index f4b61eb27d..aaa1725e1d 100644 --- a/ui/admin/app/components/form/credential-library/vault-generic/index.hbs +++ b/ui/admin/app/components/form/credential-library/vault-generic/index.hbs @@ -14,7 +14,6 @@ name='name' @value={{@model.name}} @isInvalid={{@model.errors.name}} - @isOptional={{true}} disabled={{form.disabled}} {{on 'input' (set-from-event @model 'name')}} as |F| @@ -36,7 +35,6 @@ name='description' @value={{@model.description}} @isInvalid={{@model.errors.description}} - @isOptional={{true}} disabled={{form.disabled}} as |F| > @@ -70,7 +68,6 @@ {{/if}} - {{else}} + {{else if @model.credential_type}} type !== 'json'); + return TYPES_CREDENTIAL; } } diff --git a/ui/admin/app/components/form/credential/json/index.hbs b/ui/admin/app/components/form/credential/json/index.hbs index ff8eedfc4f..90f11f567b 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| > @@ -53,7 +51,7 @@ {{#if @model.isNew}} - + + + {{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/radio/index.hbs b/ui/admin/app/components/form/credential/radio/index.hbs deleted file mode 100644 index edbbc3c4e8..0000000000 --- a/ui/admin/app/components/form/credential/radio/index.hbs +++ /dev/null @@ -1,27 +0,0 @@ -{{! - Copyright (c) HashiCorp, Inc. - SPDX-License-Identifier: BUSL-1.1 -}} - - - {{t 'form.type.label'}} - {{#each @types as |credentialType|}} - - {{t - (concat 'resources.credential.types.' credentialType) - }} - {{t - (concat 'resources.credential.help.' credentialType) - }} - - {{/each}} - \ 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 new file mode 100644 index 0000000000..76e536a5e5 --- /dev/null +++ b/ui/admin/app/components/form/credential/select/index.hbs @@ -0,0 +1,25 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +}} + + + {{t 'form.type.label'}} + + {{#let F.options as |credentialType|}} + {{t + (concat 'resources.credential.types.' credentialType) + }} + {{t + (concat 'resources.credential.help.' credentialType) + }} + {{/let}} + + \ 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 index d56d7d953e..5852879337 100644 --- a/ui/admin/app/components/form/credential/ssh_private_key/index.hbs +++ b/ui/admin/app/components/form/credential/ssh_private_key/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| > @@ -53,7 +51,7 @@ {{#if @model.isNew}} - @@ -53,7 +51,7 @@ {{#if @model.isNew}} - @@ -53,7 +51,7 @@ {{#if @model.isNew}} - - \ No newline at end of file + + + \ No newline at end of file diff --git a/ui/admin/config/features.js b/ui/admin/config/features.js index 247e7947e5..dc8cc6cb18 100644 --- a/ui/admin/config/features.js +++ b/ui/admin/config/features.js @@ -22,19 +22,17 @@ const baseEdition = { ...licensedFeatures, byow: false, 'byow-pki-hcp-cluster-id': false, - 'json-credentials': false, 'static-credentials': false, 'target-network-address': false, 'ldap-auth-methods': false, 'worker-filter': false, 'worker-filter-hcp': false, }; -// Editions maps edition keys to their associated featuresets. +// Editions maps edition keys to their associated feature sets. const featureEditions = {}; featureEditions.oss = { ...baseEdition, byow: true, - 'json-credentials': true, 'static-credentials': true, 'target-network-address': true, 'ldap-auth-methods': true, diff --git a/ui/admin/package.json b/ui/admin/package.json index 6e27367719..12cf34b858 100644 --- a/ui/admin/package.json +++ b/ui/admin/package.json @@ -64,6 +64,7 @@ "ember-a11y-refocus": "^5.0.0", "ember-a11y-testing": "^7.1.2", "ember-auto-import": "^2.10.0", + "ember-basic-dropdown": "^8.8.0", "ember-cli": "~5.12.0", "ember-cli-babel": "^8.2.0", "ember-cli-clean-css": "^3.0.0", @@ -82,6 +83,7 @@ "ember-load-initializers": "^2.1.2", "ember-modifier": "^4.2.0", "ember-page-title": "^8.2.3", + "ember-power-select": "^8.11.0", "ember-qunit": "^8.1.0", "ember-resolver": "^12.0.1", "ember-source": "~5.12.0", diff --git a/ui/admin/tests/acceptance/credential-library/create-test.js b/ui/admin/tests/acceptance/credential-library/create-test.js index 4e28c56ccd..292ddeecf4 100644 --- a/ui/admin/tests/acceptance/credential-library/create-test.js +++ b/ui/admin/tests/acceptance/credential-library/create-test.js @@ -8,6 +8,7 @@ import { visit, click, fillIn, currentURL, select } from '@ember/test-helpers'; import { setupApplicationTest } from 'admin/tests/helpers'; import { setupSqlite } from 'api/test-support/helpers/sqlite'; import { Response } from 'miragejs'; +import { faker } from '@faker-js/faker'; import { TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE, TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP, @@ -15,7 +16,7 @@ import { 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 } from 'api/models/credential'; +import { options } from 'api/models/credential-library'; module('Acceptance | credential-libraries | create', function (hooks) { setupApplicationTest(hooks); @@ -23,7 +24,7 @@ module('Acceptance | credential-libraries | create', function (hooks) { let featuresService; let getCredentialLibraryCount; - let getUsernamePasswordDomainCredentialLibraryCount; + let getTypeCredentialLibraryCount; const instances = { scopes: { @@ -73,10 +74,9 @@ module('Acceptance | credential-libraries | create', function (hooks) { // Generate resource counter getCredentialLibraryCount = () => this.server.schema.credentialLibraries.all().models.length; - getUsernamePasswordDomainCredentialLibraryCount = () => { - return this.server.schema.credentialLibraries.where({ - credentialType: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, - }).length; + getTypeCredentialLibraryCount = (credentialType) => { + return this.server.schema.credentialLibraries.where({ credentialType }) + .length; }; featuresService = this.owner.lookup('service:features'); }); @@ -100,102 +100,68 @@ module('Acceptance | credential-libraries | create', function (hooks) { assert.strictEqual(currentURL(), urls.credentialLibrary); }); - test('can create a new credential library of type vault generic', async function (assert) { - setRunOptions({ - rules: { - 'color-contrast': { - // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-08-01 - enabled: false, - }, - }, - }); - - const count = getCredentialLibraryCount(); - await visit(urls.newCredentialLibrary); - - await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE); - await select( - selectors.FIELD_CRED_TYPE, - selectors.FIELD_CRED_TYPE_SSH_VALUE, - ); - await select( - selectors.FIELD_CRED_MAP_OVERRIDES_SELECT, - selectors.FIELD_CRED_MAP_OVERRIDES_SELECT_SSH_VALUE, - ); - await fillIn(selectors.FIELD_CRED_MAP_OVERRIDES_INPUT, 'key'); - await click(selectors.FIELD_CRED_MAP_OVERRIDES_BTN); - await click(commonSelectors.SAVE_BTN); - - assert.strictEqual(getCredentialLibraryCount(), count + 1); - const credentialLibrary = this.server.schema.credentialLibraries.findBy({ - name: commonSelectors.FIELD_NAME_VALUE, - }); - assert.strictEqual( - credentialLibrary.name, - commonSelectors.FIELD_NAME_VALUE, - ); - assert.strictEqual( - credentialLibrary.credentialType, - selectors.FIELD_CRED_TYPE_SSH_VALUE, - ); - assert.deepEqual(credentialLibrary.credentialMappingOverrides, { - private_key_attribute: 'key', - }); - }); - - test('can create a new credential library with username, password and domain type for vault generic', async function (assert) { - setRunOptions({ - rules: { - 'color-contrast': { - // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-08-26 - enabled: false, + test.each( + 'can create a new credential library with credential type for vault generic', + options.credential_types, + async function (assert, type) { + setRunOptions({ + rules: { + 'color-contrast': { + // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-08-26 + enabled: false, + }, }, - }, - }); - - const credentialLibraryCount = getCredentialLibraryCount(); - const usernamePasswordDomainCredentialLibraryCount = - getUsernamePasswordDomainCredentialLibraryCount(); - await visit(urls.newCredentialLibrary); - - await fillIn(selectors.FIELD_VAULT_PATH, selectors.FIELD_VAULT_PATH_VALUE); - await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE); - await select( - selectors.FIELD_CRED_TYPE, - selectors.FIELD_CRED_TYPE_UPD_VALUE, - ); - - await select( - selectors.FIELD_CRED_MAP_OVERRIDES_SELECT, - selectors.FIELD_CRED_MAP_OVERRIDES_SELECT_DOMAIN_VALUE, - ); - await fillIn(selectors.FIELD_CRED_MAP_OVERRIDES_INPUT, 'domain'); + }); - await click(selectors.FIELD_CRED_MAP_OVERRIDES_BTN); - await click(commonSelectors.SAVE_BTN); + const credentialLibraryCount = getCredentialLibraryCount(); + const typeCredentialLibraryCount = getTypeCredentialLibraryCount(type); + await visit(urls.newCredentialLibrary); - assert.strictEqual(getCredentialLibraryCount(), credentialLibraryCount + 1); - assert.strictEqual( - getUsernamePasswordDomainCredentialLibraryCount(), - usernamePasswordDomainCredentialLibraryCount + 1, - ); + await fillIn( + selectors.FIELD_VAULT_PATH, + selectors.FIELD_VAULT_PATH_VALUE, + ); + await fillIn( + commonSelectors.FIELD_NAME, + commonSelectors.FIELD_NAME_VALUE, + ); + await select(selectors.FIELD_CRED_TYPE, type); + + const credentialMappingOverrides = {}; + options.mapping_overrides[type].forEach(async (overrideField) => { + const randName = faker.word.words(); + credentialMappingOverrides[overrideField] = randName; + await fillIn( + selectors.FIELD_CRED_MAP_OVERRIDES(overrideField), + randName, + ); + }); + await click(commonSelectors.SAVE_BTN); + + assert.strictEqual( + getCredentialLibraryCount(), + credentialLibraryCount + 1, + ); + assert.strictEqual( + getTypeCredentialLibraryCount(), + typeCredentialLibraryCount + 1, + ); - const credentialLibrary = this.server.schema.credentialLibraries.findBy({ - credentialType: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, - }); + const credentialLibrary = this.server.schema.credentialLibraries.findBy({ + credentialType: type, + }); - assert.strictEqual( - credentialLibrary.name, - commonSelectors.FIELD_NAME_VALUE, - ); - assert.strictEqual( - credentialLibrary.credentialType, - selectors.FIELD_CRED_TYPE_UPD_VALUE, - ); - assert.deepEqual(credentialLibrary.credentialMappingOverrides, { - domain_attribute: 'domain', - }); - }); + assert.strictEqual( + credentialLibrary.name, + commonSelectors.FIELD_NAME_VALUE, + ); + assert.strictEqual(credentialLibrary.credentialType, type); + assert.deepEqual( + credentialLibrary.credentialMappingOverrides, + credentialMappingOverrides, + ); + }, + ); test('can create a new credential library of type vault ssh cert', async function (assert) { setRunOptions({ diff --git a/ui/admin/tests/acceptance/credential-library/selectors.js b/ui/admin/tests/acceptance/credential-library/selectors.js index d66b0891d3..8b208703ed 100644 --- a/ui/admin/tests/acceptance/credential-library/selectors.js +++ b/ui/admin/tests/acceptance/credential-library/selectors.js @@ -26,15 +26,7 @@ export const FIELD_TTL_VALUE = 'ttl'; export const FIELD_KEY_ID = '[name=key_id]'; export const FIELD_KEY_ID_VALUE = 'key_id'; -export const FIELD_CRED_MAP_OVERRIDES_SELECT = - '[name="credential_mapping_overrides"] select'; -export const FIELD_CRED_MAP_OVERRIDES_SELECT_SSH_VALUE = - 'private_key_attribute'; -export const FIELD_CRED_MAP_OVERRIDES_SELECT_DOMAIN_VALUE = 'domain_attribute'; -export const FIELD_CRED_MAP_OVERRIDES_INPUT = - '[name="credential_mapping_overrides"] input'; -export const FIELD_CRED_MAP_OVERRIDES_BTN = - '[name="credential_mapping_overrides"] button'; +export const FIELD_CRED_MAP_OVERRIDES = (field) => `[name=${field}]`; export const TYPE_VAULT_SSH_CERT = '[value="vault-ssh-certificate"]'; export const TYPE_VAULT_LDAP = '[value="vault-ldap"]'; diff --git a/ui/admin/tests/acceptance/credential-library/update-test.js b/ui/admin/tests/acceptance/credential-library/update-test.js index 8366c7939c..1ea602eaf8 100644 --- a/ui/admin/tests/acceptance/credential-library/update-test.js +++ b/ui/admin/tests/acceptance/credential-library/update-test.js @@ -4,19 +4,20 @@ */ import { module, test } from 'qunit'; -import { visit, click, fillIn, currentURL, select } from '@ember/test-helpers'; +import { visit, click, fillIn, currentURL } from '@ember/test-helpers'; import { setupApplicationTest } from 'admin/tests/helpers'; import { setupSqlite } from 'api/test-support/helpers/sqlite'; import { Response } from 'miragejs'; +import { faker } from '@faker-js/faker'; +import * as selectors from './selectors'; +import * as commonSelectors from 'admin/tests/helpers/selectors'; +import { setRunOptions } from 'ember-a11y-testing/test-support'; import { TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE, TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC, TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP, } from 'api/models/credential-library'; -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 } from 'api/models/credential'; +import { options } from 'api/models/credential-library'; module('Acceptance | credential-libraries | update', function (hooks) { setupApplicationTest(hooks); @@ -57,17 +58,9 @@ module('Acceptance | credential-libraries | update', function (hooks) { }); instances.credentialLibrary = this.server.create('credential-library', { scope: instances.scopes.project, + type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC, credentialStore: instances.credentialStore, }); - instances.usernamePasswordDomainCredentialLibrary = this.server.create( - 'credential-library', - { - scope: instances.scopes.project, - credentialStore: instances.credentialStore, - type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC, - credential_type: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, - }, - ); instances.vaultLDAPCredentialLibrary = this.server.create( 'credential-library', { @@ -86,7 +79,6 @@ module('Acceptance | credential-libraries | update', function (hooks) { urls.credentialLibrary = `${urls.credentialLibraries}/${instances.credentialLibrary.id}`; urls.newCredentialLibrary = `${urls.credentialLibraries}/new`; urls.unknownCredentialLibrary = `${urls.credentialLibraries}/foo`; - urls.usernamePasswordDomainCredentialLibrary = `${urls.credentialLibraries}/${instances.usernamePasswordDomainCredentialLibrary.id}`; urls.vaultLDAPCredentialLibrary = `${urls.credentialLibraries}/${instances.vaultLDAPCredentialLibrary.id}`; }); @@ -126,100 +118,67 @@ module('Acceptance | credential-libraries | update', function (hooks) { .hasValue(instances.credentialLibrary.name); }); - test('can update a vault generic credential library and save changes', async function (assert) { - setRunOptions({ - rules: { - 'color-contrast': { - // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-08-01 - enabled: false, - }, - }, - }); - - await visit(urls.credentialLibrary); - - await click(commonSelectors.EDIT_BTN); - await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE); - await fillIn( - commonSelectors.FIELD_DESCRIPTION, - commonSelectors.FIELD_DESCRIPTION_VALUE, - ); - await fillIn(selectors.FIELD_VAULT_PATH, selectors.FIELD_VAULT_PATH_VALUE); - await select( - selectors.FIELD_CRED_MAP_OVERRIDES_SELECT, - selectors.FIELD_CRED_MAP_OVERRIDES_SELECT_SSH_VALUE, - ); - await fillIn(selectors.FIELD_CRED_MAP_OVERRIDES_INPUT, 'key'); - await click(selectors.FIELD_CRED_MAP_OVERRIDES_BTN); - await click(commonSelectors.SAVE_BTN); - - const credentialLibrary = this.server.schema.credentialLibraries.findBy({ - name: commonSelectors.FIELD_NAME_VALUE, - }); - assert.strictEqual( - credentialLibrary.name, - commonSelectors.FIELD_NAME_VALUE, - ); - assert.strictEqual( - credentialLibrary.description, - commonSelectors.FIELD_DESCRIPTION_VALUE, - ); - assert.strictEqual( - credentialLibrary.attributes.path, - selectors.FIELD_VAULT_PATH_VALUE, - ); - assert.deepEqual(credentialLibrary.credentialMappingOverrides, { - private_key_attribute: 'key', - }); - }); - - test('can update a vault generic credential library of username, password and domain type and save changes', async function (assert) { - setRunOptions({ - rules: { - 'color-contrast': { - // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-08-26 - enabled: false, + test.each( + 'can update a vault generic credential library with credential type and save changes', + options.credential_types, + async function (assert, type) { + setRunOptions({ + rules: { + 'color-contrast': { + // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-08-26 + enabled: false, + }, }, - }, - }); - - await visit(urls.usernamePasswordDomainCredentialLibrary); - - await click(commonSelectors.EDIT_BTN); - await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE); - await fillIn( - commonSelectors.FIELD_DESCRIPTION, - commonSelectors.FIELD_DESCRIPTION_VALUE, - ); - await fillIn(selectors.FIELD_VAULT_PATH, selectors.FIELD_VAULT_PATH_VALUE); - await select( - selectors.FIELD_CRED_MAP_OVERRIDES_SELECT, - selectors.FIELD_CRED_MAP_OVERRIDES_SELECT_DOMAIN_VALUE, - ); - await fillIn(selectors.FIELD_CRED_MAP_OVERRIDES_INPUT, 'domain'); - - await click(selectors.FIELD_CRED_MAP_OVERRIDES_BTN); - await click(commonSelectors.SAVE_BTN); + }); + instances.credentialLibrary.update({ credentialType: type }); + await visit(urls.credentialLibraries); - const credentialLibrary = this.server.schema.credentialLibraries.findBy({ - credentialType: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, - }); - assert.strictEqual( - credentialLibrary.name, - commonSelectors.FIELD_NAME_VALUE, - ); - assert.strictEqual( - credentialLibrary.description, - commonSelectors.FIELD_DESCRIPTION_VALUE, - ); - assert.strictEqual( - credentialLibrary.attributes.path, - selectors.FIELD_VAULT_PATH_VALUE, - ); - assert.deepEqual(credentialLibrary.credentialMappingOverrides, { - domain_attribute: 'domain', - }); - }); + await click(commonSelectors.HREF(urls.credentialLibrary)); + await click(commonSelectors.EDIT_BTN); + await fillIn( + commonSelectors.FIELD_NAME, + commonSelectors.FIELD_NAME_VALUE, + ); + await fillIn( + commonSelectors.FIELD_DESCRIPTION, + commonSelectors.FIELD_DESCRIPTION_VALUE, + ); + await fillIn( + selectors.FIELD_VAULT_PATH, + selectors.FIELD_VAULT_PATH_VALUE, + ); + const credentialMappingOverrides = {}; + options.mapping_overrides[type].forEach(async (overrideField) => { + const randName = faker.word.words(); + credentialMappingOverrides[overrideField] = randName; + await fillIn( + selectors.FIELD_CRED_MAP_OVERRIDES(overrideField), + randName, + ); + }); + await click(commonSelectors.SAVE_BTN); + + const credentialLibrary = this.server.schema.credentialLibraries.findBy({ + credentialType: type, + }); + assert.strictEqual( + credentialLibrary.name, + commonSelectors.FIELD_NAME_VALUE, + ); + assert.strictEqual( + credentialLibrary.description, + commonSelectors.FIELD_DESCRIPTION_VALUE, + ); + assert.strictEqual( + credentialLibrary.attributes.path, + selectors.FIELD_VAULT_PATH_VALUE, + ); + assert.deepEqual( + credentialLibrary.credentialMappingOverrides, + credentialMappingOverrides, + ); + }, + ); test('saving an existing credential library with invalid fields displays error messages', async function (assert) { setRunOptions({ 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 fc33c0ae06..3fce761092 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; }; }); @@ -134,6 +150,7 @@ module( commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE, ); + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_USERNAME_PASSWORD_DOMAIN); await fillIn(selectors.FIELD_USERNAME, selectors.FIELD_USERNAME_VALUE); @@ -169,6 +186,7 @@ module( commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE, ); + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_USERNAME_PASSWORD_DOMAIN); await fillIn( selectors.FIELD_USERNAME, @@ -208,6 +226,7 @@ module( commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE, ); + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_SSH); await click(commonSelectors.SAVE_BTN); @@ -233,7 +252,6 @@ module( }, }); - featuresService.enable('json-credentials'); const credentialsCount = getCredentialsCount(); const jsonCredentialCount = getJsonCredentialCount(); await visit(urls.credentials); @@ -243,6 +261,7 @@ module( commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE, ); + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_JSON); await click(commonSelectors.SAVE_BTN); @@ -250,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: { @@ -292,6 +341,7 @@ module( commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE, ); + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_SSH); await click(commonSelectors.CANCEL_BTN); @@ -314,7 +364,6 @@ module( }, }); - featuresService.enable('json-credentials'); const credentialsCount = getCredentialsCount(); await visit(urls.credentials); @@ -323,6 +372,7 @@ module( commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE, ); + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_JSON); await click(commonSelectors.CANCEL_BTN); @@ -348,6 +398,7 @@ module( commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE, ); + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_USERNAME_PASSWORD_DOMAIN); await click(commonSelectors.CANCEL_BTN); @@ -370,20 +421,48 @@ module( }, }); - featuresService.enable('json-credentials'); - await visit(urls.credentials); await click(commonSelectors.HREF(urls.newCredential)); + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_JSON); await fillIn(selectors.FIELD_EDITOR, selectors.FIELD_EDITOR_VALUE); assert.dom(selectors.EDITOR).includesText(selectors.FIELD_EDITOR_VALUE); + + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_USERNAME_PASSWORD); + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_JSON); 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: { @@ -489,6 +568,7 @@ module( }); await click(commonSelectors.HREF(urls.newCredential)); + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_SSH); await click(commonSelectors.SAVE_BTN); @@ -533,6 +613,7 @@ module( ); }); await click(commonSelectors.HREF(urls.newCredential)); + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_USERNAME_PASSWORD_DOMAIN); await click(commonSelectors.SAVE_BTN); assert.dom(commonSelectors.ALERT_TOAST_BODY).hasText(errorMessage); @@ -559,7 +640,6 @@ module( }, }); - featuresService.enable('json-credentials'); const errorMessage = 'Error in provided request.'; await visit(urls.credentials); this.server.post('/credentials', () => { @@ -583,41 +663,54 @@ module( }); await click(commonSelectors.HREF(urls.newCredential)); + await click(selectors.TYPE_SELECT); await click(selectors.FIELD_TYPE_JSON); await click(commonSelectors.SAVE_BTN); assert.dom(commonSelectors.ALERT_TOAST_BODY).hasText(errorMessage); }); - test('cannot navigate to json credential when feature is disabled', async function (assert) { + 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-08-01 + // [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.false(featuresService.isEnabled('json-credentials')); - assert.true( - instances.staticCredentialStore.authorized_collection_actions.credentials.includes( - 'create', - ), - ); - assert - .dom(selectors.FIELD_TYPE_USERNAME_PASSWORD) - .exists() - .hasAttribute('value', 'username_password'); - assert - .dom(selectors.FIELD_TYPE_SSH) - .exists() - .hasAttribute('value', 'ssh_private_key'); - assert.dom(selectors.FIELD_TYPE_JSON).doesNotExist(); + 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) { 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 edb7369a47..cb500edfe9 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 1ef30fdae9..ac0746ee31 100644 --- a/ui/admin/tests/acceptance/credential-store/credentials/read-test.js +++ b/ui/admin/tests/acceptance/credential-store/credentials/read-test.js @@ -9,13 +9,18 @@ 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); setupSqlite(hooks); - let featuresService; - const instances = { scopes: { org: null, @@ -26,6 +31,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) { usernameKeyPairCredential: null, jsonCredential: null, usernamePasswordDomainCredential: null, + passwordCredential: null, }; const urls = { @@ -38,6 +44,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) { jsonCredential: null, unknownCredential: null, usernamePasswordDomainCredential: null, + passwordCredential: null, }; hooks.beforeEach(async function () { @@ -57,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}`; @@ -87,8 +99,8 @@ 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`; - featuresService = this.owner.lookup('service:features'); }); test('visiting username & password credential', async function (assert) { @@ -141,7 +153,6 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) { }, }); - featuresService.enable('json-credentials'); await visit(urls.staticCredentialStore); await click(commonSelectors.HREF(urls.credentials)); @@ -173,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: { @@ -270,24 +301,27 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) { .doesNotExist(); }); - test('cannot navigate to a JSON credential form when feature not enabled', async function (assert) { + 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-08-01 + // [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.false(featuresService.isEnabled('json-credentials')); assert .dom(commonSelectors.TABLE_RESOURCE_LINK(urls.usernamePasswordCredential)) .isVisible(); assert - .dom(commonSelectors.TABLE_RESOURCE_LINK(urls.jsonCredential)) + .dom(commonSelectors.TABLE_RESOURCE_LINK(urls.passwordCredential)) .doesNotExist(); }); diff --git a/ui/admin/tests/acceptance/credential-store/credentials/selectors.js b/ui/admin/tests/acceptance/credential-store/credentials/selectors.js index c90a4b6aa4..591f22de37 100644 --- a/ui/admin/tests/acceptance/credential-store/credentials/selectors.js +++ b/ui/admin/tests/acceptance/credential-store/credentials/selectors.js @@ -3,15 +3,14 @@ * SPDX-License-Identifier: BUSL-1.1 */ -export const FIELD_TYPE_SSH = '[value=ssh_private_key]'; -export const FIELD_TYPE_USERNAME_PASSWORD = '[value=username_password]'; -export const FIELD_TYPE_USERNAME_PASSWORD_DOMAIN = - '[value=username_password_domain]'; -export const FIELD_DOMAIN = - '[name=domain]'; +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 = '[value=json]'; export const FIELD_DOMAIN_VALUE = 'g.com'; export const FIELD_USERNAME_VALUE = 'username123'; export const FIELD_PASSWORD_VALUE = 'password123'; @@ -37,3 +36,5 @@ export const MANAGE_DROPDOWN_CREDENTIAL_STORE = '[data-test-manage-credential-stores-dropdown] button'; export const REPLACE_SECRET_BTN = '.secret-editor button'; + +export const TYPE_SELECT = '[name=Type]'; 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 2f3f1a4cfa..c869df66b2 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', @@ -25,8 +32,6 @@ module( setupApplicationTest(hooks); setupSqlite(hooks); - let featuresService; - const instances = { scopes: { org: null, @@ -37,6 +42,7 @@ module( usernameKeyPairCredential: null, jsonCredential: null, usernamePasswordDomainCredential: null, + passwordCredential: null, }; const urls = { @@ -48,6 +54,7 @@ module( usernameKeyPairCredential: null, jsonCredential: null, usernamePasswordDomainCredential: null, + passwordCredential: null, }; const mockResponseMessage = 'Error in provided request.'; @@ -89,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}`; @@ -119,8 +131,7 @@ module( urls.usernameKeyPairCredential = `${urls.credentials}/${instances.usernameKeyPairCredential.id}`; urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`; urls.usernamePasswordDomainCredential = `${urls.credentials}/${instances.usernamePasswordDomainCredential.id}`; - - featuresService = this.owner.lookup('service:features'); + urls.passwordCredential = `${urls.credentials}/${instances.passwordCredential.id}`; }); test('can save changes to existing username & password credential', async function (assert) { @@ -186,7 +197,6 @@ module( }); test('can save changes to existing JSON credential', async function (assert) { - featuresService.enable('json-credentials'); assert.notEqual( instances.jsonCredential.name, commonSelectors.FIELD_NAME, @@ -236,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( @@ -267,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: { @@ -341,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: { @@ -410,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: { @@ -535,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: { @@ -684,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: { @@ -704,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 5c083daeac..22b9c36ffc 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, @@ -32,7 +33,6 @@ module('Acceptance | targets | brokered credential sources', function (hooks) { let credentialSourceCount; let randomlySelectedCredentialLibraries; let randomlySelectedCredentials; - let featuresService; const instances = { scopes: { @@ -57,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, @@ -64,7 +65,6 @@ module('Acceptance | targets | brokered credential sources', function (hooks) { }; hooks.beforeEach(async function () { - featuresService = this.owner.lookup('service:features'); // Generate resources instances.scopes.org = this.server.create('scope', { type: 'org', @@ -82,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, }); @@ -134,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 = () => @@ -205,48 +206,24 @@ module('Acceptance | targets | brokered credential sources', function (hooks) { targetName: 'tcpTarget', link: 'jsonCredential', expectedUrl: 'jsonCredential', - enableJsonFeature: true, }, 'json credential type for RDP target': { route: 'brokeredCredentialSourcesForRDPTarget', targetName: 'rdpTarget', link: 'jsonCredential', expectedUrl: 'jsonCredential', - enableJsonFeature: true, }, - }, - async function (assert, input) { - setRunOptions({ - rules: { - 'color-contrast': { - // [ember-a11y-ignore]: axe rule "color-contrast" automatically ignored on 2025-08-01 - enabled: false, - }, - }, - }); - - // needed only if the test is for json credential - if (input.enableJsonFeature) { - featuresService.enable('json-credentials'); - } - await visit(urls[input.route]); - - await click(commonSelectors.TABLE_RESOURCE_LINK(urls[input.link])); - - assert.strictEqual(currentURL(), urls[input.expectedUrl]); - }, - ); - - test.each( - 'cannot navigate to a json type credential when feature is disabled', - { - 'for TCP target': { + 'password credential type for TCP target': { route: 'brokeredCredentialSourcesForTCPTarget', targetName: 'tcpTarget', + link: 'passwordCredential', + expectedUrl: 'passwordCredential', }, - 'for RDP target': { + 'password credential type for RDP target': { route: 'brokeredCredentialSourcesForRDPTarget', targetName: 'rdpTarget', + link: 'passwordCredential', + expectedUrl: 'passwordCredential', }, }, async function (assert, input) { @@ -259,17 +236,11 @@ module('Acceptance | targets | brokered credential sources', function (hooks) { }, }); - const jsonCredential = instances.credentials[3]; - instances[input.targetName].update({ - brokeredCredentialSourceIds: [...randomlySelectedCredentials], - }); await visit(urls[input.route]); - assert.false(featuresService.isEnabled('json-credentials')); - assert - .dom(commonSelectors.TABLE_ROW(4)) - .includesText(jsonCredential.name); - assert.dom(commonSelectors.HREF(urls.jsonCredential)).doesNotExist(); + await click(commonSelectors.TABLE_RESOURCE_LINK(urls[input.link])); + + assert.strictEqual(currentURL(), urls[input.expectedUrl]); }, ); @@ -471,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, @@ -574,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, @@ -616,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, diff --git a/ui/admin/tests/acceptance/targets/injected-application-credential-sources-test.js b/ui/admin/tests/acceptance/targets/injected-application-credential-sources-test.js index c0789177f5..49e4b00c8e 100644 --- a/ui/admin/tests/acceptance/targets/injected-application-credential-sources-test.js +++ b/ui/admin/tests/acceptance/targets/injected-application-credential-sources-test.js @@ -15,6 +15,7 @@ import { TYPE_CREDENTIAL_USERNAME_PASSWORD, TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, TYPE_CREDENTIAL_JSON, + TYPE_CREDENTIAL_PASSWORD, } from 'api/models/credential'; import { setRunOptions } from 'ember-a11y-testing/test-support'; @@ -121,6 +122,7 @@ module( (cred) => ![ TYPE_CREDENTIAL_JSON, + TYPE_CREDENTIAL_PASSWORD, TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, ].includes(cred.type), ); @@ -130,6 +132,7 @@ module( (cred) => { return ( cred.credential_type !== TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN && + cred.credential_type !== TYPE_CREDENTIAL_PASSWORD && cred.type !== TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP ); }, @@ -191,6 +194,7 @@ module( this.server.schema.credentialLibraries.where((c) => { return ( c.credential_type !== TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN && + c.credential_type !== TYPE_CREDENTIAL_PASSWORD && c.type !== TYPE_CREDENTIAL_LIBRARY_VAULT_LDAP ); }).models.length; @@ -200,6 +204,7 @@ module( (cred) => ![ TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN, + TYPE_CREDENTIAL_PASSWORD, TYPE_CREDENTIAL_JSON, ].includes(cred.type), ).models.length; diff --git a/ui/admin/tests/unit/abilities/credential-test.js b/ui/admin/tests/unit/abilities/credential-test.js deleted file mode 100644 index 2dc7f838ec..0000000000 --- a/ui/admin/tests/unit/abilities/credential-test.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; - -module('Unit | Abilities | credential', function (hooks) { - setupTest(hooks); - - let featuresService; - - hooks.beforeEach(function () { - // Enable json-credentials feature by default - featuresService = this.owner.lookup('service:features'); - }); - - test('can read credentials, including JSON credentials, when authorized and json-credentials feature is enabled', function (assert) { - featuresService.enable('json-credentials'); - const service = this.owner.lookup('service:can'); - const store = this.owner.lookup('service:store'); - const credential = store.createRecord('credential', { - authorized_actions: ['read'], - type: 'json', - }); - assert.true(service.can('read credential', credential)); - credential.type = 'username_password'; - assert.true(service.can('read credential', credential)); - credential.type = 'ssh_private_key'; - assert.true(service.can('read credential', credential)); - }); - - test('cannot read credentials, including JSON credentials, when unauthorized and json-credentials feature is enabled', function (assert) { - featuresService.enable('json-credentials'); - const service = this.owner.lookup('service:can'); - const store = this.owner.lookup('service:store'); - const credential = store.createRecord('credential', { - authorized_actions: [], - type: 'json', - }); - assert.false(service.can('read credential', credential)); - credential.type = 'username_password'; - assert.false(service.can('read credential', credential)); - credential.type = 'ssh_private_key'; - assert.false(service.can('read credential', credential)); - }); - - test('cannot read credentials, including JSON credentials, when unauthorized and json-credentials feature is disabled', function (assert) { - const service = this.owner.lookup('service:can'); - const store = this.owner.lookup('service:store'); - const credential = store.createRecord('credential', { - authorized_actions: [], - type: 'json', - }); - assert.false(featuresService.isEnabled('json-credentials')); - assert.false(service.can('read credential', credential)); - credential.type = 'username_password'; - assert.false(service.can('read credential', credential)); - credential.type = 'ssh_private_key'; - assert.false(service.can('read credential', credential)); - }); - - test('can read credentials, excepting JSON credentials, when authorized and json-credentials feature is disabled', function (assert) { - const service = this.owner.lookup('service:can'); - const store = this.owner.lookup('service:store'); - const credential = store.createRecord('credential', { - authorized_actions: ['read'], - type: 'json', - }); - assert.false(featuresService.isEnabled('json-credentials')); - assert.false(service.can('read credential', credential)); - credential.type = 'username_password'; - assert.true(service.can('read credential', credential)); - credential.type = 'ssh_private_key'; - assert.true(service.can('read credential', credential)); - }); -}); diff --git a/ui/desktop/app/styles/app.scss b/ui/desktop/app/styles/app.scss index e53d43c10c..03eb12f012 100644 --- a/ui/desktop/app/styles/app.scss +++ b/ui/desktop/app/styles/app.scss @@ -331,6 +331,8 @@ .credential-header { padding: 1.5rem; width: 33%; + display: flex; + flex-direction: column; } .credential-secret {