chore: handle nested data from static json credentials (#2611)

This commit is contained in:
Dharini Jeeva authored and GitHub committed 2024-12-11 15:08:51 -08:00
1 parent 2869020e65
commit 6de20b03ef
5 files changed
+164 -20

No files matched your search

+12 -3
View File
@@ -8,7 +8,16 @@ import GeneratedCredentialModel from '../generated/models/credential';
/**
* Supported Credential types.
*/
export const types = ['username_password', 'ssh_private_key', 'json'];
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 TYPES_CREDENTIAL = Object.freeze([
TYPE_CREDENTIAL_USERNAME_PASSWORD,
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
TYPE_CREDENTIAL_JSON,
]);
export default class CredentialModel extends GeneratedCredentialModel {
// =attributes
@@ -28,7 +37,7 @@ export default class CredentialModel extends GeneratedCredentialModel {
* @type {boolean}
*/
get isJSON() {
return this.type === 'json';
return this.type === TYPE_CREDENTIAL_JSON;
}
/**
@@ -36,6 +45,6 @@ export default class CredentialModel extends GeneratedCredentialModel {
* @type {boolean}
*/
get isUnknown() {
return !types.includes(this.type);
return !TYPES_CREDENTIAL.includes(this.type);
}
}
+31 -11
View File
@@ -10,6 +10,7 @@ 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';
import { TYPE_CREDENTIAL_JSON } from 'api/models/credential';
export const STATUS_SESSION_ACTIVE = 'active';
export const STATUS_SESSION_PENDING = 'pending';
@@ -36,12 +37,14 @@ export class SessionCredential {
name;
description;
type;
credentialtype;
constructor(id, name, description, type) {
constructor(id, name, description, type, credentialtype) {
this.id = id;
this.name = name;
this.description = description;
this.type = type;
this.credentialType = credentialtype;
}
};
@@ -77,7 +80,11 @@ export class SessionCredential {
get secrets() {
if (this.#payloadSecret?.decoded) {
const secretJSON = this.#payloadSecret.decoded;
return this.extractSecrets(secretJSON, this.source.type);
return this.extractSecrets(
secretJSON,
this.source.type,
this.source.credentialType,
);
} else {
// decode from base64
const decodedString = atob(this.#payloadSecret.raw);
@@ -87,16 +94,22 @@ export 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.
* We only need to flatten the object if the type is vault or if its a json type credential.
* @param {object} secretJSON - The payload secret JSON object.
* @param {string} type - The credential source type.
* @param {string} credentialType - The credential type.
* @returns {SessionCredential.SecretItem[]} - The array of secret items.
*/
extractSecrets(secretJSON, type) {
const source =
TYPES_CREDENTIAL_LIBRARY.includes(type) && secretJSON?.data
? flattenObject(secretJSON?.data)
: secretJSON;
extractSecrets(secretJSON, type, credentialType) {
let source;
if (credentialType === TYPE_CREDENTIAL_JSON) {
source = flattenObject(secretJSON);
} else if (TYPES_CREDENTIAL_LIBRARY.includes(type) && secretJSON?.data) {
source = flattenObject(secretJSON.data);
} else {
source = secretJSON;
}
return Object.entries(source).map(
([key, value]) => new SessionCredential.SecretItem(key, value),
);
@@ -105,8 +118,15 @@ export class SessionCredential {
// =methods
constructor(cred) {
const { id, name, description, type } = cred.credential_source;
this.source = new SessionCredential.Source(id, name, description, type);
const { id, name, description, type, credential_type } =
cred.credential_source;
this.source = new SessionCredential.Source(
id,
name,
description,
type,
credential_type,
);
this.#payloadSecret = cred.secret;
this.rawCredential = cred;
}
+4 -1
View File
@@ -89,12 +89,14 @@ export default function initializeMockIPC(server, config) {
name: 'Credential Library 0',
description: 'Source Description',
credential_store_id: 'csvlt_Q1HFGt7Jpm',
type: 'vault',
type: 'vault-generic',
},
secret: {
raw: 'eyJhcnJheSI6WyJvbmUiLCJ0d28iLCJ0aHJlZSIsIm9uZSIsInR3byIsInRocmVlIiwib25lIiwidHdvIiwidGhyZWUiLCJvbmUiLCJ0d28iLCJ0aHJlZSJdLCJuZXN0ZWQiOnsiYm9vbCI6dHJ1ZSwibG9uZyI6IjEyMjM1MzQ1NmFzZWRmYTQzd3J0ZjIzNGYyM2FzZGdmYXNkZnJnYXdzZWZhd3NlZnNkZjQiLCJzZWNlcmV0Ijoic28gbmVzdGVkIn0sInRlc3QiOiJwaHJhc2UifQ',
decoded: {
data: {
backslash: 'password\\withslash\\',
backslash1: 'password\\fwithslash\\f',
password: '123',
username: 'test',
email: {
@@ -127,6 +129,7 @@ export default function initializeMockIPC(server, config) {
nested_secret: {
session_token: 'ZXCVBNMLKJHGFDSAQWERTYUIOP0987654321',
complex_nest: {
blackslash: 'password\\withslash\\',
hash: 'qazxswedcvfrtgbnjhyujm.1234567890',
},
},
@@ -5,7 +5,7 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { types } from 'api/models/credential';
import { TYPES_CREDENTIAL } from 'api/models/credential';
export default class FormCredentialComponent extends Component {
// =services
@@ -18,7 +18,7 @@ export default class FormCredentialComponent extends Component {
*/
get credentialTypes() {
return this.features.isEnabled('json-credentials')
? types
: types.filter((type) => type !== 'json');
? TYPES_CREDENTIAL
: TYPES_CREDENTIAL.filter((type) => type !== 'json');
}
}
@@ -148,13 +148,125 @@ module('Acceptance | projects | sessions | session', function (hooks) {
await click(TARGET_CONNECT_BUTTON);
assert.strictEqual(currentURL(), urls.session);
assert.dom('credential-panel-header').doesNotExist();
assert.dom('credential-panel-body').doesNotExist();
assert.dom('.credential-panel-header').doesNotExist();
assert.dom('.credential-panel-body').doesNotExist();
assert
.dom('[data-test-no-credentials]')
.hasText(`Connected You can now access ${instances.target.name}`);
});
test('visiting session with vault type credentials should display nested data in a key/value format without escape characters', async function (assert) {
this.ipcStub.withArgs('cliExists').returns(true);
this.ipcStub.withArgs('connect').returns({
session_id: instances.session.id,
address: 'a_123',
port: 'p_123',
protocol: 'tcp',
credentials: [
{
credential_source: {
id: 'clvlt_4cvscMTl0N',
name: 'Credential Library 0',
description: 'Source Description',
credential_store_id: 'csvlt_Q1HFGt7Jpm',
type: 'vault-generic',
},
secret: {
raw: 'eyJhcnJheSI6WyJvbmUiLCJ0d28iLCJ0aHJlZSIsIm9uZSIsInR3byIsInRocmVlIiwib25lIiwidHdvIiwidGhyZWUiLCJvbmUiLCJ0d28iLCJ0aHJlZSJdLCJuZXN0ZWQiOnsiYm9vbCI6dHJ1ZSwibG9uZyI6IjEyMjM1MzQ1NmFzZWRmYTQzd3J0ZjIzNGYyM2FzZGdmYXNkZnJnYXdzZWZhd3NlZnNkZjQiLCJzZWNlcmV0Ijoic28gbmVzdGVkIn0sInRlc3QiOiJwaHJhc2UifQ',
decoded: {
data: {
backslash: 'password\\with\\tslash',
email: {
address: 'test.com',
},
},
metadata: {
created_time: '2024-04-12T18:38:36.226715555Z',
custom_metadata: null,
deletion_time: '',
destroyed: false,
version: 8,
},
},
},
},
],
});
await visit(urls.target);
await click(TARGET_CONNECT_BUTTON);
assert.strictEqual(currentURL(), urls.session);
assert.dom('.credential-panel-header').exists();
assert.dom('.credential-panel-body').exists();
assert.dom('.credential-secret').exists();
// Check if nested data is displayed in a key/value format without escape characters
assert.dom('.secret-container:nth-of-type(1)').includesText('backslash');
await click('.secret-container:nth-of-type(1) .hds-icon');
const expectedOutput = String.raw`password\with\tslash`;
assert
.dom('.secret-container:nth-of-type(1) .secret-content')
.hasText(expectedOutput);
assert
.dom('.secret-container:nth-of-type(2)')
.includesText('email.address');
await click('.secret-container:nth-of-type(2) .hds-icon');
assert
.dom('.secret-container:nth-of-type(2) .secret-content')
.hasText('test.com');
});
test('visiting session with static type credentials should display nested data in a key/value format without escape characters', async function (assert) {
this.ipcStub.withArgs('cliExists').returns(true);
this.ipcStub.withArgs('connect').returns({
session_id: instances.session.id,
address: 'a_123',
port: 'p_123',
protocol: 'tcp',
credentials: [
{
credential_source: {
id: 'credjson_7cKBbBEkC3',
credential_store_id: 'csst_yzlsot2pum',
type: 'static',
credential_type: 'json',
},
secret: {
raw: 'eyJhcnJheSI6WyJvbmUiLCJ0d28iLCJ0aHJlZSIsIm9uZSIsInR3byIsInRocmVlIiwib25lIiwidHdvIiwidGhyZWUiLCJvbmUiLCJ0d28iLCJ0aHJlZSJdLCJuZXN0ZWQiOnsiYm9vbCI6dHJ1ZSwibG9uZyI6IjEyMjM1MzQ1NmFzZWRmYTQzd3J0ZjIzNGYyM2FzZGdmYXNkZnJnYXdzZWZhd3NlZnNkZjQiLCJzZWNlcmV0Ijoic28gbmVzdGVkIn0sInRlc3QiOiJwaHJhc2UifQ==',
decoded: {
nested_secret: {
complex_nest: {
blackslash: 'password\\with\\tslash',
},
},
},
},
},
],
});
await visit(urls.target);
await click(TARGET_CONNECT_BUTTON);
assert.strictEqual(currentURL(), urls.session);
assert.dom('.credential-panel-header').exists();
assert.dom('.credential-panel-body').exists();
assert.dom('.credential-secret').exists();
// Check if nested data is displayed in a key/value format without escape characters
const expectedOutput = String.raw`password\with\tslash`;
assert
.dom('.secret-container:nth-of-type(1)')
.includesText('nested_secret.complex_nest.blackslash');
await click('.secret-container:nth-of-type(1) .hds-icon');
assert
.dom('.secret-container:nth-of-type(1) .secret-content')
.hasText(expectedOutput);
});
test('visiting a session that does not have permissions to read a host', async function (assert) {
assert.expect(1);
this.server.get('/hosts/:id', () => new Response(403));