Merge Boundary UI OSS branch 'main' at commit f0530e1f

This commit is contained in:
github-team-secure-boundary committed 2023-02-01 18:46:53 +00:00
commit 0c1543e13c
34 files changed
+1421 -182

No files matched your search

@@ -2,11 +2,11 @@ import ModelAbility from './model';
/**
* Provides abilities for credential.
*/
export default class CredentialLibaryAbility extends ModelAbility {
export default class CredentialLibraryAbility extends ModelAbility {
// =permissions
/**
* Only "known" credential libary types may be read.
* Only "known" credential library types may be read.
* @type {boolean}
*/
get canRead() {
@@ -67,4 +67,50 @@ export default class GeneratedCredentialLibraryModel extends BaseModel {
description: 'The path in Vault to request credentials from.',
})
path;
@attr('string', {
isNestedAttribute: true,
description: 'The username to use when making an SSH connection.',
})
username;
@attr('string', {
isNestedAttribute: true,
description: 'The desired key type to use when generating a private key.',
})
key_type;
@attr('number', {
isNestedAttribute: true,
description:
'Specifies the number of bits used when generating the private key. Not used if key type is ed25519',
})
key_bits;
@attr('string', {
isNestedAttribute: true,
description: 'Specifies the requested time to live for the certificate.',
})
ttl;
@attr('string', {
isNestedAttribute: true,
description:
'Specifies the key id that the created certificate should have.',
})
key_id;
@attr('object-as-array', {
isNestedAttribute: true,
description:
'Specifies a map of the critical options that the certificate should be signed for.',
})
critical_options;
@attr('object-as-array', {
isNestedAttribute: true,
description:
'Specifies a map of the extensions that the certificate should be signed for.',
})
extensions;
}
+30 -3
View File
@@ -5,12 +5,20 @@ import GeneratedCredentialLibraryModel from '../generated/models/credential-libr
*/
export const options = {
http_method: ['GET', 'POST'],
key_type: ['ed25519', 'ecdsa', 'rsa'],
};
export const TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC = 'vault-generic';
export const TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE =
'vault-ssh-certificate';
/**
* Supported Credential Library types.
*/
export const types = ['vault'];
export const TYPES_CREDENTIAL_LIBRARY = Object.freeze([
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
]);
export default class CredentialLibraryModel extends GeneratedCredentialLibraryModel {
// =attributes
@@ -20,7 +28,7 @@ export default class CredentialLibraryModel extends GeneratedCredentialLibraryMo
* @type {boolean}
*/
get isUnknown() {
return !types.includes(this.type);
return !TYPES_CREDENTIAL_LIBRARY.includes(this.type);
}
/**
@@ -28,6 +36,25 @@ export default class CredentialLibraryModel extends GeneratedCredentialLibraryMo
* @type {boolean}
*/
get isVault() {
return this.type === 'vault';
return (
this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC ||
this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE
);
}
/**
* True if credential is a generic vault type.
* @type {boolean}
*/
get isVaultGeneric() {
return this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC;
}
/**
* True if credential is a vault ssh cert type.
* @type {boolean}
*/
get isVaultSSHCertificate() {
return this.type === TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE;
}
}
@@ -1,4 +1,5 @@
import ApplicationSerializer from './application';
import { TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC } from '../models/credential-library';
export default class CredentialLibrarySerializer extends ApplicationSerializer {
// =properties
@@ -16,6 +17,10 @@ export default class CredentialLibrarySerializer extends ApplicationSerializer {
serialize() {
const serialized = super.serialize(...arguments);
if (serialized.attributes) {
if (serialized.type !== TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC) {
delete serialized.attributes.http_method;
}
// Serialize `http_request_body` only if `http_method` is POST
if (!serialized.attributes?.http_method?.match(/post/i))
delete serialized.attributes.http_request_body;
@@ -0,0 +1,22 @@
import Transform from '@ember-data/serializer/transform';
export default class ObjectAsArrayTransform extends Transform {
deserialize(serialized) {
const obj = serialized || {};
return Object.entries(obj).map(([key, value]) => ({ key, value }));
}
serialize(deserialized) {
const array = deserialized || [];
if (!array.length) {
return null;
}
return array.reduce((result, currentValue) => {
const { key, value } = currentValue;
result[key] = value;
return result;
}, {});
}
}
@@ -0,0 +1 @@
export { default } from 'api/transforms/object-as-array';
@@ -2,12 +2,15 @@ import factory from '../generated/factories/credential-library';
import { faker } from '@faker-js/faker';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
const types = ['vault'];
import {
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
TYPES_CREDENTIAL_LIBRARY,
} from 'api/models/credential-library';
export default factory.extend({
id: () => generateId('cl_'),
type: (i) => types[i % types.length],
type: (i) => TYPES_CREDENTIAL_LIBRARY[i % TYPES_CREDENTIAL_LIBRARY.length],
authorized_actions: () =>
permissions.authorizedActionsFor('credential-library') || [
@@ -19,12 +22,19 @@ export default factory.extend({
attributes() {
switch (this.type) {
case 'vault':
case TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC:
return {
http_method: 'GET',
http_request_body: faker.random.word(),
path: faker.system.directoryPath(),
};
case TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE:
return {
username: faker.random.word(),
key_type: faker.random.word(),
key_bits: faker.datatype.number(999),
path: faker.system.directoryPath(),
};
}
},
});
@@ -1,5 +1,6 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC } from 'api/models/credential-library';
module('Unit | Model | credential library', function (hooks) {
setupTest(hooks);
@@ -13,7 +14,9 @@ module('Unit | Model | credential library', function (hooks) {
test('it has isUnknown property and returns the expected values', async function (assert) {
assert.expect(2);
const store = this.owner.lookup('service:store');
const modelA = store.createRecord('credential-library', { type: 'vault' });
const modelA = store.createRecord('credential-library', {
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
});
const modelB = store.createRecord('credential-library', {
type: 'unknown',
});
@@ -1,5 +1,9 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
} from 'api/models/credential-library';
module('Unit | Serializer | credential library', function (hooks) {
setupTest(hooks);
@@ -9,7 +13,7 @@ module('Unit | Serializer | credential library', function (hooks) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('credential-library');
const record = store.createRecord('credential-library', {
type: 'vault',
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
name: 'Name',
description: 'Description',
path: '/vault/path',
@@ -19,7 +23,7 @@ module('Unit | Serializer | credential library', function (hooks) {
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
type: 'vault',
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
credential_store_id: null,
name: 'Name',
description: 'Description',
@@ -27,6 +31,13 @@ module('Unit | Serializer | credential library', function (hooks) {
attributes: {
path: '/vault/path',
http_method: 'GET',
critical_options: null,
extensions: null,
key_bits: null,
key_id: null,
key_type: null,
ttl: null,
username: null,
},
});
});
@@ -40,7 +51,7 @@ module('Unit | Serializer | credential library', function (hooks) {
id: '1',
type: 'credential-library',
attributes: {
type: 'vault',
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
name: 'Name',
description: 'Description',
path: '/vault/path',
@@ -53,13 +64,20 @@ module('Unit | Serializer | credential library', function (hooks) {
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
type: 'vault',
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
credential_store_id: null,
name: 'Name',
description: 'Description',
attributes: {
path: '/vault/path',
http_method: 'GET',
critical_options: null,
extensions: null,
key_bits: null,
key_id: null,
key_type: null,
ttl: null,
username: null,
},
version: 1,
});
@@ -75,8 +93,14 @@ module('Unit | Serializer | credential library', function (hooks) {
let serializedRecord = record.serialize();
assert.deepEqual(serializedRecord, {
attributes: {
http_method: null,
path: null,
critical_options: null,
extensions: null,
key_bits: null,
key_id: null,
key_type: null,
ttl: null,
username: null,
},
credential_store_id: null,
description: null,
@@ -89,6 +113,7 @@ module('Unit | Serializer | credential library', function (hooks) {
assert.expect(2);
const store = this.owner.lookup('service:store');
const record = store.createRecord('credential-library', {
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
http_method: 'GET',
http_request_body: 'body',
});
@@ -99,11 +124,18 @@ module('Unit | Serializer | credential library', function (hooks) {
attributes: {
http_method: 'GET',
path: null,
critical_options: null,
extensions: null,
key_bits: null,
key_id: null,
key_type: null,
ttl: null,
username: null,
},
credential_store_id: null,
description: null,
name: null,
type: null,
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
},
'http_request_body attribute is not expected'
);
@@ -117,13 +149,66 @@ module('Unit | Serializer | credential library', function (hooks) {
http_method: 'POST',
http_request_body: 'body',
path: null,
critical_options: null,
extensions: null,
key_bits: null,
key_id: null,
key_type: null,
ttl: null,
username: null,
},
credential_store_id: null,
description: null,
name: null,
type: null,
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
},
'http_request_body attribute is expected'
);
});
test('it serializes vault_ssh_certificate correctly', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('credential-library');
store.push({
data: {
id: '1',
type: 'credential-library',
attributes: {
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
version: 1,
name: 'Name',
description: 'Description',
path: '/vault/path',
username: 'user',
key_type: 'rsa',
key_bits: 100,
ttl: '100',
key_id: 'id',
extensions: [{ key: 'key', value: 'value' }],
critical_options: [{ key: 'key', value: 'value' }],
},
},
});
const record = store.peekRecord('credential-library', '1');
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
credential_store_id: null,
name: 'Name',
description: 'Description',
attributes: {
path: '/vault/path',
username: 'user',
key_type: 'rsa',
key_bits: 100,
ttl: '100',
key_id: 'id',
extensions: { key: 'value' },
critical_options: { key: 'value' },
},
version: 1,
});
});
});
@@ -0,0 +1,52 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Transform | object as array', function (hooks) {
setupTest(hooks);
test('it deserializes an object to an array of key value objects', function (assert) {
assert.expect(1);
let transform = this.owner.lookup('transform:object-as-array');
const deserialized = transform.deserialize({
first: 'Hey',
second: 'There',
third: 'Everyone',
});
assert.deepEqual(deserialized, [
{ key: 'first', value: 'Hey' },
{ key: 'second', value: 'There' },
{ key: 'third', value: 'Everyone' },
]);
});
test('it deserializes undefined to an empty object', function (assert) {
assert.expect(1);
let transform = this.owner.lookup('transform:object-as-array');
const deserialized = transform.deserialize(undefined);
assert.deepEqual(deserialized, []);
});
test('it serializes an array of key value objects to an object', function (assert) {
assert.expect(1);
let transform = this.owner.lookup('transform:object-as-array');
const serialized = transform.serialize([
{ key: 'first', value: 'Hey' },
{ key: 'second', value: 'There' },
{ key: 'third', value: 'Everyone' },
]);
assert.deepEqual(serialized, {
first: 'Hey',
second: 'There',
third: 'Everyone',
});
});
test('it serializes an empty array of key value objects to null', function (assert) {
assert.expect(1);
let transform = this.owner.lookup('transform:object-as-array');
const serialized = transform.serialize([]);
assert.deepEqual(serialized, null);
});
});
+1
View File
@@ -34,6 +34,7 @@ titles:
provider: Provider
success: Success
create-resources: Create Resources
choose-an-option: Choose an option
descriptions:
empty-set: There are no items to display yet. You may be able to add items or try back later.
cluster-url-initialization: To get started, please enter your cluster URL
+26 -2
View File
@@ -468,9 +468,33 @@ credential-library:
http_request_body:
label: HTTP Method POST Request Body
help: The body of the HTTP request the library sends to Vault when requesting credentials. Only allowed when HTTP method is set to POST.
username:
label: Username
help: The username to use when making an SSH connection. This can be templated to use the requesting user's name. This will be included in the valid_principles when making the request to Vault.
key_type:
label: Key Type
help: Specifies the desired key type to use when generating a private key.
key_bits:
label: Key Bits
help: Specifies the number of bits used generating the private key.
ttl:
label: TTL
help: Specifies the Requested Time to Live for the certificate.
key_id:
label: Key ID
help: Specifies the key id that the created certificate should have.
critical_options:
label: Critical Options
help: Specifies a map of the critical options that the certificate should be signed for.
extensions:
label: Extensions
help: Specifies a map of the extensions that the certificate should be signed for.
types:
vault: Vault
static: Static
vault-generic: Generic Secrets
vault-ssh-certificate: SSH Certificates
help:
vault-generic: Authorize sessions using credentials brokered by Vault
vault-ssh-certificate: Authorize sessions using the SSH Secrets Engine in Vault
credential:
title: Credential
title_plural: Credentials
+1 -1
View File
@@ -34,7 +34,7 @@
},
"dependencies": {
"@embroider/test-setup": "^0.48.1",
"@hashicorp/design-system-components": "^1.3.2",
"@hashicorp/design-system-components": "^1.5.1",
"codemirror": "5.65.7",
"ember-auto-import": "^2.4.2",
"ember-cli-babel": "^7.26.10",
@@ -0,0 +1,20 @@
import CredentialLibraryAbility from 'api/abilities/credential-library';
import { inject as service } from '@ember/service';
export default class OverrideCredentialLibraryAbility extends CredentialLibraryAbility {
// =service
@service features;
/**
* This override ensures that vault ssh cert may be read only if the
* credential-library-vault-ssh-certificate feature flag is enabled.
* All other types are subject to the standard logic found in the api addon.
*/
get canRead() {
return this.features.isEnabled(
'credential-library-vault-ssh-certificate'
) || !this.model.isVaultSSHCertificate
? super.canRead
: false;
}
}
@@ -1,129 +1,10 @@
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<form.input
@value={{@model.type}}
@label={{t 'form.type.label'}}
readonly={{true}}
@disabled={{true}}
/>
<form.input
@name='name'
@type='name'
@value={{@model.name}}
@label={{t 'form.name.label'}}
@error={{@model.errors.name}}
@helperText={{t 'form.name.help'}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.name}}
<field.errors as |errors|>
{{#each @model.errors.name as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
<form.textarea
@name='description'
@type='description'
@value={{@model.description}}
@label={{t 'form.description.label'}}
@error={{@model.errors.description}}
@helperText={{t 'form.description.help'}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.description}}
<field.errors as |errors|>
{{#each @model.errors.description as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.textarea>
<form.input
@name='vault_path'
@type='text'
@value={{@model.path}}
@label={{t 'resources.credential-library.form.vault_path.label'}}
@error={{@model.errors.path}}
@helperText={{t 'resources.credential-library.form.vault_path.help'}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.path}}
<field.errors as |errors|>
{{#each @model.errors.path as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
<form.fieldset>
<:title>{{t 'resources.credential-library.form.http_method.label'}}</:title>
<:description>
{{t 'resources.credential-library.form.http_method.help'}}
</:description>
<:body>
<form.radioGroup
@name='http_method'
@selectedValue={{@model.http_method}}
@changed={{fn (mut @model.http_method)}}
@inline={{true}}
disabled={{@model.isSaving}}
as |radioGroup|
>
{{#each this.httpMethodOptions as |httpMethod|}}
<radioGroup.radio @label={{httpMethod}} @value={{httpMethod}} />
{{/each}}
</form.radioGroup>
</:body>
</form.fieldset>
{{#if this.isHttpRequestBodyAllowed}}
<form.textarea
@name='http_request_body'
@type='text'
@value={{@model.http_request_body}}
@label={{t 'resources.credential-library.form.http_request_body.label'}}
@error={{@model.errors.http_request_body}}
@helperText={{t
'resources.credential-library.form.http_request_body.help'
}}
@disabled={{this.cannotSaveHttpRequestBody}}
readonly={{false}}
as |field|
>
{{#if @model.errors.http_request_body}}
<field.errors as |errors|>
{{#each @model.errors.http_request_body as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.textarea>
{{/if}}
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
{{#if @model.type}}
{{component
(concat 'form/credential-library/' @model.type)
model=@model
submit=@submit
cancel=@cancel
changeType=@changeType
edit=@edit
}}
{{/if}}
@@ -0,0 +1,169 @@
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<Hds::Form::TextInput::Field
@isRequired={{false}}
@isOptional={{true}}
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
@type='text'
name='name'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
>
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
{{#if @model.errors.name}}
<F.Error as |E|>
{{#each @model.errors.name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field
@isRequired={{false}}
@isOptional={{true}}
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
name='description'
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
{{#if
(and @model.isNew (feature-flag 'credential-library-vault-ssh-certificate'))
}}
<Hds::Form::RadioCard::Group @name='type' @alignment='center' as |G|>
<G.Legend>{{t 'form.type.label'}}</G.Legend>
{{#each this.types as |type|}}
<G.RadioCard
@value={{type}}
@maxWidth='20rem'
@checked={{eq type @model.type}}
{{on 'input' (fn @changeType type)}}
as |R|
>
<R.Label>{{t
(concat 'resources.credential-library.types.' type)
}}</R.Label>
<R.Description>{{t
(concat 'resources.credential-library.help.' type)
}}</R.Description>
</G.RadioCard>
{{/each}}
</Hds::Form::RadioCard::Group>
{{else}}
<InfoField
@value={{t (concat 'resources.credential-library.types.' @model.type)}}
as |F|
>
<F.Label>{{t 'form.type.label'}}</F.Label>
<F.HelperText>
{{t (concat 'resources.credential-library.help.' @model.type)}}
</F.HelperText>
</InfoField>
{{/if}}
<Hds::Form::TextInput::Field
@isRequired={{true}}
@isOptional={{false}}
@value={{@model.path}}
@isInvalid={{@model.errors.path}}
@type='text'
name='vault_path'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'path')}}
as |F|
>
<F.Label>{{t
'resources.credential-library.form.vault_path.label'
}}</F.Label>
<F.HelperText>{{t
'resources.credential-library.form.vault_path.help'
}}</F.HelperText>
{{#if @model.errors.path}}
<F.Error as |E|>
{{#each @model.errors.path as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Radio::Group
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'http_method')}}
@isRequired={{true}}
@layout='horizontal'
@name='http_method'
as |G|
>
<G.Legend>
{{t 'resources.credential-library.form.http_method.label'}}
</G.Legend>
<G.HelperText>
{{t 'resources.credential-library.form.http_method.help'}}
</G.HelperText>
{{#each this.httpMethodOptions as |httpMethod|}}
<G.Radio::Field
checked={{eq @model.http_method httpMethod}}
@value={{httpMethod}}
as |F|
>
<F.Label>{{httpMethod}}</F.Label>
</G.Radio::Field>
{{/each}}
</Hds::Form::Radio::Group>
{{#if this.isHttpRequestBodyAllowed}}
<Hds::Form::Textarea::Field
@isRequired={{true}}
@isOptional={{false}}
@value={{@model.http_request_body}}
@isInvalid={{@model.errors.http_request_body}}
name='http_request_body'
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t
'resources.credential-library.form.http_request_body.label'
}}</F.Label>
<F.HelperText>{{t
'resources.credential-library.form.http_request_body.help'
}}</F.HelperText>
{{#if @model.errors.http_request_body}}
<F.Error as |E|>
{{#each @model.errors.http_request_body as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
{{/if}}
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -1,19 +1,26 @@
import Component from '@glimmer/component';
import { options } from 'api/models/credential-library';
import { computed } from '@ember/object';
import {
options,
TYPES_CREDENTIAL_LIBRARY,
} from 'api/models/credential-library';
export default class FormCredentialLibraryIndexComponent extends Component {
export default class FormCredentialLibraryVaultGenericComponent extends Component {
// =properties
/**
* @type {object}
*/
httpMethodOptions = options.http_method;
/**
*
* @type {Array.<string>}
*/
types = TYPES_CREDENTIAL_LIBRARY;
/**
* Only allow HTTP request body field if http_method is set to POST.
* @type {boolean}
*/
@computed('args.model.http_method')
get isHttpRequestBodyAllowed() {
return this.args.model.http_method?.match(/post/i);
}
@@ -0,0 +1,292 @@
<Rose::Form
@edit={{@edit}}
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<Hds::Form::TextInput::Field
@isOptional={{true}}
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
@type='text'
name='name'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
>
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
{{#if @model.errors.name}}
<F.Error as |E|>
{{#each @model.errors.name as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field
@isOptional={{true}}
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
name='description'
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
{{#if @model.isNew}}
<Hds::Form::RadioCard::Group @name='type' @alignment='center' as |G|>
<G.Legend>{{t 'form.type.label'}}</G.Legend>
{{#each this.types as |type|}}
<G.RadioCard
@value={{type}}
@maxWidth='20rem'
@checked={{eq type @model.type}}
{{on 'input' (fn @changeType type)}}
as |R|
>
<R.Label>{{t
(concat 'resources.credential-library.types.' type)
}}</R.Label>
<R.Description>{{t
(concat 'resources.credential-library.help.' type)
}}</R.Description>
</G.RadioCard>
{{/each}}
</Hds::Form::RadioCard::Group>
{{else}}
<InfoField
@value={{t (concat 'resources.credential-library.types.' @model.type)}}
as |F|
>
<F.Label>{{t 'form.type.label'}}</F.Label>
<F.HelperText>
{{t (concat 'resources.credential-library.help.' @model.type)}}
</F.HelperText>
</InfoField>
{{/if}}
<Hds::Form::TextInput::Field
@isRequired={{true}}
@value={{@model.path}}
@isInvalid={{@model.errors.path}}
@type='text'
name='vault_path'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'path')}}
as |F|
>
<F.Label>{{t
'resources.credential-library.form.vault_path.label'
}}</F.Label>
<F.HelperText>{{t
'resources.credential-library.form.vault_path.help'
}}</F.HelperText>
{{#if @model.errors.path}}
<F.Error as |E|>
{{#each @model.errors.path as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::TextInput::Field
@isRequired={{true}}
@value={{@model.username}}
@isInvalid={{@model.errors.username}}
@type='text'
name='username'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'username')}}
as |F|
>
<F.Label>{{t 'resources.credential-library.form.username.label'}}</F.Label>
<F.HelperText>{{t
'resources.credential-library.form.username.help'
}}</F.HelperText>
{{#if @model.errors.username}}
<F.Error as |E|>
{{#each @model.errors.username as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Select::Field
@isOptional={{true}}
@width='100%'
name='key_type'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'key_type')}}
as |F|
>
<F.Label>{{t 'resources.credential-library.form.key_type.label'}}</F.Label>
<F.HelperText>{{t
'resources.credential-library.form.key_type.help'
}}</F.HelperText>
<F.Options>
<option disabled hidden selected value=''>{{t
'titles.choose-an-option'
}}</option>
{{#each this.keyTypes as |keyType|}}
<option
value={{keyType}}
selected={{eq keyType @model.key_type}}
>{{keyType}}</option>
{{/each}}
</F.Options>
{{#if @model.errors.key_type}}
<F.Error as |E|>
{{#each @model.errors.key_type as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Select::Field>
{{#if this.showKeyBits}}
<Hds::Form::TextInput::Field
@isOptional={{true}}
@value={{@model.key_bits}}
@isInvalid={{@model.errors.key_bits}}
@type='text'
name='key_bits'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'key_bits')}}
as |F|
>
<F.Label>{{t
'resources.credential-library.form.key_bits.label'
}}</F.Label>
<F.HelperText>{{t
'resources.credential-library.form.key_bits.help'
}}</F.HelperText>
{{#if @model.errors.key_bits}}
<F.Error as |E|>
{{#each @model.errors.key_bits as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{/if}}
<Hds::Form::TextInput::Field
@isOptional={{true}}
@value={{@model.ttl}}
@isInvalid={{@model.errors.ttl}}
@type='text'
name='ttl'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'ttl')}}
as |F|
>
<F.Label>{{t 'resources.credential-library.form.ttl.label'}}</F.Label>
<F.HelperText>{{t
'resources.credential-library.form.ttl.help'
}}</F.HelperText>
{{#if @model.errors.ttl}}
<F.Error as |E|>
{{#each @model.errors.ttl as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::TextInput::Field
@isOptional={{true}}
@value={{@model.key_id}}
@isInvalid={{@model.errors.key_id}}
@type='text'
name='key_id'
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'key_id')}}
as |F|
>
<F.Label>{{t 'resources.credential-library.form.key_id.label'}}</F.Label>
<F.HelperText>{{t
'resources.credential-library.form.key_id.help'
}}</F.HelperText>
{{#if @model.errors.key_id}}
<F.Error as |E|>
{{#each @model.errors.key_id as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Form::Field::KeyValueList
@layout='horizontal'
@isOptional={{true}}
@name='critical_options'
@options={{@model.critical_options}}
@addOption={{fn this.addOption 'critical_options'}}
@removeOptionByIndex={{fn this.removeOptionByIndex 'critical_options'}}
@disabled={{form.disabled}}
as |M|
>
<M.Legend>
{{t 'resources.credential-library.form.critical_options.label'}}
</M.Legend>
<M.HelperText>
{{t 'resources.credential-library.form.critical_options.help'}}
</M.HelperText>
{{#if @model.errors.critical_options}}
<M.Error as |E|>
{{#each @model.errors.critical_options as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</M.Error>
{{/if}}
</Form::Field::KeyValueList>
<Form::Field::KeyValueList
@layout='horizontal'
@isOptional={{true}}
@name='extensions'
@options={{@model.extensions}}
@addOption={{fn this.addOption 'extensions'}}
@removeOptionByIndex={{fn this.removeOptionByIndex 'extensions'}}
@disabled={{form.disabled}}
as |M|
>
<M.Legend>
{{t 'resources.credential-library.form.extensions.label'}}
</M.Legend>
<M.HelperText>
{{t 'resources.credential-library.form.extensions.help'}}
</M.HelperText>
{{#if @model.errors.extensions}}
<M.Error as |E|>
{{#each @model.errors.extensions as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</M.Error>
{{/if}}
</Form::Field::KeyValueList>
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -0,0 +1,55 @@
import Component from '@glimmer/component';
import { action, set } from '@ember/object';
import {
options,
TYPES_CREDENTIAL_LIBRARY,
} from 'api/models/credential-library';
export default class FormCredentialLibraryVaultSshCertComponent extends Component {
// =properties
/**
* @type {object}
*/
keyTypes = options.key_type;
/**
* @type {Array.<string>}
*/
types = TYPES_CREDENTIAL_LIBRARY;
/**
* Boolean to determine if the key bits field should be displayed on the form
* @returns {boolean}
*/
get showKeyBits() {
const keyType = this.args.model.key_type;
return keyType === 'rsa' || keyType === 'ecdsa';
}
/**
* Adds a key/value option object. We recreate a new array after adding
* so that ember is aware that the array has been modified.
* @param field {string}
* @param key {string}
* @param value {string}
*/
@action
addOption(field, { key, value }) {
const existingArray = this.args.model[field] ?? [];
const newArray = [...existingArray, { key, value }];
set(this.args.model, field, newArray);
}
/**
* Removes an option by index. We recreate a new array after
* splicing out the item so that ember is aware that the array has been modified.
* @param field {string}
* @param index {number}
*/
@action
removeOptionByIndex(field, index) {
const newArray = this.args.model[field].filter((_, i) => i !== index);
set(this.args.model, field, newArray);
}
}
@@ -0,0 +1,98 @@
<Hds::Form::Fieldset
@layout={{@layout}}
@isRequired={{@isRequired}}
@isOptional={{@isOptional}}
as |F|
>
{{yield F}}
<F.Control>
<Hds::Table class='key-value-list' name={{@name}}>
<:head as |H|>
<H.Tr>
<H.Th>{{t 'form.key.label'}}</H.Th>
<H.Th>{{t 'form.value.label'}}</H.Th>
<H.Th>
{{#if @removeOptionByIndex}}
{{t 'titles.actions'}}
{{/if}}
</H.Th>
</H.Tr>
</:head>
<:body as |B|>
{{#each @options as |option index|}}
<B.Tr>
<B.Td>
<Hds::Form::TextInput::Field
@value={{option.key}}
@type='text'
disabled={{@disabled}}
{{on 'input' (set-from-event option 'key')}}
as |F|
>
<F.Label class='sr-only'>{{t 'form.key.label'}}</F.Label>
</Hds::Form::TextInput::Field>
</B.Td>
<B.Td>
<Hds::Form::TextInput::Field
@value={{option.value}}
@type='text'
disabled={{@disabled}}
{{on 'input' (set-from-event option 'value')}}
as |F|
>
<F.Label class='sr-only'>{{t 'form.value.label'}}</F.Label>
</Hds::Form::TextInput::Field>
</B.Td>
<B.Td>
{{#if @removeOptionByIndex}}
<Hds::Button
@text={{t 'actions.remove'}}
@color='critical'
@icon='trash'
@isIconOnly={{true}}
disabled={{@disabled}}
{{on 'click' (fn @removeOptionByIndex index)}}
/>
{{/if}}
</B.Td>
</B.Tr>
{{/each}}
<B.Tr>
<B.Td>
<Hds::Form::TextInput::Field
@value={{this.newOptionKey}}
@type='text'
disabled={{@disabled}}
{{on 'input' (set-from-event this 'newOptionKey')}}
as |F|
>
<F.Label class='sr-only'>{{t 'form.key.label'}}</F.Label>
</Hds::Form::TextInput::Field></B.Td>
<B.Td>
<Hds::Form::TextInput::Field
@value={{this.newOptionValue}}
@type='text'
disabled={{@disabled}}
{{on 'input' (set-from-event this 'newOptionValue')}}
as |F|
>
<F.Label class='sr-only'>{{t 'form.value.label'}}</F.Label>
</Hds::Form::TextInput::Field>
</B.Td>
<B.Td>
<Hds::Button
@text={{t 'actions.add'}}
@color='secondary'
type='button'
disabled={{or @disabled (not this.newOptionKey)}}
{{on 'click' this.addOption}}
/>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
</F.Control>
</Hds::Form::Fieldset>
@@ -0,0 +1,42 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { assert } from '@ember/debug';
export default class MappingListComponent extends Component {
// =attributes
/**
* @type {string}
*/
@tracked newOptionKey = '';
/**
* @type {string}
*/
@tracked newOptionValue = '';
// =actions
/**
* If a new key value is entered and an addOption method was specified,
* calls addOption with the new key and value. Resets key and value.
*/
@action
addOption() {
assert(
'[boundary-admin-key-value-list-field] `@addOption` is required.',
this.args.addOption
);
if (this.newOptionKey) {
this.args.addOption({
key: this.newOptionKey,
value: this.newOptionValue,
});
}
this.newOptionKey = '';
this.newOptionValue = '';
}
}
@@ -7,6 +7,7 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
@service intl;
// =attributes
queryParams = ['type'];
/**
* Translated new credential libraries breadcrumb
@@ -1,5 +1,6 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesCredentialLibraryRoute extends Route {
// =services
@@ -19,4 +20,21 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
reload: true,
});
}
/**
* Copies the contents of array fields in order to force the instance
* into a dirty state. This ensures that `model.rollbackAttributes()` reverts
* to the original expected array.
*
* The deep copy implemented here is required to ensure that both the
* array itself and its members are all new.
*
* @param {credentialLibraryModel} credentialLibrary
*/
@action
edit(credentialLibrary) {
const { critical_options, extensions } = credentialLibrary;
credentialLibrary.critical_options = structuredClone(critical_options);
credentialLibrary.extensions = structuredClone(extensions);
}
}
@@ -1,10 +1,22 @@
import Route from '@ember/routing/route';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC } from 'api/models/credential-library';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesNewRoute extends Route {
// =services
@service store;
@service router;
@service features;
// =attributes
queryParams = {
type: {
refreshModel: true,
},
};
// =methods
@@ -12,13 +24,29 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
* Creates a new unsaved credential library in current credential store.
* @return {CredentialLibraryModel}
*/
model() {
const { id: credential_store_id, type } = this.modelFor(
model({ type = TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC }) {
const { id: credential_store_id } = this.modelFor(
'scopes.scope.credential-stores.credential-store'
);
// Set the type to generic vault if feature flag isn't enabled in cases where
// user sets the query parameter manually
if (!this.features.isEnabled('credential-library-vault-ssh-certificate')) {
type = TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC;
}
return this.store.createRecord('credential-library', {
type,
credential_store_id,
});
}
/**
* Update type of credential library
* @param {string} type
*/
@action
changeType(type) {
this.router.replaceWith({ queryParams: { type } });
}
}
+44
View File
@@ -637,3 +637,47 @@
margin-top: sizing.rems(xs) - sizing.rems(xxxxs); // 10
}
}
.key-value-list {
border: 0;
/* stylelint-disable selector-class-pattern */
.hds-table__thead {
.hds-table__th {
height: auto;
line-height: 1;
}
}
.hds-table__thead,
.hds-table__tbody {
.hds-table__tr {
// Remove background color on all cells (especially header cells)
background-color: transparent;
}
.hds-table__th,
.hds-table__td {
padding: 0.75rem 1rem;
// Rely on right padding for cell spacing...
padding-left: 0;
&:last-child {
// ...except in the last cell, which doesn't need right padding
padding-right: 0;
// Shrink last cell to minimum possible width
width: 1%;
white-space: nowrap;
}
// Reset field margins to rely instead on cell paddings
.hds-form-field--layout-vertical,
.hds-form-text-input {
margin: 0;
}
}
}
/* stylelint-enable selector-class-pattern */
}
@@ -50,6 +50,7 @@
<page.body>
<Form::CredentialLibrary
@model={{@model}}
@edit={{route-action 'edit' @model}}
@submit={{route-action 'save' @model}}
@cancel={{route-action 'cancel' @model}}
/>
@@ -19,6 +19,7 @@
@model={{@model}}
@submit={{route-action 'save' @model}}
@cancel={{route-action 'cancel' @model}}
@changeType={{route-action 'changeType'}}
/>
</page.body>
</Rose::Layout::Page>
+4 -3
View File
@@ -15,7 +15,7 @@ const featureEditions = {
'vault-worker-filter': false,
'target-worker-filters-v2': true,
'target-network-address': true,
'credential-library-vault-ssh-cert': false,
'credential-library-vault-ssh-certificate': false,
},
};
featureEditions.enterprise = {
@@ -23,6 +23,7 @@ featureEditions.enterprise = {
'ssh-target': true,
'vault-worker-filter': true,
'target-worker-filters-v2-ingress': true,
'credential-library-vault-ssh-certificate': true,
};
featureEditions.hcp = {
...featureEditions.enterprise,
@@ -152,7 +153,7 @@ module.exports = function (environment) {
ENV.featureFlags['target-worker-filters-v2-ingress'] = true;
ENV.featureFlags['target-worker-filters-v2-hcp'] = true;
ENV.featureFlags['target-network-address'] = true;
ENV.featureFlags['credential-library-vault-ssh-cert'] = true;
ENV.featureFlags['credential-library-vault-ssh-certificate'] = true;
}
if (environment === 'test') {
@@ -178,7 +179,7 @@ module.exports = function (environment) {
ENV.featureFlags['vault-worker-filter'] = true;
ENV.featureFlags['target-worker-filters-v2'] = true;
ENV.featureFlags['target-network-address'] = true;
ENV.featureFlags['credential-library-vault-ssh-cert'] = true;
ENV.featureFlags['credential-library-vault-ssh-certificate'] = true;
}
if (environment === 'production') {
@@ -1,10 +1,18 @@
import { module, test } from 'qunit';
import { visit, click, fillIn, currentURL, find } from '@ember/test-helpers';
import {
visit,
click,
fillIn,
currentURL,
find,
select,
} from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { Response } from 'miragejs';
import { TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE } from 'api/models/credential-library';
module('Acceptance | credential-libraries | create', function (hooks) {
setupApplicationTest(hooks);
@@ -83,6 +91,76 @@ module('Acceptance | credential-libraries | create', function (hooks) {
assert.strictEqual(getCredentialLibraryCount(), count + 1);
});
test('can create a new credential library of type vault ssh cert', async function (assert) {
assert.expect(12);
const count = getCredentialLibraryCount();
await visit(urls.newCredentialLibrary);
await click('[value="vault-ssh-certificate"]');
await fillIn('[name="name"]', 'name');
await fillIn('[name="description"]', 'description');
await fillIn('[name="vault_path"]', 'path');
await fillIn('[name="username"]', 'username');
await select('[name="key_type"]', 'rsa');
await fillIn('[name="key_bits"]', 100);
await fillIn('[name="ttl"]', 'ttl');
await fillIn('[name="key_id"]', 'key_id');
await fillIn(
'[name="critical_options"] tbody td:nth-of-type(1) input',
'co_key'
);
await fillIn(
'[name="critical_options"] tbody td:nth-of-type(2) input',
'co_value'
);
await click('[name="critical_options"] button');
await fillIn(
'[name="extensions"] tbody td:nth-of-type(1) input',
'ext_key'
);
await fillIn(
'[name="extensions"] tbody td:nth-of-type(2) input',
'ext_value'
);
await click('[name="extensions"] button');
await click('[type="submit"]');
assert.strictEqual(getCredentialLibraryCount(), count + 1);
assert.strictEqual(
this.server.schema.credentialLibraries.where({
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
}).length,
1
);
const credentialLibrary = this.server.schema.credentialLibraries.findBy({
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
});
assert.strictEqual(credentialLibrary.name, 'name');
assert.strictEqual(credentialLibrary.description, 'description');
assert.strictEqual(credentialLibrary.attributes.path, 'path');
assert.strictEqual(credentialLibrary.attributes.username, 'username');
assert.strictEqual(credentialLibrary.attributes.key_type, 'rsa');
assert.strictEqual(credentialLibrary.attributes.key_bits, 100);
assert.strictEqual(credentialLibrary.attributes.ttl, 'ttl');
assert.strictEqual(credentialLibrary.attributes.key_id, 'key_id');
assert.deepEqual(credentialLibrary.attributes.critical_options, {
co_key: 'co_value',
});
assert.deepEqual(credentialLibrary.attributes.extensions, {
ext_key: 'ext_value',
});
});
test('ecdsa and rsa key types bring up a key bits field', async function (assert) {
assert.expect(3);
await visit(urls.newCredentialLibrary);
await click('[value="vault-ssh-certificate"]');
await select('[name="key_type"]', 'ed25519');
assert.dom('[name="key_bits"]').doesNotExist();
await select('[name="key_type"]', 'ecdsa');
assert.dom('[name="key_bits"]').isVisible();
await select('[name="key_type"]', 'rsa');
assert.dom('[name="key_bits"]').isVisible();
});
test('Users cannot navigate to new credential library route without proper authorization', async function (assert) {
assert.expect(2);
instances.credentialStore.authorized_collection_actions[
@@ -107,7 +185,7 @@ module('Acceptance | credential-libraries | create', function (hooks) {
assert.strictEqual(getCredentialLibraryCount(), count);
});
test('saving a new credential library with invalid fields displays error messasges', async function (assert) {
test('saving a new credential library with invalid fields displays error messages', async function (assert) {
assert.expect(2);
this.server.post('/credential-libraries', () => {
return new Response(
@@ -135,8 +213,17 @@ module('Acceptance | credential-libraries | create', function (hooks) {
'The request was invalid.'
);
assert.ok(
find('.rose-form-error-message').textContent.trim(),
find('.hds-form-error__message').textContent.trim(),
'Name is required.'
);
});
test('cannot select vault ssh cert when feature is disabled', async function (assert) {
const featuresService = this.owner.lookup('service:features');
featuresService.disable('credential-library-vault-ssh-certificate');
assert.expect(1);
await visit(urls.newCredentialLibrary);
assert.dom('[value="vault-ssh-certificate"]').doesNotExist();
});
});
@@ -4,6 +4,7 @@ import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE } from 'api/models/credential-library';
module('Acceptance | credential-libraries | read', function (hooks) {
setupApplicationTest(hooks);
@@ -78,6 +79,23 @@ module('Acceptance | credential-libraries | read', function (hooks) {
assert.notOk(find('main tbody .rose-table-header-cell a'));
});
test('cannot navigate to vault ssh cert form when feature is not enabled', async function (assert) {
assert.expect(1);
instances.credentialLibrary = this.server.create('credential-library', {
scope: instances.scopes.project,
credentialStore: instances.credentialStore,
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
});
await visit(
`${urls.credentialLibraries}/${instances.credentialLibrary.id}`
);
const featuresService = this.owner.lookup('service:features');
featuresService.disable('credential-library-vault-ssh-certificate');
await visit(urls.credentialLibraries);
assert.dom('.rose-table-body tr:nth-of-type(2) a').doesNotExist();
});
test('visiting an unknown credential library displays 404 message', async function (assert) {
assert.expect(1);
await visit(urls.unknownCredentialLibrary);
@@ -1,9 +1,17 @@
import { module, test } from 'qunit';
import { visit, click, fillIn, currentURL, find } from '@ember/test-helpers';
import {
visit,
click,
fillIn,
currentURL,
find,
select,
} from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { Response } from 'miragejs';
import { TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE } from 'api/models/credential-library';
module('Acceptance | credential-libraries | update', function (hooks) {
setupApplicationTest(hooks);
@@ -130,7 +138,7 @@ module('Acceptance | credential-libraries | update', function (hooks) {
'The request was invalid.'
);
assert.ok(
find('.rose-form-error-message').textContent.trim(),
find('.hds-form-error__message').textContent.trim(),
'Name is required.'
);
});
@@ -181,4 +189,61 @@ module('Acceptance | credential-libraries | update', function (hooks) {
);
}
});
test('can update a vault ssh cert credential library and save changes', async function (assert) {
assert.expect(10);
instances.credentialLibrary = this.server.create('credential-library', {
scope: instances.scopes.project,
credentialStore: instances.credentialStore,
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
});
await visit(
`${urls.credentialLibraries}/${instances.credentialLibrary.id}`
);
await click('form [type="button"]:not(:disabled)', 'Activate edit mode');
await fillIn('[name="name"]', 'name');
await fillIn('[name="description"]', 'description');
await fillIn('[name="vault_path"]', 'path');
await fillIn('[name="username"]', 'username');
await select('[name="key_type"]', 'rsa');
await fillIn('[name="key_bits"]', 100);
await fillIn('[name="ttl"]', 'ttl');
await fillIn('[name="key_id"]', 'key_id');
await fillIn(
'[name="critical_options"] tbody td:nth-of-type(1) input',
'co_key'
);
await fillIn(
'[name="critical_options"] tbody td:nth-of-type(2) input',
'co_value'
);
await click('[name="critical_options"] button');
await fillIn(
'[name="extensions"] tbody td:nth-of-type(1) input',
'ext_key'
);
await fillIn(
'[name="extensions"] tbody td:nth-of-type(2) input',
'ext_value'
);
await click('[name="extensions"] button');
await click('[type="submit"]');
const credentialLibrary = this.server.schema.credentialLibraries.findBy({
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
});
assert.strictEqual(credentialLibrary.name, 'name');
assert.strictEqual(credentialLibrary.description, 'description');
assert.strictEqual(credentialLibrary.attributes.path, 'path');
assert.strictEqual(credentialLibrary.attributes.username, 'username');
assert.strictEqual(credentialLibrary.attributes.key_type, 'rsa');
assert.strictEqual(credentialLibrary.attributes.key_bits, 100);
assert.strictEqual(credentialLibrary.attributes.ttl, 'ttl');
assert.strictEqual(credentialLibrary.attributes.key_id, 'key_id');
assert.deepEqual(credentialLibrary.attributes.critical_options, {
co_key: 'co_value',
});
assert.deepEqual(credentialLibrary.attributes.extensions, {
ext_key: 'ext_value',
});
});
});
@@ -0,0 +1,44 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupIntl } from 'ember-intl/test-support';
module('Integration | Component | key-value-list', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
test('it renders fields', async function (assert) {
await render(hbs`
<Form::Field::KeyValueList as |M|>
<M.Legend>Label</M.Legend>
<M.HelperText>Help</M.HelperText>
<M.Error as |E|>
<E.Message>Error!</E.Message>
</M.Error>
</Form::Field::KeyValueList>
`);
assert.dom('tbody tr').exists({ count: 1 });
assert.dom('legend').exists().hasText('Label');
assert.dom('.hds-form-helper-text').exists().hasText('Help');
assert.dom('.hds-form-error__message').exists().hasText('Error!');
assert.dom('button').exists().hasText('Add');
});
test('it renders multiple options', async function (assert) {
this.options = [
{ key: 'one', value: 'two' },
{ key: 'three', value: 'four' },
];
// Template block usage:
await render(hbs`
<Form::Field::KeyValueList
@options={{this.options}}
/>
`);
assert.dom('tbody tr').exists({ count: 3 });
});
});
@@ -0,0 +1,73 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
} from 'api/models/credential-library';
module('Unit | Abilities | credential-library', function (hooks) {
setupTest(hooks);
let features;
hooks.beforeEach(function () {
features = this.owner.lookup('service:features');
features.enable('credential-library-vault-ssh-certificate');
});
test('can read credential library type when authorized and feature is enabled', function (assert) {
assert.expect(2);
const canService = this.owner.lookup('service:can');
const store = this.owner.lookup('service:store');
const credentialLibrary = store.createRecord('credential-library', {
authorized_actions: ['read'],
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
});
assert.true(canService.can('read credential-library', credentialLibrary));
credentialLibrary.type = TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC;
assert.true(canService.can('read credential-library', credentialLibrary));
});
test('cannot read credential library type when unauthorized and feature is enabled', function (assert) {
assert.expect(2);
const canService = this.owner.lookup('service:can');
const store = this.owner.lookup('service:store');
const credentialLibrary = store.createRecord('credential-library', {
authorized_actions: [],
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
});
assert.false(canService.can('read credential-library', credentialLibrary));
credentialLibrary.type = TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC;
assert.false(canService.can('read credential-library', credentialLibrary));
});
test('cannot read credential library type when unauthorized and feature is disabled', function (assert) {
assert.expect(2);
const canService = this.owner.lookup('service:can');
const store = this.owner.lookup('service:store');
const featuresService = this.owner.lookup('service:features');
featuresService.disable('credential-library-vault-ssh-certificate');
const credentialLibrary = store.createRecord('credential-library', {
authorized_actions: [],
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
});
assert.false(canService.can('read credential-library', credentialLibrary));
credentialLibrary.type = TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC;
assert.false(canService.can('read credential-library', credentialLibrary));
});
test('can read vault-generic but not vault-ssh-certificate when authorized and feature is disabled', function (assert) {
assert.expect(2);
const canService = this.owner.lookup('service:can');
const store = this.owner.lookup('service:store');
const featuresService = this.owner.lookup('service:features');
featuresService.disable('credential-library-vault-ssh-certificate');
const credentialLibrary = store.createRecord('credential-library', {
authorized_actions: ['read'],
type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
});
assert.false(canService.can('read credential-library', credentialLibrary));
credentialLibrary.type = TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC;
assert.true(canService.can('read credential-library', credentialLibrary));
});
});
+37 -19
View File
@@ -4689,43 +4689,46 @@
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==
"@hashicorp/design-system-components@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@hashicorp/design-system-components/-/design-system-components-1.3.2.tgz#42d10b20bf93bdf4587012af8a4a944f3538272a"
integrity sha512-aXm5pv9LurVFpoHTgOo87H2LxcdykSPMo88W2E/sw5dEEwJPZARI/Mg3H1i3EjR8Psc1COAozP5jpOhfer1b4w==
"@hashicorp/design-system-components@^1.5.1":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@hashicorp/design-system-components/-/design-system-components-1.5.1.tgz#ed02dd834db2d254f2dc6c108f3520ddf3268de3"
integrity sha512-P1kSd6t3V/cbAiWBWw/iib7WOZFDBIXFqbFP2UvT9LuZoP6tejrRfVy8Xhiy+NxCb2uVR543T3d8M/7E+N2e0Q==
dependencies:
"@hashicorp/design-system-tokens" "^1.2.0"
"@hashicorp/ember-flight-icons" "^3.0.1"
"@hashicorp/design-system-tokens" "^1.3.0"
"@hashicorp/ember-flight-icons" "^3.0.2"
dialog-polyfill "^0.5.6"
ember-auto-import "^2.4.2"
ember-cached-decorator-polyfill "^0.1.4"
ember-cli-babel "^7.26.11"
ember-cli-htmlbars "^6.1.0"
ember-cli-sass "^10.0.1"
ember-composable-helpers "^4.4.1"
ember-focus-trap "^1.0.1"
ember-keyboard "^8.1.0"
ember-named-blocks-polyfill "^0.2.5"
ember-style-modifier "^0.8.0"
ember-truth-helpers "^3.0.0"
sass "^1.43.4"
"@hashicorp/design-system-tokens@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@hashicorp/design-system-tokens/-/design-system-tokens-1.2.0.tgz#e83a69934c5c0c84de3c4572811e56543905bf39"
integrity sha512-N74hb1mLImbY1NyH7Of4UDY7HBO5A4VSdXzXyLO2ZrB/WYIfX++uKjOMknca0lAAIMC7kqaYm3OIbND/sHbu8g==
"@hashicorp/design-system-tokens@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@hashicorp/design-system-tokens/-/design-system-tokens-1.3.0.tgz#0a143f7fb20b72ac003e56c7408e23b0b55d45f9"
integrity sha512-Re16f4tqyAApChk20Eev0xVWijCo8m3YYIo4A84tStTTkXk4d63lbY1MuebWaaAjcjzKGuyPh0w3u0SJWJFCUg==
"@hashicorp/ember-flight-icons@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@hashicorp/ember-flight-icons/-/ember-flight-icons-3.0.1.tgz#38895453a7994d7089bc058ccaa11f9c7eb9584e"
integrity sha512-1gmzv3d//YzW87vd5FsJQpgq+addDdl3xgPkT1v9uVaDh7k8AZsz9IltymhP+Q62OIGHhx7GwagIwnwf3B6bAQ==
"@hashicorp/ember-flight-icons@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@hashicorp/ember-flight-icons/-/ember-flight-icons-3.0.2.tgz#8d884c07842a6c88df18ca680d7883a59222a2ba"
integrity sha512-WomQg1hw/IHA1N9hC77WbTNazVXqu2RdRoaVCGT99NTXQ4S7Bw7vhHheR4JAgt10ksMZFI3X/bJVHxFfjUCkSQ==
dependencies:
"@hashicorp/flight-icons" "^2.11.1"
"@hashicorp/flight-icons" "^2.12.0"
ember-auto-import "^2.4.2"
ember-cli-babel "^7.26.11"
ember-cli-htmlbars "^6.1.0"
"@hashicorp/flight-icons@^2.11.1":
version "2.11.1"
resolved "https://registry.yarnpkg.com/@hashicorp/flight-icons/-/flight-icons-2.11.1.tgz#0ef70fac396699fb1987afab9570786eda57e95b"
integrity sha512-JD3/o1jtru9b2nzMeYYABcQ3TCcBx3wv0XjibdjQ18yhZ4b3zW6v0sl9eRDl+wbfVfBeXLXuGabt7ZXWNaPCCA==
"@hashicorp/flight-icons@^2.12.0":
version "2.12.0"
resolved "https://registry.yarnpkg.com/@hashicorp/flight-icons/-/flight-icons-2.12.0.tgz#48bc21f21678668ffe9147b181a2991d8b151fc7"
integrity sha512-PhjTTHCjoq4EJirifbxLxnxXnCRf1NUAYZ1WnFW8i0yOmmax6fgjsJRPlf0VIGsR8R7isFpjuy6gJ5c7mNhE0w==
"@humanwhocodes/config-array@^0.5.0":
version "0.5.0"
@@ -11908,6 +11911,11 @@ dezalgo@^1.0.4:
asap "^2.0.0"
wrappy "1"
dialog-polyfill@^0.5.6:
version "0.5.6"
resolved "https://registry.yarnpkg.com/dialog-polyfill/-/dialog-polyfill-0.5.6.tgz#7507b4c745a82fcee0fa07ce64d835979719599a"
integrity sha512-ZbVDJI9uvxPAKze6z146rmfUZjBqNEwcnFTVamQzXH+svluiV7swmVIGr7miwADgfgt1G2JQIytypM9fbyhX4w==
diff-sequences@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
@@ -13230,6 +13238,16 @@ ember-compatibility-helpers@^1.2.5:
fs-extra "^9.1.0"
semver "^5.4.1"
ember-composable-helpers@^4.4.1:
version "4.5.0"
resolved "https://registry.yarnpkg.com/ember-composable-helpers/-/ember-composable-helpers-4.5.0.tgz#94febbdf4348e64f45f7a6f993f326e32540a61e"
integrity sha512-XjpDLyVPsLCy6kd5dIxZonOECCO6AA5sY5Hr6tYUbJg3s5ghFAiFWaNcYraYC+fL2yPJQAswwpfwGlQORUJZkw==
dependencies:
"@babel/core" "^7.0.0"
broccoli-funnel "2.0.1"
ember-cli-babel "^7.26.3"
resolve "^1.10.0"
ember-composable-helpers@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/ember-composable-helpers/-/ember-composable-helpers-5.0.0.tgz#055bab3a3e234ab2917499b1465e968c253ca885"