chore: handle nested data from static json credentials (#2611)
This commit is contained in:
1 parent
2869020e65
commit
6de20b03ef
5 files changed
+164
-20
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user