From 36c2aee271934750a5684b50242415a2d96f64b3 Mon Sep 17 00:00:00 2001 From: Dharini Jeeva Date: Tue, 26 Nov 2024 14:34:10 -0800 Subject: [PATCH] fix vault brokered credentials display (#2583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 🐛 brokered credential display in credential panel --- addons/api/addon/models/session.js | 25 +++++++++-- .../api/addon/utils/flatten-nested-object.js | 27 ++++++++++++ addons/api/mirage/scenarios/ipc.js | 29 +++++++++++++ addons/api/tests/unit/models/session-test.js | 42 ++++++++++++++++++ .../unit/utils/flatten-nested-object-test.js | 43 +++++++++++++++++++ .../components/credentials-panel-test.js | 26 +++++++++++ 6 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 addons/api/addon/utils/flatten-nested-object.js create mode 100644 addons/api/tests/unit/utils/flatten-nested-object-test.js diff --git a/addons/api/addon/models/session.js b/addons/api/addon/models/session.js index dc9bc9275..8e3dbef9a 100644 --- a/addons/api/addon/models/session.js +++ b/addons/api/addon/models/session.js @@ -8,6 +8,8 @@ import { equal } from '@ember/object/computed'; import { tracked } from '@glimmer/tracking'; import { A } from '@ember/array'; import { inject as service } from '@ember/service'; +import { flattenObject } from '../utils/flatten-nested-object'; +import { TYPES_CREDENTIAL_LIBRARY } from 'api/models/credential-library'; export const STATUS_SESSION_ACTIVE = 'active'; export const STATUS_SESSION_PENDING = 'pending'; @@ -23,7 +25,7 @@ export const statusTypes = [ /** * */ -class SessionCredential { +export class SessionCredential { // =classes /** @@ -75,9 +77,7 @@ class SessionCredential { get secrets() { if (this.#payloadSecret?.decoded) { const secretJSON = this.#payloadSecret.decoded; - return Object.keys(secretJSON).map( - (key) => new SessionCredential.SecretItem(key, secretJSON[key]), - ); + return this.extractSecrets(secretJSON, this.source.type); } else { // decode from base64 const decodedString = atob(this.#payloadSecret.raw); @@ -85,6 +85,23 @@ class SessionCredential { } } + /** + * Extracts secrets from the payload secret JSON object. + * We only need to flatten the object if the type is vault-generic + * or vault-ssh-certificate and we only need the data object. + * @param {object} secretJSON - The payload secret JSON object. + * @returns {SessionCredential.SecretItem[]} - The array of secret items. + */ + extractSecrets(secretJSON, type) { + const source = + TYPES_CREDENTIAL_LIBRARY.includes(type) && secretJSON?.data + ? flattenObject(secretJSON?.data) + : secretJSON; + return Object.entries(source).map( + ([key, value]) => new SessionCredential.SecretItem(key, value), + ); + } + // =methods constructor(cred) { diff --git a/addons/api/addon/utils/flatten-nested-object.js b/addons/api/addon/utils/flatten-nested-object.js new file mode 100644 index 000000000..5b62befcc --- /dev/null +++ b/addons/api/addon/utils/flatten-nested-object.js @@ -0,0 +1,27 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ +import { typeOf } from '@ember/utils'; + +/** + * Flattens a nested object into a single-level object. + * @param obj + * @param result + * @param parentKey + * @returns {Object} + */ + +export const flattenObject = (obj, result = {}, parentKey = '') => { + for (const key in obj) { + // Check if the value is an object and it is not null + if (typeOf(obj[key]) === 'object' && obj[key] !== null) { + // Recursively flatten the object + flattenObject(obj[key], result, `${parentKey}${key}.`); + } else if (obj[key]) { + // Only add to result if the value is not null + result[`${parentKey}${key}`] = obj[key]; + } + } + return result; +}; diff --git a/addons/api/mirage/scenarios/ipc.js b/addons/api/mirage/scenarios/ipc.js index 62e17c9c5..34b300e2c 100644 --- a/addons/api/mirage/scenarios/ipc.js +++ b/addons/api/mirage/scenarios/ipc.js @@ -83,6 +83,35 @@ export default function initializeMockIPC(server, config) { raw: btoa('just-a-random-string'), }, }, + { + credential_source: { + id: 'clvlt_4cvscMTl0N', + name: 'Credential Library 0', + description: 'Source Description', + credential_store_id: 'csvlt_Q1HFGt7Jpm', + type: 'vault', + }, + secret: { + raw: 'eyJhcnJheSI6WyJvbmUiLCJ0d28iLCJ0aHJlZSIsIm9uZSIsInR3byIsInRocmVlIiwib25lIiwidHdvIiwidGhyZWUiLCJvbmUiLCJ0d28iLCJ0aHJlZSJdLCJuZXN0ZWQiOnsiYm9vbCI6dHJ1ZSwibG9uZyI6IjEyMjM1MzQ1NmFzZWRmYTQzd3J0ZjIzNGYyM2FzZGdmYXNkZnJnYXdzZWZhd3NlZnNkZjQiLCJzZWNlcmV0Ijoic28gbmVzdGVkIn0sInRlc3QiOiJwaHJhc2UifQ', + decoded: { + data: { + password: '123', + username: 'test', + email: { + address: 'test.com', + }, + }, + metadata: { + created_time: '2024-04-12T18:38:36.226715555Z', + custom_metadata: null, + deletion_time: '', + destroyed: false, + version: 8, + }, + }, + }, + }, + { credential_source: { id: 'credjson_7cKBbBEkC3', diff --git a/addons/api/tests/unit/models/session-test.js b/addons/api/tests/unit/models/session-test.js index dfd431917..f34e6ee80 100644 --- a/addons/api/tests/unit/models/session-test.js +++ b/addons/api/tests/unit/models/session-test.js @@ -5,6 +5,8 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; +import { SessionCredential } from 'api/models/session'; +import { TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC } from 'api/models/credential-library'; module('Unit | Model | session', function (hooks) { setupTest(hooks); @@ -16,6 +18,46 @@ module('Unit | Model | session', function (hooks) { assert.notOk(model.isAvailable); }); + test('`extractSecrets` method should return an array of secret items', function (assert) { + const sessionCredential = new SessionCredential({ + credential_source: { + id: '123', + name: 'Test Source', + description: 'Test Description', + type: 'Test Type', + }, + }); + + const secrets = { + username: 'user', + password: 'pass', + }; + + const formattedSecretes = sessionCredential.extractSecrets( + secrets, + TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC, + ); + assert.deepEqual(formattedSecretes, [ + new SessionCredential.SecretItem('username', 'user'), + new SessionCredential.SecretItem('password', 'pass'), + ]); + + // check for nested data, empty values should be filtered out + const nestedSecrets = { + data: { username: 'user', password: '', email: 'test.com' }, + }; + + const formattedNestedSecrets = sessionCredential.extractSecrets( + nestedSecrets, + TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC, + ); + + assert.deepEqual(formattedNestedSecrets, [ + new SessionCredential.SecretItem('username', 'user'), + new SessionCredential.SecretItem('email', 'test.com'), + ]); + }); + test('it allows cancellation of an active session', function (assert) { let store = this.owner.lookup('service:store'); let model = store.createRecord('session', { diff --git a/addons/api/tests/unit/utils/flatten-nested-object-test.js b/addons/api/tests/unit/utils/flatten-nested-object-test.js new file mode 100644 index 000000000..0a5b00b5d --- /dev/null +++ b/addons/api/tests/unit/utils/flatten-nested-object-test.js @@ -0,0 +1,43 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { flattenObject } from 'api/utils/flatten-nested-object'; +import { module, test } from 'qunit'; + +module('Unit | Utility | flatten-nested-object', function () { + test('it flattens a nested object to single level object', function (assert) { + const input = { + a: 1, + b: { c: 2, d: 3, e: { f: { g: { h: 10 } } } }, + }; + const expected = { + a: 1, + 'b.c': 2, + 'b.d': 3, + 'b.e.f.g.h': 10, + }; + assert.deepEqual(flattenObject(input), expected); + }); + + test('removes keys with null/empty values', function (assert) { + const inputOne = { + a: null, + b: { c: 2 }, + d: { + e: { f: { g: { h: '' } } }, + }, + }; + const inputTwo = { + d: { e: null }, + }; + const expectedResultOne = { + 'b.c': 2, + }; + const expectedResultTwo = {}; + + assert.deepEqual(flattenObject(inputOne), expectedResultOne); + assert.deepEqual(flattenObject(inputTwo), expectedResultTwo); + }); +}); diff --git a/ui/desktop/tests/integration/components/credentials-panel-test.js b/ui/desktop/tests/integration/components/credentials-panel-test.js index e27a365e0..d37fc4d5e 100644 --- a/ui/desktop/tests/integration/components/credentials-panel-test.js +++ b/ui/desktop/tests/integration/components/credentials-panel-test.js @@ -40,6 +40,21 @@ module('Integration | Component | credentials-panel', function (hooks) { ], }; + const vaultCredentials = { + source: { + id: 'clvlt_4cvscMTl0N', + name: 'Credential Library 2', + description: 'Source Description', + type: 'vault', + }, + secrets: [ + { + key: 'username', + value: 'password', + }, + ], + }; + test('it renders', async function (assert) { assert.expect(1); this.set('credentials', [credential]); @@ -51,6 +66,17 @@ module('Integration | Component | credentials-panel', function (hooks) { assert.dom('.credential-name').hasText('Credential Library 1'); }); + test('it correctly shows parsed secret from vault', async function (assert) { + this.set('credentials', [vaultCredentials]); + + await render(hbs` + + `); + assert.dom('.credential-name').hasText('Credential Library 2'); + assert.dom('.secret-container').isVisible(); + assert.dom('.secret-key').hasText('username'); + }); + test('it shows code editor when toggle clicked', async function (assert) { assert.expect(1); this.set('credentials', [credential]);