chore: manual merge from main

This commit is contained in:
Randall Morey committed 2021-08-04 11:31:07 -04:00
commit 7b51d15ba4
98 files changed
+4702 -2822

No files matched your search

@@ -8,6 +8,11 @@ import { attr } from '@ember-data/model';
export default class GeneratedCredentialLibraryModel extends BaseModel {
// =attributes
@attr('string', {
description: 'The type of the resource, to help differentiate schemas',
})
type;
@attr('string', {
description: 'The owning credential store ID.',
})
+2 -1
View File
@@ -39,7 +39,8 @@ export default class BaseModel extends Model {
@attr({
readOnly: true,
emptyArrayIfMissing: true,
}) authorized_actions;
})
authorized_actions;
set scopeModel(model) {
if (model) {
@@ -2,6 +2,13 @@ import GeneratedCredentialLibraryModel from '../generated/models/credential-libr
import { attr } from '@ember-data/model';
import { fragment } from 'ember-data-model-fragments/attributes';
/**
* Enum options for credential library.
*/
export const options = {
http_method: ['GET', 'POST'],
};
export default class CredentialLibraryModel extends GeneratedCredentialLibraryModel {
// =error attributes
// These attributes exist solely to capture errors on nested fields.
@@ -1,5 +1,6 @@
import Fragment from 'ember-data-model-fragments/fragment';
import { attr } from '@ember-data/model';
import { options } from './credential-library';
export default class FragmentCredentialLibraryAttributesModel extends Fragment {
//= attributes (vault)
@@ -7,6 +8,7 @@ export default class FragmentCredentialLibraryAttributesModel extends Fragment {
@attr('string', {
description:
'The HTTP method the library uses when requesting credentials from Vault.',
defaultValue: () => options.http_method[0],
})
http_method;
+90
View File
@@ -20,6 +20,16 @@ export default class TargetModel extends GeneratedTargetModel {
})
host_sets;
/**
* Credential library ids are read only and can be
* persisted via a dedicated call to `addCredentialLibraries()`.
*/
@fragmentArray('fragment-string', {
readOnly: true,
emptyArrayIfMissing: true,
})
application_credential_library_ids;
/**
* An array of resolved host set and host catalog instances. Model instances
* must already be loaded into the store (this method will not load unloaded
@@ -36,6 +46,24 @@ export default class TargetModel extends GeneratedTargetModel {
.filter((hostSetRef) => hostSetRef.model !== null);
}
/**
* An array of resolved credential library instances. Model instances
* must already be loaded into the store (this method will not load unloaded
* instances). Unresolvable instances are excluded from the array.
* @type {[CredentialLibraryModel]}
*/
@computed('application_credential_library_ids.[]', 'store')
get credentialLibraries() {
return this.application_credential_library_ids
.map((credential_library_fragment) =>
this.store.peekRecord(
'credential-library',
credential_library_fragment.value
)
)
.filter(Boolean);
}
/**
* Sessions associated with this target (but only already loaded sessions).
* @type {SessionModel[]}
@@ -122,4 +150,66 @@ export default class TargetModel extends GeneratedTargetModel {
removeHostSet(hostSetID, options) {
return this.removeHostSets([hostSetID], options);
}
/**
* Adds credential libraries via the `add-credential-libraries` method.
* See serializer and adapter for more information.
* @param {[string]} credentialLibraryIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
addCredentialLibraries(
credentialLibraryIDs,
options = { adapterOptions: {} }
) {
const defaultAdapterOptions = {
method: 'add-credential-libraries',
credentialLibraryIDs,
};
// There is no "deep merge" in ES.
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Delete credential libraries via the `remove-credential-libraries` method.
* See serializer and adapter for more information.
* @param {[string]} credentialLibraryIDs
* @param {object} options
* @param {object} options.adapterOptions
* @return {Promise}
*/
removeCredentialLibraries(
credentialLibraryIDs,
options = { adapterOptions: {} }
) {
const defaultAdapterOptions = {
method: 'remove-credential-libraries',
credentialLibraryIDs,
};
// There is no "deep merge" in ES.
return this.save({
...options,
adapterOptions: {
...defaultAdapterOptions,
...options.adapterOptions,
},
});
}
/**
* Delete a single credential library set via the `remove-credential-libraries` method.
* @param {number} credentialLibraryID
* @param {object} options
* @return {Promise}
*/
removeCredentialLibrary(credentialLibraryID, options) {
return this.removeCredentialLibraries([credentialLibraryID], options);
}
}
@@ -19,6 +19,16 @@ export default class FragmentCredentialStoreAttributesSerializer extends JSONSer
if (type === 'string' && json[key] === '') json[key] = null;
// Do not serialize read-only attributes.
if (options.readOnly) delete json[key];
// Token cannot be unset. If it's falsy, it must be removed.
if (key === 'token' && !json[key]) delete json[key];
// Client certificate key cannot be unset when certificate is set.
// If it's falsy, it must be removed.
if (
key === 'client_certificate_key' &&
!json[key] &&
json['client_certificate']
)
delete json[key];
return value;
}
}
+25 -1
View File
@@ -6,7 +6,11 @@ export default class TargetSerializer extends ApplicationSerializer {
/**
* If `adapterOptions.hostSets` is set to an array of host set models,
* the resulting target serialization should include **only host sets**
* and the version. Normally, host sets are not serialized.
* and the version.
* If `adapterOptions.credentialLibraries` is set to an array of
* credential library models, the resulting target serialization should
* include **only credential libraries** and the version.
* Normally, neither host sets or credential libraries are not serialized.
* @override
* @param {Snapshot} snapshot
* @return {object}
@@ -16,6 +20,12 @@ export default class TargetSerializer extends ApplicationSerializer {
const hostSetIDs = snapshot?.adapterOptions?.hostSetIDs;
if (hostSetIDs)
serialized = this.serializeWithHostSets(snapshot, hostSetIDs);
const credentialLibraryIDs = snapshot?.adapterOptions?.credentialLibraryIDs;
if (credentialLibraryIDs)
serialized = this.serializeWithCredentialLibraries(
snapshot,
credentialLibraryIDs
);
return serialized;
}
@@ -32,4 +42,18 @@ export default class TargetSerializer extends ApplicationSerializer {
host_set_ids: hostSetIDs,
};
}
/**
* Returns a payload containing only the credential_library_ids array using IDs
* passed into the function (rather than existing credential libraries on the model).
* @param {Snapshot} snapshot
* @param {[string]} credentialLibraryIDs
* @return {object}
*/
serializeWithCredentialLibraries(snapshot, credentialLibraryIDs) {
return {
version: snapshot.attr('version'),
application_credential_library_ids: credentialLibraryIDs,
};
}
}
+29 -2
View File
@@ -366,10 +366,10 @@ export default function () {
const target = targets.find(id);
const updatedAttrs = {
version: attrs.version,
hostSetIds: target.hostSetIds,
};
// If adding host sets, push them into the array
if (method === 'add-host-sets') {
updatedAttrs.hostSetIds = target.hostSetIds;
attrs.hostSetIds.forEach((id) => {
if (!updatedAttrs.hostSetIds.includes(id)) {
updatedAttrs.hostSetIds.push(id);
@@ -378,10 +378,28 @@ export default function () {
}
// If deleting host sets, filter them out of the array
if (method === 'remove-host-sets') {
updatedAttrs.hostSetIds = target.hostSetIds;
updatedAttrs.hostSetIds = updatedAttrs.hostSetIds.filter((id) => {
return !attrs.hostSetIds.includes(id);
});
}
// If adding credential libraries, push them into the array
if (method === 'add-credential-libraries') {
updatedAttrs.credentialLibraryIds = target.credentialLibraryIds;
attrs.applicationCredentialLibraryIds.forEach((id) => {
if (!updatedAttrs.credentialLibraryIds.includes(id)) {
updatedAttrs.credentialLibraryIds.push(id);
}
});
}
// If deleting credential libraries, filter them out of the array
if (method === 'remove-credential-libraries') {
updatedAttrs.credentialLibraryIds = target.credentialLibraryIds;
updatedAttrs.credentialLibraryIds =
updatedAttrs.credentialLibraryIds.filter((id) => {
return !attrs.applicationCredentialLibraryIds.includes(id);
});
}
return target.update(updatedAttrs);
}
);
@@ -467,6 +485,7 @@ export default function () {
this.get('/credential-stores/:id');
this.post('/credential-stores');
this.patch('/credential-stores/:id');
this.del('/credential-stores/:id');
// credential-libraries
@@ -478,7 +497,15 @@ export default function () {
) => credentialLibraries.where({ credentialStoreId })
);
this.get('/credential-libraries/:id');
this.post('/credential-libraries/:id');
this.post(
'/credential-libraries',
function ({ credentialStores, credentialLibraries }) {
const attrs = this.normalizedRequestAttrs();
const credentialStore = credentialStores.find(attrs.credentialStoreId);
attrs.scopeId = credentialStore.scope.id;
return credentialLibraries.create(attrs);
}
);
this.del('/credential-libraries/:id');
this.patch('/credential-libraries/:id');
@@ -1,14 +1,20 @@
import factory from '../generated/factories/credential-library';
import { random, system } from 'faker';
const types = ['vault'];
export default factory.extend({
id: (i) => `credential-library-id-${i}`,
type: (i) => types[i % types.length],
attributes() {
return {
http_method: 'GET',
http_request_body: random.word(),
path: system.directoryPath(),
};
switch (this.type) {
case 'vault':
return {
http_method: 'GET',
http_request_body: random.word(),
path: system.directoryPath(),
};
}
},
});
+15 -5
View File
@@ -7,7 +7,7 @@ const hostSetChance = 0.3;
export default factory.extend({
/**
* -1 means "unlimited" and we want to generate these on occassion.
* -1 means "unlimited" and we want to generate these on occasion.
*/
session_connection_limit: () => random.arrayElement([-1, datatype.number()]),
@@ -25,11 +25,11 @@ export default factory.extend({
},
/**
* Randomly selects existing host sets to assign to target.
* Randomly selects existing host sets and credential libraries to assign to target.
*/
withRandomHostSets: trait({
withAssociations: trait({
afterCreate(target, server) {
let randomlySelectedHostSets;
let randomlySelectedHostSets, randomlySelectedCredentialLibraries;
randomlySelectedHostSets = server.schema.hostSets
// BLERG: fun fact, for no reason at all, the element passed
// into a where function is not a full model instance, as you might
@@ -38,7 +38,17 @@ export default factory.extend({
// the result set of `where` _is a collection of model instances_.
.where((hostSet) => hostSet.scopeId === target.scope.id)
.models.filter(() => randomBoolean(hostSetChance));
target.update({ hostSets: randomlySelectedHostSets });
randomlySelectedCredentialLibraries = server.schema.credentialLibraries
.where(
(credentialLibrary) => credentialLibrary.scopeId === target.scope.id
)
.models.filter(() => randomBoolean());
target.update({
hostSets: randomlySelectedHostSets,
credentialLibraries: randomlySelectedCredentialLibraries,
});
},
}),
});
+1
View File
@@ -3,4 +3,5 @@ import { Model, belongsTo, hasMany } from 'ember-cli-mirage';
export default Model.extend({
scope: belongsTo(),
hostSets: hasMany(),
credentialLibraries: hasMany(),
});
+1 -1
View File
@@ -47,8 +47,8 @@ export default function (server) {
// Other resources
server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => {
server.createList('host-catalog', 2, { scope }, 'withChildren');
server.createList('target', 2, { scope }, 'withRandomHostSets');
server.createList('session', 4, { scope }, 'withAssociations');
server.createList('credential-store', 3, { scope }, 'withAssociations');
server.createList('target', 2, { scope }, 'withAssociations');
});
}
+3
View File
@@ -23,6 +23,9 @@ export default ApplicationSerializer.extend({
return { id: host_set_id, host_catalog_id };
});
}
if (model.credentialLibraryIds?.length) {
json.application_credential_library_ids = model.credentialLibraryIds;
}
return json;
},
});
+3 -3
View File
@@ -30,7 +30,7 @@
"ember-cli-mirage": "^1.1.8",
"ember-data": "^3.20.0",
"ember-data-model-fragments": "5.0.0-beta.2",
"ember-fetch": "^8.0.1"
"ember-fetch": "^8.1.0"
},
"devDependencies": {
"@ember/optional-features": "^2.0.0",
@@ -60,8 +60,8 @@
"ember-source-channel-url": "^3.0.0",
"ember-template-lint": "^2.15.0",
"ember-try": "^1.4.0",
"eslint": "^7.17.0",
"eslint-config-prettier": "^7.1.0",
"eslint": "^7.31.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-ember": "^10.1.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.3.1",
+158
View File
@@ -223,4 +223,162 @@ module('Unit | Model | target', function (hooks) {
'Target has two associated sessions loaded in the store'
);
});
test('it has a `credentialLibraries` array of resolved model instances (if those instances are already in the store)', function (assert) {
assert.expect(4);
const store = this.owner.lookup('service:store');
store.push({
data: {
id: '123abc',
type: 'target',
attributes: {
application_credential_library_ids: [{ value: '1' }, { value: '2' }],
},
},
});
const target = store.peekRecord('target', '123abc');
assert.equal(
target.application_credential_library_ids.length,
2,
'Target has two entires in application_credential_library_ids'
);
assert.equal(
target.credentialLibraries.length,
0,
'Target has no resolved credentialLibraries because they are not loaded yet'
);
store.push({
data: {
id: '1',
type: 'credential-library',
attributes: {},
},
});
store.push({
data: {
id: '2',
type: 'credential-library',
attributes: {},
},
});
// Since `credentialLibraries` is computed on `application_credential_library_ids`,
// not the store itself, it's necessary to do this assignment to kick-off the
// computed update.
/* eslint-disable no-self-assign */
target.application_credential_library_ids =
target.application_credential_library_ids;
/* eslint-enable no-self-assign */
assert.equal(
target.application_credential_library_ids.length,
2,
'Target has two entires in application_credential_library_ids'
);
assert.equal(
target.credentialLibraries.length,
2,
'Target has two resolved credentialLibraries'
);
});
test('it has an `addCredentialLibraries` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/targets/123abc:add-credential-libraries',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
application_credential_library_ids: ['123_abc', 'foobar'],
version: 1,
});
return { id: '123abc' };
}
);
const store = this.owner.lookup('service:store');
store.push({
data: {
id: '123abc',
type: 'target',
attributes: {
name: 'Target',
description: 'Description',
application_credential_library_ids: [{ value: '1' }, { value: '2' }],
version: 1,
scope: {
scope_id: 'o_1',
type: 'scope',
},
},
},
});
const model = store.peekRecord('target', '123abc');
await model.addCredentialLibraries(['123_abc', 'foobar']);
});
test('it has a `removeCredentialLibraries` method that targets a specific POST API endpoint and serialization', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/targets/123abc:remove-credential-libraries',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
application_credential_library_ids: ['1', '2'],
version: 1,
});
return { id: '123abc' };
}
);
const store = this.owner.lookup('service:store');
store.push({
data: {
id: '123abc',
type: 'target',
attributes: {
name: 'Target',
description: 'Description',
application_credential_library_ids: [{ value: '1' }, { value: '2' }],
version: 1,
scope: {
scope_id: 'o_1',
type: 'scope',
},
},
},
});
const model = store.peekRecord('target', '123abc');
await model.removeCredentialLibraries(['1', '2']);
});
test('it has a `removeCredentialLibrary` method that deletes a single credential library using `removeCredentialLibraries` method', async function (assert) {
assert.expect(1);
this.server.post(
'/v1/targets/123abc:remove-credential-libraries',
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
application_credential_library_ids: ['2'],
version: 1,
});
return { id: '123abc' };
}
);
const store = this.owner.lookup('service:store');
store.push({
data: {
id: '123abc',
type: 'target',
attributes: {
name: 'Target',
description: 'Description',
application_credential_library_ids: [{ value: '1' }, { value: '2' }],
version: 1,
scope: {
scope_id: 'o_1',
type: 'scope',
},
},
},
});
const model = store.peekRecord('target', '123abc');
await model.removeCredentialLibrary('2');
});
});
@@ -203,6 +203,7 @@ module('Unit | Serializer | application', function (hooks) {
name: 'Target 1',
scope: { scope_id: 'o_123' },
host_sets: [],
application_credential_library_ids: [],
},
relationships: {},
},
@@ -9,6 +9,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',
name: 'Name',
description: 'Description',
attributes: {
@@ -20,6 +21,7 @@ module('Unit | Serializer | credential library', function (hooks) {
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
type: 'vault',
credential_store_id: null,
name: 'Name',
description: 'Description',
@@ -40,6 +42,7 @@ module('Unit | Serializer | credential library', function (hooks) {
id: '1',
type: 'credential-library',
attributes: {
type: 'vault',
name: 'Name',
description: 'Description',
attributes: {
@@ -54,6 +57,7 @@ module('Unit | Serializer | credential library', function (hooks) {
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
type: 'vault',
credential_store_id: null,
name: 'Name',
description: 'Description',
@@ -6,26 +6,112 @@ module(
function (hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function (assert) {
let store = this.owner.lookup('service:store');
let serializer = store.serializerFor(
'fragment-credential-store-attributes'
test('it serializes empty strings to null', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const record = store.createRecord(
'fragment-credential-store-attributes',
{
address: '',
ca_cert: '',
client_certificate: '',
client_certificate_key: '',
namespace: '',
tls_server_name: '',
tls_skip_verify: false,
}
);
assert.ok(serializer);
let serializedRecord = record.serialize();
assert.deepEqual(serializedRecord, {
address: null,
ca_cert: null,
client_certificate: null,
client_certificate_key: null,
namespace: null,
tls_server_name: null,
tls_skip_verify: false,
});
});
test('it serializes records', function (assert) {
let store = this.owner.lookup('service:store');
let record = store.createRecord(
test('it does not serialize token if present but falsy', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const record = store.createRecord(
'fragment-credential-store-attributes',
{}
{
token: null,
address: 'http://localhost:5000',
ca_cert: 'ca-cert-123',
client_certificate: 'client-cert-123',
client_certificate_key: 'client-cert-key-456',
namespace: 'foobar',
tls_server_name: 'yes',
tls_skip_verify: false,
}
);
let serializedRecord = record.serialize();
assert.deepEqual(
serializedRecord,
{
address: 'http://localhost:5000',
ca_cert: 'ca-cert-123',
client_certificate: 'client-cert-123',
client_certificate_key: 'client-cert-key-456',
namespace: 'foobar',
tls_server_name: 'yes',
tls_skip_verify: false,
},
'token attribute is not expected'
);
});
test('it does not serialize client certificate key if present but falsy when client certificate is present', function (assert) {
assert.expect(2);
const store = this.owner.lookup('service:store');
const record = store.createRecord(
'fragment-credential-store-attributes',
{
client_certificate: 'client-cert-123',
client_certificate_key: '',
address: 'http://localhost:5000',
token: 'foo',
ca_cert: 'ca-cert-123',
namespace: 'foobar',
tls_server_name: 'yes',
tls_skip_verify: false,
}
);
let serializedRecord = record.serialize();
assert.deepEqual(
serializedRecord,
{
client_certificate: 'client-cert-123',
address: 'http://localhost:5000',
token: 'foo',
ca_cert: 'ca-cert-123',
namespace: 'foobar',
tls_server_name: 'yes',
tls_skip_verify: false,
},
'client certificate key attribute is not expected'
);
let serializedRecord = record.serialize();
assert.ok(serializedRecord);
record.client_certificate_key = 'client-cert-key-456';
serializedRecord = record.serialize();
assert.deepEqual(
serializedRecord,
{
client_certificate: 'client-cert-123',
client_certificate_key: 'client-cert-key-456',
address: 'http://localhost:5000',
token: 'foo',
ca_cert: 'ca-cert-123',
namespace: 'foobar',
tls_server_name: 'yes',
tls_skip_verify: false,
},
'client certificate key attribute is expected'
);
});
}
);
@@ -4,7 +4,7 @@ import { setupTest } from 'ember-qunit';
module('Unit | Serializer | target', function (hooks) {
setupTest(hooks);
test('it serializes targets normally, without host sets', function (assert) {
test('it serializes targets normally, without host sets or credential libraries', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('target');
@@ -15,6 +15,7 @@ module('Unit | Serializer | target', function (hooks) {
{ host_set_id: '1', host_catalog_id: '2' },
{ host_set_id: '3', host_catalog_id: '4' },
],
application_credential_library_ids: [{ value: '1' }, { value: '2' }],
scope: {
scope_id: 'org_1',
type: 'org',
@@ -63,6 +64,27 @@ module('Unit | Serializer | target', function (hooks) {
});
});
test('it serializes only credential libraries and version when an `adapterOptions.credentialLibraryIDs` array is passed', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('target');
const record = store.createRecord('target', {
name: 'User',
description: 'Description',
application_credential_library_ids: [{ value: '1' }, { value: '2' }],
version: 1,
});
const snapshot = record._createSnapshot();
snapshot.adapterOptions = {
credentialLibraryIDs: ['4', '5'],
};
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
application_credential_library_ids: ['4', '5'],
version: 1,
});
});
test('it normalizes records with array fields', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
@@ -75,6 +97,7 @@ module('Unit | Serializer | target', function (hooks) {
{ host_set_id: '1', host_catalog_id: '2' },
{ host_set_id: '3', host_catalog_id: '4' },
],
application_credential_library_ids: ['1'],
};
const normalized = serializer.normalizeSingleResponse(
store,
@@ -92,13 +115,14 @@ module('Unit | Serializer | target', function (hooks) {
{ host_set_id: '1', host_catalog_id: '2' },
{ host_set_id: '3', host_catalog_id: '4' },
],
application_credential_library_ids: [{ value: '1' }],
},
relationships: {},
},
});
});
test('it normalizes missing host_sets to empty array', function (assert) {
test('it normalizes missing host_sets and credential libraries to empty array', function (assert) {
assert.expect(1);
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('target');
@@ -122,6 +146,7 @@ module('Unit | Serializer | target', function (hooks) {
name: 'Target 1',
scope: { scope_id: 'o_123' },
host_sets: [],
application_credential_library_ids: [],
},
relationships: {},
},
+2 -2
View File
@@ -53,8 +53,8 @@
"ember-source-channel-url": "^3.0.0",
"ember-template-lint": "^2.15.0",
"ember-try": "^1.4.0",
"eslint": "^7.17.0",
"eslint-config-prettier": "^7.1.0",
"eslint": "^7.31.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-ember": "^10.1.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.3.1",
+2 -2
View File
@@ -68,8 +68,8 @@
"ember-source-channel-url": "^3.0.0",
"ember-template-lint": "^2.15.0",
"ember-try": "^1.4.0",
"eslint": "^7.17.0",
"eslint-config-prettier": "^7.1.0",
"eslint": "^7.31.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-ember": "^10.1.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.3.1",
+4 -8
View File
@@ -1,9 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4577 1.67511L5.56323 7.36908L10.5292 13.3821L13.2972 11.0961L17.1724 15.5238L13.7226 18.5433L15.2044 20.2364L18.6543 17.2169L20.1097 18.8797L16.662 21.8973L18.1438 23.5904L23.2846 19.091L15.0328 9.66276L17.4237 7.68814L12.4577 1.67511Z" fill="black"/>
<mask id="path-2-inside-1" fill="white">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.766 4.35302L5.62911 4.255L5.53249 11.2901L9.12206 11.3394L9.20978 17.2227L4.62573 17.2911L4.6593 19.5409L9.24333 19.4725L9.27627 21.682L4.69506 21.7503L4.7286 24L11.5596 23.8982L11.3728 11.3703L12.6694 11.3882L12.766 4.35302Z"/>
</mask>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.766 4.35302L5.62911 4.255L5.53249 11.2901L9.12206 11.3394L9.20978 17.2227L4.62573 17.2911L4.6593 19.5409L9.24333 19.4725L9.27627 21.682L4.69506 21.7503L4.7286 24L11.5596 23.8982L11.3728 11.3703L12.6694 11.3882L12.766 4.35302Z" fill="black"/>
<path d="M5.62911 4.255L5.64971 2.75515L4.14985 2.73455L4.12925 4.23441L5.62911 4.255ZM12.766 4.35302L14.2659 4.37362L14.2865 2.87376L12.7866 2.85316L12.766 4.35302ZM5.53249 11.2901L4.03264 11.2695L4.01204 12.7694L5.5119 12.79L5.53249 11.2901ZM9.12206 11.3394L10.6219 11.3171L10.6002 9.85959L9.14265 9.83958L9.12206 11.3394ZM9.20978 17.2227L9.23216 18.7226L10.732 18.7002L10.7096 17.2004L9.20978 17.2227ZM4.62573 17.2911L4.60335 15.7913L3.10352 15.8137L3.1259 17.3135L4.62573 17.2911ZM4.6593 19.5409L3.15947 19.5633L3.18185 21.0631L4.68168 21.0407L4.6593 19.5409ZM9.24333 19.4725L10.7432 19.4501L10.7208 17.9503L9.22095 17.9727L9.24333 19.4725ZM9.27627 21.682L9.29863 23.1818L10.7985 23.1594L10.7761 21.6596L9.27627 21.682ZM4.69506 21.7503L4.6727 20.2504L3.17286 20.2728L3.19522 21.7726L4.69506 21.7503ZM4.7286 24L3.22877 24.0224L3.25113 25.5222L4.75096 25.4998L4.7286 24ZM11.5596 23.8982L11.5819 25.398L13.0818 25.3756L13.0594 23.8758L11.5596 23.8982ZM11.3728 11.3703L11.3934 9.87048L9.84992 9.84929L9.87293 11.3927L11.3728 11.3703ZM12.6694 11.3882L12.6488 12.888L14.1487 12.9086L14.1693 11.4087L12.6694 11.3882ZM5.60851 5.75486L12.7454 5.85288L12.7866 2.85316L5.64971 2.75515L5.60851 5.75486ZM7.03235 11.3107L7.12897 4.2756L4.12925 4.23441L4.03264 11.2695L7.03235 11.3107ZM9.14265 9.83958L5.55309 9.79028L5.5119 12.79L9.10146 12.8393L9.14265 9.83958ZM7.62222 11.3618L7.70995 17.2451L10.7096 17.2004L10.6219 11.3171L7.62222 11.3618ZM9.1874 15.7229L4.60335 15.7913L4.64811 18.791L9.23216 18.7226L9.1874 15.7229ZM3.1259 17.3135L3.15947 19.5633L6.15913 19.5185L6.12557 17.2688L3.1259 17.3135ZM4.68168 21.0407L9.2657 20.9723L9.22095 17.9727L4.63692 18.0411L4.68168 21.0407ZM7.74349 19.4949L7.77644 21.7043L10.7761 21.6596L10.7432 19.4501L7.74349 19.4949ZM9.25391 20.1821L4.6727 20.2504L4.71742 23.2501L9.29863 23.1818L9.25391 20.1821ZM3.19522 21.7726L3.22877 24.0224L6.22843 23.9777L6.19489 21.7279L3.19522 21.7726ZM4.75096 25.4998L11.5819 25.398L11.5372 22.3983L4.70624 22.5002L4.75096 25.4998ZM13.0594 23.8758L12.8726 11.348L9.87293 11.3927L10.0597 23.9205L13.0594 23.8758ZM12.69 9.88829L11.3934 9.87048L11.3522 12.8702L12.6488 12.888L12.69 9.88829ZM11.2662 4.33242L11.1696 11.3676L14.1693 11.4087L14.2659 4.37362L11.2662 4.33242Z" fill="black" mask="url(#path-2-inside-1)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.2796 1.26121C9.20838 0.46864 7.88301 0 6.44818 0C2.88695 0 0 2.88695 0 6.44818C0 9.00798 1.4916 11.2194 3.65306 12.2607L3.68943 9.61275C2.80735 8.84314 2.25 7.71075 2.25 6.44818C2.25 4.12959 4.12959 2.25 6.44818 2.25C7.18811 2.25 7.88333 2.44143 8.48703 2.77746L10.2796 1.26121Z" fill="black"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.18544 1.11673L4.58911 4.91271L7.89977 8.92139L9.74509 7.3974L12.3286 10.3492L10.0287 12.3622L11.0166 13.4909L13.3165 11.4779L14.2867 12.5865L11.9883 14.5982L12.9762 15.7269L16.4034 12.7273L10.9021 6.44183L12.4961 5.12541L9.18544 1.11673Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.39097 2.9997L4.63303 2.93436L4.56862 7.62444L6.96166 7.65731L7.02014 11.5795L3.96411 11.6251L3.98649 13.1249L7.04251 13.0793L7.06447 14.5523L4.01033 14.5979L4.03269 16.0977L8.58667 16.0298L8.46213 7.67792L9.32656 7.68979L9.39097 2.9997Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.73342 0.840799C7.01928 0.312425 6.13571 0 5.17915 0C2.805 0 0.880371 1.92463 0.880371 4.29878C0.880371 6.00532 1.87477 7.47961 3.31575 8.1738L3.33999 6.4085C2.75194 5.89543 2.38037 5.1405 2.38037 4.29878C2.38037 2.75306 3.63343 1.5 5.17915 1.5C5.67244 1.5 6.13592 1.62762 6.53838 1.85163L7.73342 0.840799Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

+76 -9
View File
@@ -253,29 +253,96 @@ target:
actions:
create: New Target
delete: Delete Target
add-host-sets: Add Host Sets
add-host-sets: Add Host Sources
add-credential-libraries: Add Credential Sources
types:
tcp: TCP
host-set:
title: Host Source
title_plural: Host Sources
messages:
welcome:
title: Welcome to Host Sets
description: No hosts sets in this target.
title: Welcome to Host Sources
description: No hosts sources in this target.
none:
title: No Host Sets Available
description: No hosts sets available to add to target.
title: No Host Sources Available
description: No hosts sources available to add to target.
add:
title: Add Host Sets
description: Select host sets to assign to this target.
title: Add Host Sources
description: Select host sources to assign to this target.
credential-library:
title: Credential Source
title_plural: Credential Sources
messages:
welcome:
title: Welcome to Credential Sources
description: No credential sources in this target.
none:
title: No Credential Sources Available
description: No credential sources available to add to this target.
add:
title: Add Credential Sources
description: Select credential sources to assign to this target.
credential-store:
title: Credential Store
title_plural: Credential Stores
description: A credential stores is a collection of credentials and credential libraries.
description: A credential store is a collection of credentials and credential libraries.
messages:
welcome:
title: Welcome to Credential Stores
actions:
delete: Delete Credential Store
types:
vault: Vault
form:
address:
label: Address
help: The address of the Vault server. This should be a complete URL such as https://127.0.0.1:8200.
namespace:
label: Namespace
help: A Vault namespace. Requires Vault Enterprise.
ca_cert:
label: CA Certificate
help: A PEM-encoded CA certificate to verify the Vault server's TLS certificate.
tls_server_name:
label: TLS Server Name
help: Name to use as the SNI host when connecting to Vault via TLS.
tls_skip_verify:
label: TLS Skip Verify
help: Disable verification of TLS certificates. Using this option is highly discouraged as it decreases the security of data transmissions to and from the Vault server.
token:
label: Token
help: A token used for accessing Vault.
token_hmac:
label: Token HMAC
client_certificate:
label: Client Certificate
help: A PEM-encoded client certificate to use for TLS authentication to the Vault server.
client_certificate_key:
label: Client Certificate Key
help: A PEM-encoded private key matching the client certificate from Client Certificate.
client_certificate_key_hmac:
label: Client Certificate Key HMAC
credential-library:
title: Credential Library
title_plural: Credential Libraries
description: A credential library is a resource that provides credentials.
actions:
create: New Credential Library
delete: Delete Credential Library
messages:
none:
title: No Credential Libraries Available
description: No Credential Libraries available in this credential store.
description: No Credential Libraries available in this credential store.
form:
vault_path:
label: Vault Path
help: The path the library uses when requesting credentials from Vault.
http_method:
label: HTTP Method
help: The HTTP method the library uses when requesting credentials from Vault.
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.
types:
vault: Vault
@@ -1,4 +1,4 @@
<div class="rose-form-checkbox{{if @error " error"}}">
<div class="rose-form-checkbox{{if @error " error"}} {{if @inline " inline"}}">
<Input
...attributes
@@ -68,3 +68,61 @@ Renders a form input checkbox.
}
}}</Story>
</Preview>
```htmlbars
<Rose::Form::Checkbox
@name="boolean-value"
@label="Yes?"
@checked={{false}}
@error={{false}}
@disabled={{false}}
@inline={{false}}
/>
```
<Preview>
<Story name="Checkbox with inline style" height="10rem">{{
template: hbs`
<Rose::Form::Checkbox
@name='checkbox-1'
@label='Checkbox1 🛎'
@description={{description}}
@helperText={{helperText}}
@checked={{checked}}
@error={{error}}
@disabled={{disabled}}
@inline={{inline}}
/>
<Rose::Form::Checkbox
@name='checkbox-2'
@label='Checkbox2 🛎'
@description={{description}}
@helperText={{helperText}}
@checked={{checked}}
@error={{error}}
@disabled={{disabled}}
@inline={{inline}}
/>
<Rose::Form::Checkbox
@name='checkbox-3'
@label='Checkbox3 🛎'
@description={{description}}
@helperText={{helperText}}
@checked={{checked}}
@error={{error}}
@disabled={{disabled}}
@inline={{inline}}
/>
`,
context: {
description: text('description', 'Short description in label'),
helperText: text('helperText', 'This is an optional help text.'),
checked: boolean('checked', false),
error: boolean('error', false),
disabled: boolean('disabled', false),
inline: boolean('inline', false)
}
}}</Story>
</Preview>
@@ -20,6 +20,7 @@
input=(component 'rose/form/input' disabled=this.disabled)
select=(component 'rose/form/select' disabled=this.disabled)
textarea=(component 'rose/form/textarea' disabled=this.disabled)
fieldset=(component 'rose/form/fieldset')
actions=(component
(if this.showEditToggleButton
'rose/form/actions/edit-toggle'
@@ -1,3 +1,3 @@
{{yield (hash
radio=(component 'rose/form/radio/radio' name=@name selectedValue=@selectedValue disabled=@disabled changed=@changed)
)}}
radio=(component 'rose/form/radio/radio' name=@name selectedValue=@selectedValue disabled=@disabled changed=@changed inline=@inline)
)}}
@@ -15,6 +15,7 @@ Renders a form radiofield group.
@name={{name}}
@selectedValue="disagree"
@changed={{fn}}
@inline={{false}}
as |radioGroup|
>
<radioGroup.radio
@@ -39,6 +40,7 @@ Renders a form radiofield group.
@name={{name}}
@selectedValue={{selectedValue}}
@changed={{changed}}
@inline={{inline}}
as |radioGroup|
>
<radioGroup.radio
@@ -59,6 +61,7 @@ Renders a form radiofield group.
context: {
name: text('name', 'mustagree'),
disabled: boolean('disabled', false),
inline: boolean('inline', true),
selectedValue: text('selectedValue', 'disagree'),
firstFieldLabel: text('first label', 'Agree'),
firstFieldValue: text('first value', 'agree'),
@@ -1,4 +1,4 @@
<div class="rose-form-radio{{if @error " error"}}">
<div class="rose-form-radio{{if @error " error"}} {{if @inline "inline"}}">
<RadioButton
...attributes
@@ -54,3 +54,71 @@ Renders a form radiofield.
}
}}</Story>
</Preview>
```htmlbars
<Rose::Form::Radio::Radio
@label="Yes"
@error={{false}}
@value="yes-agree"
@selectedValue="yes-agree"
@disabled={{false}}
@changed={{fn}}
@inline={{true}}
/>
```
<Preview>
<Story name="Radio with inline style" height="10rem">{{
template: hbs`
<Rose::Form::Radio::Radio
@label='Radiofield1'
@error={{error}}
@value={{value}}
@selectedValue={{selectedValue}}
@disabled={{disabled}}
@icon={{icon}}
@inline={{inline}}
/>
<Rose::Form::Radio::Radio
@label='Radiofield2'
@error={{error}}
@value={{value}}
@selectedValue={{selectedValue}}
@disabled={{disabled}}
@icon={{icon}}
@inline={{inline}}
/>
<Rose::Form::Radio::Radio
@label='Radiofield3'
@error={{error}}
@value={{value}}
@selectedValue={{selectedValue}}
@disabled={{disabled}}
@icon={{icon}}
@inline={{inline}}
/>
`,
context: {
error: boolean('error', false),
value: text('value', 'value and selectedValue must match to select radiofield'),
selectedValue: text(
'selectedValue',
'value and selectedValue must match to select radiofield'
),
disabled: boolean('disabled', false),
icon: select(
'icon',
{
'none': null,
'user-organization': 'user-organization',
'copy-action': 'copy-action',
'unfold-more': 'unfold-more',
'unfold-less': 'unfold-less',
},
null
),
inline: boolean('inline', false)
}
}}</Story>
</Preview>
@@ -2,6 +2,10 @@
@use '../../utilities/type';
.rose-form-checkbox {
&.inline {
display: inline-block;
margin-right: sizing.rems(s);
}
@include type.type(m);
position: relative;
margin-bottom: sizing.rems(m);
@@ -5,7 +5,7 @@
margin-bottom: sizing.rems(m);
.rose-form-fieldset-title {
@include type.type(s, bold);
@include type.type(s, semibold);
}
.rose-form-fieldset-description {
@@ -21,8 +21,13 @@ $radio-svg: (
);
$data-svg-prefix: 'data:image/svg+xml;utf-8,';
.rose-form-radio {
&.inline {
display: inline-block;
.rose-form-radio-label {
margin-right: sizing.rems(s);
}
}
@include type.type(m);
position: relative;
margin-bottom: sizing.rems(xs);
+3 -3
View File
@@ -73,8 +73,8 @@
"ember-source-channel-url": "^3.0.0",
"ember-template-lint": "^2.15.0",
"ember-try": "^1.4.0",
"eslint": "^7.17.0",
"eslint-config-prettier": "^7.1.0",
"eslint": "^7.31.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-ember": "^10.1.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.3.1",
@@ -83,7 +83,7 @@
"prettier": "^2.2.1",
"qunit": "^2.13.0",
"qunit-dom": "^1.6.0",
"sass": "^1.26.5",
"sass": "^1.36.0",
"sass-lint": "^1.13.1"
},
"engines": {
@@ -0,0 +1,127 @@
<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.attributes.path}}
@label={{t 'resources.credential-library.form.vault_path.label'}}
@error={{@model.errors.attributes_path}}
@helperText={{t 'resources.credential-library.form.vault_path.help'}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.attributes_path}}
<field.errors as |errors|>
{{#each @model.errors.attributes_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.attributes.http_method}}
@changed={{fn (mut @model.attributes.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.attributes.http_request_body}}
@label={{t 'resources.credential-library.form.http_request_body.label'}}
@error={{@model.errors.attributes_http_request_body}}
@helperText={{t
'resources.credential-library.form.http_request_body.help'
}}
@disabled={{this.cannotSaveHttpRequestBody}}
readonly={{false}}
as |field|
>
{{#if @model.errors.attributes_http_request_body}}
<field.errors as |errors|>
{{#each @model.errors.attributes_http_request_body as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.textarea>
{{/if}}
<form.actions
@disabled={{if @model.cannotSave @model.cannotSave}}
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
</Rose::Form>
@@ -0,0 +1,20 @@
import Component from '@glimmer/component';
import { options } from 'api/models/credential-library';
import { computed } from '@ember/object';
export default class FormCredentialLibraryIndexComponent extends Component {
// =properties
/**
* @type {object}
*/
httpMethodOptions = options.http_method;
/**
* Only allow HTTP request body field if http_method is set to POST.
* @type {boolean}
*/
@computed('args.model.attributes.http_method')
get isHttpRequestBodyAllowed() {
return this.args.model.attributes.http_method?.match(/post/i);
}
}
@@ -52,6 +52,188 @@
{{/if}}
</form.textarea>
<form.input
@name='address'
@type='text'
@value={{@model.attributes.address}}
@label={{t 'resources.credential-store.form.address.label'}}
@error={{@model.errors.attributes_address}}
@helperText={{t 'resources.credential-store.form.address.help'}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.attributes_address}}
<field.errors as |errors|>
{{#each @model.errors.attributes_address as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
<form.input
@name='token'
@type='text'
@value={{@model.attributes.token}}
@label={{t 'resources.credential-store.form.token.label'}}
@error={{@model.errors.attributes_token}}
@helperText={{t 'resources.credential-store.form.token.help'}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.attributes_token}}
<field.errors as |errors|>
{{#each @model.errors.attributes_token as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
{{#unless @model.isNew}}
<form.input
@value={{@model.attributes.token_hmac}}
@label={{t 'resources.credential-store.form.token_hmac.label'}}
readonly={{true}}
@disabled={{true}}
/>
{{/unless}}
<form.input
@name='namespace'
@type='text'
@value={{@model.attributes.namespace}}
@label={{t 'resources.credential-store.form.namespace.label'}}
@error={{@model.errors.attributes_namespace}}
@helperText={{t 'resources.credential-store.form.namespace.help'}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.attributes_namespace}}
<field.errors as |errors|>
{{#each @model.errors.attributes_namespace as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
<form.input
@name='tls_server_name'
@type='text'
@value={{@model.attributes.tls_server_name}}
@label={{t 'resources.credential-store.form.tls_server_name.label'}}
@error={{@model.errors.attributes_tls_server_name}}
@helperText={{t 'resources.credential-store.form.tls_server_name.help'}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.attributes_tls_server_name}}
<field.errors as |errors|>
{{#each @model.errors.attributes_tls_server_name as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
<form.checkbox
@label={{t 'resources.credential-store.form.tls_skip_verify.label'}}
@helperText={{t 'resources.credential-store.form.tls_skip_verify.help'}}
@checked={{@model.attributes.tls_skip_verify}}
onChange={{fn
(mut @model.attributes.tls_skip_verify)
(if @model.attributes.tls_skip_verify false true)
}}
@error={{@model.errors.attributes_tls_skip_verify}}
as |field|
>
{{#if @model.errors.attributes_tls_skip_verify}}
<field.errors as |errors|>
{{#each @model.errors.attributes_tls_skip_verify as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.checkbox>
<form.textarea
@name='client_certificate'
@type='text'
@value={{@model.attributes.client_certificate}}
@label={{t 'resources.credential-store.form.client_certificate.label'}}
@error={{@model.errors.attributes_client_certificate}}
@helperText={{t 'resources.credential-store.form.client_certificate.help'}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.attributes_client_certificate}}
<field.errors as |errors|>
{{#each @model.errors.attributes_client_certificate as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.textarea>
<form.textarea
@name='client_certificate_key'
@type='text'
@value={{@model.attributes.client_certificate_key}}
@label={{t 'resources.credential-store.form.client_certificate_key.label'}}
@error={{@model.errors.attributes_client_certificate_key}}
@helperText={{t
'resources.credential-store.form.client_certificate_key.help'
}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.attributes_client_certificate_key}}
<field.errors as |errors|>
{{#each @model.errors.attributes_client_certificate_key as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.textarea>
{{#unless @model.isNew}}
<form.input
@value={{@model.attributes.client_certificate_key_hmac}}
@label={{t
'resources.credential-store.form.client_certificate_key_hmac.label'
}}
readonly={{true}}
@disabled={{true}}
/>
{{/unless}}
<form.textarea
@name='ca_cert'
@type='text'
@value={{@model.attributes.ca_cert}}
@label={{t 'resources.credential-store.form.ca_cert.label'}}
@error={{@model.errors.attributes_ca_cert}}
@helperText={{t 'resources.credential-store.form.ca_cert.help'}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|
>
{{#if @model.errors.attributes_ca_cert}}
<field.errors as |errors|>
{{#each @model.errors.attributes_ca_cert as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.textarea>
<form.actions
@disabled={{if @model.cannotSave @model.cannotSave}}
@enableEditText={{t 'actions.edit-form'}}
@@ -0,0 +1,63 @@
{{#if this.hasAvailableCredentialLibraries}}
<Rose::Form
class='full-width'
@onSubmit={{fn this.submit @submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@error={{@model.errors.base}}
as |form|
>
<form.actions
@submitText={{t 'resources.target.actions.add-credential-libraries'}}
@cancelText={{t 'actions.cancel'}}
/>
<Rose::Table as |table|>
<table.header as |header|>
<header.row as |row|>
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
<row.headerCell>{{t 'form.type.label'}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
{{#each this.filteredCredentialLibraries as |credentialLibrary|}}
<body.row as |row|>
<row.headerCell>
<form.checkbox
@label={{credentialLibrary.id}}
@description={{credentialLibrary.description}}
onChange={{fn this.toggleCredentialLibrary credentialLibrary}}
/>
</row.headerCell>
<row.cell>{{credentialLibrary.name}}</row.cell>
<row.cell>
<Rose::Badge>{{t
(concat
'resources.credential-library.types.' credentialLibrary.type
)
}}</Rose::Badge>
</row.cell>
</body.row>
{{/each}}
</table.body>
</Rose::Table>
</Rose::Form>
{{/if}}
{{#unless this.hasAvailableCredentialLibraries}}
<Rose::Layout::Centered>
<Rose::Message
@title={{t 'resources.target.credential-library.messages.none.title'}}
as |message|
>
<message.description>
{{t 'resources.target.credential-library.messages.none.description'}}
</message.description>
<message.link @route='scopes.scope.targets.target.credential-libraries'>
<Rose::Icon @name='chevron-left' />
{{t 'actions.back'}}
</message.link>
</Rose::Message>
</Rose::Layout::Centered>
{{/unless}}
@@ -0,0 +1,65 @@
import Component from '@glimmer/component';
import { computed, action } from '@ember/object';
import { A } from '@ember/array';
export default class FormTargetAddCredentialLibrariesIndexComponent extends Component {
// =properties
/**
* Array of selected credential library IDs.
* @type {EmberArray}
*/
selectedCredentialLibraryIDs = A();
/**
* Checks for unassigned credential libraries.
* @param {[CredentialLibraryModel]} filteredCredentialLibraries
* @type {boolean}
*/
@computed('filteredCredentialLibraries.length')
get hasAvailableCredentialLibraries() {
return this.filteredCredentialLibraries.length > 0;
}
/**
* Filter out credential libraries not already added to the target.
* @type {[CredentialLibraryModel]}
*/
@computed(
'args.{credentialLibraries.[],model.application_credential_library_ids.[]}'
)
get filteredCredentialLibraries() {
// Get IDs for credential libraries already added to the current target
const currentCredentialLibraryIDs =
this.args.model.application_credential_library_ids.map(
(credentialLibrary) => credentialLibrary.value
);
const notAddedCredentialLibraries = this.args.credentialLibraries.filter(
({ id }) => !currentCredentialLibraryIDs.includes(id)
);
return notAddedCredentialLibraries;
}
// =actions
/**
* Add/Remove credential library to current selection
* @param CredentialLibraryModel
*/
@action
toggleCredentialLibrary(credentialLibrary) {
if (!this.selectedCredentialLibraryIDs.includes(credentialLibrary.id)) {
this.selectedCredentialLibraryIDs.addObject(credentialLibrary.id);
} else {
this.selectedCredentialLibraryIDs.removeObject(credentialLibrary.id);
}
}
/**
* Submit selected credential library ids
*/
@action
submit(fn) {
fn(this.selectedCredentialLibraryIDs);
}
}
@@ -9,7 +9,7 @@
>
<form.actions
@submitText={{t 'actions.add-host-sets'}}
@submitText={{t 'resources.target.actions.add-host-sets'}}
@cancelText={{t 'actions.cancel'}}
/>
@@ -0,0 +1,18 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesController extends Controller {
// =services
@service intl;
// =attributes
/**
* Translated credential libraries breadcrumb
* @type {string}
*/
get breadCrumb() {
return this.intl.t('resources.credential-library.title_plural');
}
}
@@ -0,0 +1,18 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesNewController extends Controller {
// =services
@service intl;
// =attributes
/**
* Translated new credential libraries breadcrumb
* @type {string}
*/
get breadCrumb() {
return this.intl.t('titles.new');
}
}
@@ -0,0 +1,18 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ScopesScopeTargetsTargetCredentialLibrariesController extends Controller {
// =services
@service intl;
// =attributes
/**
* Translated credential library breadcrumb
* @type {string}
*/
get breadCrumb() {
return this.intl.t('resources.credential-library.title_plural');
}
}
+10 -1
View File
@@ -65,6 +65,8 @@ Router.map(function () {
this.route('target', { path: ':target_id' }, function () {
this.route('host-sets', function () {});
this.route('add-host-sets');
this.route('credential-libraries');
this.route('add-credential-libraries');
});
this.route('new');
});
@@ -91,7 +93,14 @@ Router.map(function () {
'credential-store',
{ path: ':credential_store_id' },
function () {
this.route('credential-libraries', function () {});
this.route('credential-libraries', function () {
this.route('new');
this.route(
'credential-library',
{ path: ':credential_library_id' },
function () {}
);
});
}
);
this.route('new');
@@ -1,6 +1,7 @@
import Route from '@ember/routing/route';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeCredentialStoresRoute extends Route {
@@ -44,4 +45,19 @@ export default class ScopesScopeCredentialStoresRoute extends Route {
credentialStore.rollbackAttributes();
if (isNew) this.transitionTo('scopes.scope.credential-stores');
}
/**
* Deletes the credential store and redirects to index
* @param {CredentialStoreModel} credentialStore
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(credentialStore) {
await credentialStore.destroyRecord();
await this.replaceWith('scopes.scope.credential-stores');
this.refresh();
}
}
@@ -10,7 +10,7 @@ export default class ScopesScopeCredentialStoresCredentialStoreRoute extends Rou
}
/**
* Render credential store header and navigation page sections.
* Render credential store header, navigation and actions page sections.
* @override
*/
renderTemplate() {
@@ -25,5 +25,10 @@ export default class ScopesScopeCredentialStoresCredentialStoreRoute extends Rou
into: 'scopes/scope/credential-stores/credential-store',
outlet: 'navigation',
});
this.render('scopes/scope/credential-stores/credential-store/-actions', {
into: 'scopes/scope/credential-stores/credential-store',
outlet: 'actions',
});
}
}
@@ -1,4 +1,8 @@
import Route from '@ember/routing/route';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesRoute extends Route {
// =methods
@@ -13,4 +17,56 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrari
);
return this.store.query('credential-library', { credential_store_id });
}
// =actions
/**
* Rollback changes on a credential library.
* @param {CredentialLibraryModel} credentialLibrary
*/
@action
cancel(credentialLibrary) {
const { isNew } = credentialLibrary;
credentialLibrary.rollbackAttributes();
if (isNew)
this.transitionTo(
'scopes.scope.credential-stores.credential-store.credential-libraries'
);
}
/**
* Handle save of a credential library
* @param {CredentialLibraryModel} credentialLibrary
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success'
)
async save(credentialLibrary) {
await credentialLibrary.save();
await this.transitionTo(
'scopes.scope.credential-stores.credential-store.credential-libraries.credential-library',
credentialLibrary
);
this.refresh();
}
/**
* Handle delete of a credential library
* @param {CredentialLibraryModel} credentialLibrary
*/
@action
@loading
@confirm('questions.delete-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.delete-success')
async delete(credentialLibrary) {
await credentialLibrary.destroyRecord();
await this.replaceWith(
'scopes.scope.credential-stores.credential-store.credential-libraries'
);
this.refresh();
}
}
@@ -0,0 +1,49 @@
import Route from '@ember/routing/route';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesCredentialLibraryRoute extends Route {
// =methods
/**
* Load a credential-library using current credential store and its parent scope.
* @param {object} params
* @param {string} params.credential_library_id
* @return {CredentialLibraryModel}
*/
async model({ credential_library_id }) {
return this.store.findRecord('credential-library', credential_library_id, {
reload: true,
});
}
/**
* Renders the credential library specific templates for header, navigation, and action page sections.
* @override
*/
renderTemplate() {
super.renderTemplate(...arguments);
this.render(
'scopes/scope/credential-stores/credential-store/credential-libraries/credential-library/-header',
{
into: 'scopes/scope/credential-stores/credential-store',
outlet: 'header',
}
);
this.render(
'scopes/scope/credential-stores/credential-store/credential-libraries/credential-library/-navigation',
{
into: 'scopes/scope/credential-stores/credential-store',
outlet: 'navigation',
}
);
this.render(
'scopes/scope/credential-stores/credential-store/credential-libraries/credential-library/-actions',
{
into: 'scopes/scope/credential-stores/credential-store',
outlet: 'actions',
}
);
}
}
@@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesCredentialLibraryIndexRoute extends Route {}
@@ -0,0 +1,46 @@
import Route from '@ember/routing/route';
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesNewRoute extends Route {
// =methods
/**
* Creates a new unsaved credential library in current credential store.
* @return {CredentialLibraryModel}
*/
model() {
const { id: credential_store_id, type } = this.modelFor(
'scopes.scope.credential-stores.credential-store'
);
return this.store.createRecord('credential-library', {
type,
credential_store_id,
});
}
/**
* Renders new credential library specific templates for header, navigation,
* and action page sections.
* @override
*/
renderTemplate() {
super.renderTemplate(...arguments);
this.render(
'scopes/scope/credential-stores/credential-store/credential-libraries/new/-header',
{
into: 'scopes/scope/credential-stores/credential-store',
outlet: 'header',
}
);
this.render('_empty', {
into: 'scopes/scope/credential-stores/credential-store',
outlet: 'navigation',
});
this.render('_empty', {
into: 'scopes/scope/credential-stores/credential-store',
outlet: 'actions',
});
}
}
@@ -0,0 +1,94 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { all } from 'rsvp';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeTargetsTargetAddCredentialLibrariesRoute extends Route {
// =services
@service intl;
@service notify;
// =methods
/**
* Empty out any previously loaded credential libraries.
*/
beforeModel() {
this.store.unloadAll('credential-library');
}
/**
* Returns the current target and all credential libraries.
* @return {{target: TargetModel, credentialLibraries: [CredentialLibraryModel]}}
*/
async model() {
const target = this.modelFor('scopes.scope.targets.target');
const { id: scope_id } = this.modelFor('scopes.scope');
const credentialStores = await this.store.query('credential-store', {
scope_id,
});
await all(
credentialStores.map(({ id: credential_store_id }) =>
this.store.query('credential-library', { credential_store_id })
)
);
const credentialLibraries = this.store.peekAll('credential-library');
return {
target,
credentialLibraries,
};
}
/**
* Renders credential libraries specific header template.
* @override
*/
renderTemplate() {
super.renderTemplate(...arguments);
this.render(
'scopes/scope/targets/target/add-credential-libraries/-header',
{
into: 'scopes/scope/targets/target',
outlet: 'header',
}
);
this.render('-empty', {
into: 'scopes/scope/targets/target',
outlet: 'navigation',
});
this.render('-empty', {
into: 'scopes/scope/targets/target',
outlet: 'actions',
});
}
// =actions
/**
* Add credential libraries to current target
* @param {TargetModel} target
* @param {[string]} credentialLibraryIDs
*/
@action
@loading
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.add-success')
async save(target, credentialLibraryIDs) {
await target.addCredentialLibraries(credentialLibraryIDs);
this.replaceWith('scopes.scope.targets.target.credential-libraries');
}
/**
* Redirect to target credential libraries as if nothing ever happened.
*/
@action
cancel() {
this.replaceWith('scopes.scope.targets.target.credential-libraries');
}
}
@@ -0,0 +1,51 @@
import Route from '@ember/routing/route';
import { all } from 'rsvp';
import { action } from '@ember/object';
import loading from 'ember-loading/decorator';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
export default class ScopesScopeTargetsTargetCredentialLibrariesRoute extends Route {
// =methods
/**
* Loads all credential libraries under the current target.
* @return {Promise{[CredentialLibraryModel]}}
*/
beforeModel() {
const { application_credential_library_ids: credential_libraries } =
this.modelFor('scopes.scope.targets.target');
return all(
credential_libraries.map((credential_library) =>
this.store.findRecord('credential-library', credential_library.value, {
reload: true,
})
)
);
}
/**
* Returns the previously loaded target instance.
* @return {TargetModel}
*/
model() {
return this.modelFor('scopes.scope.targets.target');
}
// =actions
/**
* Remove a credential library from the current target.
* @param {TargetModel} target
* @param {CredentialLibraryModel} credentialLibrary
*/
@action
@loading
@confirm('questions.remove-confirm')
@notifyError(({ message }) => message, { catch: true })
@notifySuccess('notifications.remove-success')
async removeCredentialLibrary(target, credentialLibrary) {
await target.removeCredentialLibrary(credentialLibrary.id);
this.refresh();
}
}
+1 -1
View File
@@ -85,7 +85,7 @@
</nav.link>
{{#if (feature-flag 'credential-store')}}
<nav.link @route='scopes.scope.credential-stores'>
<Rose::Icon @name='app-icons/credential' @size='medium' />
<Rose::Icon @name='app-icons/credentials' @size='medium' />
{{t 'resources.credential-store.title_plural'}}
</nav.link>
{{/if}}
@@ -15,7 +15,7 @@
</page.navigation>
<page.actions>
{{!-- {{outlet 'actions'}} --}}
{{outlet 'actions'}}
</page.actions>
<page.body>
@@ -0,0 +1,20 @@
<Rose::Dropdown
@text={{t 'actions.manage'}}
@dropdownRight={{true}}
as |dropdown|
>
<dropdown.link
@route='scopes.scope.credential-stores.credential-store.credential-libraries.new'
@model={{@model}}
>
{{t 'resources.credential-library.actions.create'}}
</dropdown.link>
<dropdown.separator />
<dropdown.button
@style='danger'
@disabled={{@model.canSave}}
{{on 'click' (route-action 'delete' @model)}}
>
{{t 'resources.credential-store.actions.delete'}}
</dropdown.button>
</Rose::Dropdown>
@@ -0,0 +1,3 @@
{{page-title @model.displayName}}
{{outlet}}
@@ -0,0 +1,13 @@
<Rose::Dropdown
@text={{t 'actions.manage'}}
@dropdownRight={{true}}
as |dropdown|
>
<dropdown.button
@style='danger'
@disabled={{@model.canSave}}
{{on 'click' (route-action 'delete' @model)}}
>
{{t 'resources.credential-library.actions.delete'}}
</dropdown.button>
</Rose::Dropdown>
@@ -0,0 +1,12 @@
<h2>
{{t 'resources.credential-library.title'}}
<DocLink @doc='credential-library' @iconSize='large' />
</h2>
<p>{{t 'resources.credential-library.description'}}</p>
<Copyable
@text={{@model.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<code>{{@model.id}}</code>
</Copyable>
@@ -0,0 +1,7 @@
<Rose::Nav::Tabs as |nav|>
<nav.link
@route='scopes.scope.credential-stores.credential-store.credential-libraries.credential-library.index'
>
{{t 'titles.details'}}
</nav.link>
</Rose::Nav::Tabs>
@@ -0,0 +1,5 @@
<Form::CredentialLibrary
@model={{@model}}
@submit={{route-action 'save' @model}}
@cancel={{route-action 'cancel' @model}}
/>
@@ -4,6 +4,7 @@
<table.header as |header|>
<header.row as |row|>
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
<row.headerCell>{{t 'form.type.label'}}</row.headerCell>
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
</header.row>
</table.header>
@@ -11,9 +12,21 @@
{{#each @model as |credentialLibrary|}}
<body.row as |row|>
<row.headerCell>
{{credentialLibrary.displayName}}
<LinkTo
@route='scopes.scope.credential-stores.credential-store.credential-libraries.credential-library'
@model={{credentialLibrary.id}}
>
{{credentialLibrary.displayName}}
</LinkTo>
<p>{{credentialLibrary.description}}</p>
</row.headerCell>
<row.cell>
<Rose::Badge>{{t
(concat
'resources.credential-library.types.' credentialLibrary.type
)
}}</Rose::Badge>
</row.cell>
<row.cell>
<Copyable
@text={{credentialLibrary.id}}
@@ -38,6 +51,12 @@
<message.description>
{{t 'resources.credential-library.messages.none.description'}}
</message.description>
<message.link
@route='scopes.scope.credential-stores.credential-store.credential-libraries.new'
>
<Rose::Icon @name='plus-circle-outline' />
{{t 'titles.new'}}
</message.link>
</Rose::Message>
</Rose::Layout::Centered>
@@ -0,0 +1,7 @@
{{page-title (t 'titles.new')}}
<Form::CredentialLibrary
@model={{@model}}
@submit={{route-action 'save' @model}}
@cancel={{route-action 'cancel' @model}}
/>
@@ -0,0 +1,3 @@
<h2>
{{t 'titles.new'}}
</h2>
@@ -6,9 +6,11 @@
<page.header>
<h2>
{{t 'resources.credential-store.title_plural'}}
<DocLink @doc='credential-stores' @iconSize='large' />
<DocLink @doc='credential-store' @iconSize='large' />
</h2>
<p>{{t 'resources.credential-store.description'}}</p>
{{#if @model}}
<p>{{t 'resources.credential-store.description'}}</p>
{{/if}}
</page.header>
<page.actions>
@@ -46,7 +48,11 @@
<p>{{credentialStore.description}}</p>
</row.headerCell>
<row.cell>
<Rose::Badge>{{credentialStore.type}}</Rose::Badge>
<Rose::Badge>{{t
(concat
'resources.credential-store.types.' credentialStore.type
)
}}</Rose::Badge>
</row.cell>
<row.cell>
<Copyable
@@ -61,6 +67,25 @@
{{/each}}
</table.body>
</Rose::Table>
{{else}}
<Rose::Layout::Centered>
<Rose::Message
@title={{t 'resources.credential-store.messages.welcome.title'}}
as |message|
>
<message.description>
{{t 'resources.credential-store.description'}}
</message.description>
<message.link @route='scopes.scope.credential-stores.new'>
<Rose::Icon @name='plus-circle-outline' />
{{t 'titles.new'}}
</message.link>
</Rose::Message>
</Rose::Layout::Centered>
{{/if}}
</page.body>
</Rose::Layout::Page>
@@ -6,6 +6,13 @@
<dropdown.link @route='scopes.scope.targets.target.add-host-sets'>
{{t 'resources.target.actions.add-host-sets'}}
</dropdown.link>
{{#if (feature-flag 'credential-store')}}
<dropdown.link
@route='scopes.scope.targets.target.add-credential-libraries'
>
{{t 'resources.target.actions.add-credential-libraries'}}
</dropdown.link>
{{/if}}
<dropdown.separator />
<dropdown.button
@style='danger'
@@ -3,6 +3,11 @@
{{t 'titles.details'}}
</nav.link>
<nav.link @route='scopes.scope.targets.target.host-sets'>
{{t 'resources.host-set.title_plural'}}
{{t 'resources.target.host-set.title_plural'}}
</nav.link>
{{#if (feature-flag 'credential-store')}}
<nav.link @route='scopes.scope.targets.target.credential-libraries'>
{{t 'resources.target.credential-library.title_plural'}}
</nav.link>
{{/if}}
</Rose::Nav::Tabs>
@@ -0,0 +1,27 @@
{{page-title (t 'resources.target.actions.add-credential-libraries')}}
{{#if @model.credentialLibraries}}
<Form::Target::AddCredentialLibraries
@model={{@model.target}}
@credentialLibraries={{@model.credentialLibraries}}
@submit={{route-action 'save' @model.target}}
@cancel={{route-action 'cancel'}}
/>
{{/if}}
{{#unless @model.credentialLibraries}}
<Rose::Layout::Centered>
<Rose::Message
@title={{t 'resources.target.credential-library.messages.none.title'}}
as |message|
>
<message.description>
{{t 'resources.target.credential-library.messages.none.description'}}
</message.description>
<message.link @route='scopes.scope.targets.target.credential-libraries'>
<Rose::Icon @name='chevron-left' />
{{t 'actions.back'}}
</message.link>
</Rose::Message>
</Rose::Layout::Centered>
{{/unless}}
@@ -0,0 +1,4 @@
<h2>
{{t 'resources.target.credential-library.messages.add.title'}}
</h2>
<p>{{t 'resources.target.credential-library.messages.add.description'}}</p>
@@ -0,0 +1,91 @@
{{page-title (t 'resources.target.credential-library.title_plural')}}
{{#if @model.credentialLibraries}}
<Rose::Table as |table|>
<table.header as |header|>
<header.row as |row|>
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
<row.headerCell>{{t 'form.type.label'}}</row.headerCell>
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
<row.headerCell>{{t 'titles.actions'}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
{{#each @model.credentialLibraries as |credentialLibrary|}}
<body.row as |row|>
<row.headerCell>
<LinkTo
@route='scopes.scope.credential-stores.credential-store.credential-libraries.credential-library'
@models={{array
credentialLibrary.credential_store_id
credentialLibrary.id
}}
>
{{credentialLibrary.displayName}}
</LinkTo>
<p>{{credentialLibrary.description}}</p>
</row.headerCell>
<row.cell>
<Rose::Badge>{{t
(concat
'resources.credential-library.types.' credentialLibrary.type
)
}}</Rose::Badge>
</row.cell>
<row.cell>
<Copyable
@text={{credentialLibrary.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<code>{{credentialLibrary.id}}</code>
</Copyable>
</row.cell>
<row.cell>
<Rose::Dropdown
@icon='more-horizontal'
@iconOnly={{true}}
@showCaret={{false}}
@dropdownRight={{true}}
@text={{t 'actions.manage'}}
as |dropdown|
>
<dropdown.button
@style='danger'
{{on
'click'
(route-action
'removeCredentialLibrary' @model credentialLibrary
)
}}
>
{{t 'actions.remove'}}
</dropdown.button>
</Rose::Dropdown>
</row.cell>
</body.row>
{{/each}}
</table.body>
</Rose::Table>
{{else}}
<Rose::Layout::Centered>
<Rose::Message
@title={{t 'resources.target.credential-library.messages.welcome.title'}}
as |message|
>
<message.description>
{{t 'resources.target.credential-library.messages.welcome.description'}}
</message.description>
<message.link
@route='scopes.scope.targets.target.add-credential-libraries'
>
<Rose::Icon @name='plus-circle-outline' />
{{t 'resources.target.actions.add-credential-libraries'}}
</message.link>
</Rose::Message>
</Rose::Layout::Centered>
{{/if}}
@@ -1,4 +1,4 @@
{{page-title (t 'resources.host-set.title_plural')}}
{{page-title (t 'resources.target.host-set.title_plural')}}
{{#if @model.hostSets}}
+7 -7
View File
@@ -55,7 +55,8 @@ module.exports = function (environment) {
'host-set': '/host-sets',
'host-set.new': '/host-sets/new',
'host-set.add-hosts': '/host-sets/add-hosts',
'credential-stores': '/credential-stores',
'credential-store': '/credential-stores',
'credential-library': '/credential-libraries',
host: '/hosts',
'host.new': '/hosts/new',
role: '/roles',
@@ -94,7 +95,7 @@ module.exports = function (environment) {
search: false,
filter: false,
capabilities: false,
'credential-store': false,
'credential-store': true,
},
};
@@ -130,11 +131,7 @@ module.exports = function (environment) {
if (API_HOST) ENV.contentSecurityPolicy['connect-src'].push(API_HOST);
// Enable features in development
ENV.featureFlags['primary-auth-method'] = true;
ENV.featureFlags['oidc'] = true;
ENV.featureFlags['oidc-crud'] = true;
ENV.featureFlags['credential-store'] = true;
//ENV.featureFlags['oidc-account-crud'] = true;
// ENV.featureFlags['credential-store'] = true;
}
if (environment === 'test') {
@@ -157,6 +154,9 @@ module.exports = function (environment) {
ENV.sessionPollingTimeoutSeconds = 0.25;
ENV.oidcPollingTimeoutSeconds = 0;
ENV.enableConfirmService = false;
// Enable tests for development features
// ENV.featureFlags['credential-store'] = true;
}
if (environment === 'production') {
+4 -4
View File
@@ -51,7 +51,7 @@
"ember-auto-import": "^1.10.1",
"ember-breadcrumbs": "^0.2.3",
"ember-cli": "~3.27.0",
"ember-cli-app-version": "^4.0.0",
"ember-cli-app-version": "^5.0.0",
"ember-cli-babel": "^7.23.0",
"ember-cli-code-coverage": "^1.0.2",
"ember-cli-content-security-policy": "^1.1.1",
@@ -77,8 +77,8 @@
"ember-source": "~3.24.0",
"ember-template-lint": "^2.15.0",
"ember-template-lint-plugin-prettier": "^2.0.0",
"eslint": "^7.17.0",
"eslint-config-prettier": "^7.1.0",
"eslint": "^7.31.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-ember": "^10.1.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.3.1",
@@ -89,7 +89,7 @@
"qunit": "^2.13.0",
"qunit-dom": "^1.6.0",
"rose": "*",
"sass": "^1.29.0",
"sass": "^1.36.0",
"sass-lint": "^1.13.1",
"sinon": "^11.1.1"
},
@@ -0,0 +1,298 @@
import { module, test } from 'qunit';
import { visit, click, fillIn, currentURL, find } 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 { resolve, reject } from 'rsvp';
import sinon from 'sinon';
module('Acceptance | credential-library', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let getCredentialLibraryCount;
const instances = {
scopes: {
global: null,
org: null,
project: null,
},
};
const urls = {
globalScope: null,
orgScope: null,
projectScope: null,
credentialStores: null,
credentialStore: null,
credentialLibrary: null,
credentialLibraries: null,
newCredentialLibrary: null,
unknownCredentialLibrary: null,
};
hooks.beforeEach(function () {
// Generate resources
instances.scopes.global = this.server.create('scope', { id: 'global' });
instances.scopes.org = this.server.create('scope', {
type: 'org',
scope: { id: 'global', type: 'global' },
});
instances.scopes.project = this.server.create('scope', {
type: 'project',
scope: { id: instances.scopes.org.id, type: 'org' },
});
instances.credentialStore = this.server.create('credential-store', {
scope: instances.scopes.project,
});
instances.credentialLibrary = this.server.create('credential-library', {
scope: instances.scopes.project,
});
// Generate route URLs for resources
urls.globalScope = `/scopes/global/scopes`;
urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`;
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
urls.credentialStores = `${urls.projectScope}/credential-stores`;
urls.credentialStore = `${urls.credentialStores}/${instances.credentialStore.id}`;
urls.credentialLibraries = `${urls.credentialStore}/credential-libraries`;
urls.credentialLibrary = `${urls.credentialLibraries}/${instances.credentialLibrary.id}`;
urls.newCredentialLibrary = `${urls.credentialLibraries}/new`;
urls.unknownCredentialLibrary = `${urls.credentialLibraries}/foo`;
// Generate resource counter
getCredentialLibraryCount = () =>
this.server.schema.credentialLibraries.all().models.length;
authenticateSession({});
});
test('visiting credential libraries', async function (assert) {
assert.expect(2);
await visit(urls.credentialLibraries);
await a11yAudit();
assert.equal(currentURL(), urls.credentialLibraries);
await visit(urls.credentialLibrary);
await a11yAudit();
assert.equal(currentURL(), urls.credentialLibrary);
});
test('visiting an unknown credential library displays 404 message', async function (assert) {
assert.expect(1);
await visit(urls.unknownCredentialLibrary);
await a11yAudit();
console.debug(find('.rose-message-subtitle'));
assert.ok(find('.rose-message-subtitle').textContent.trim(), 'Error 404');
});
test('can create a credential library', async function (assert) {
assert.expect(1);
const count = getCredentialLibraryCount();
await visit(urls.newCredentialLibrary);
await fillIn('[name="name"]', 'random string');
await click('[type="submit"]');
assert.equal(getCredentialLibraryCount(), count + 1);
});
test('can cancel create a new credential library', async function (assert) {
assert.expect(2);
const count = getCredentialLibraryCount();
await visit(urls.newCredentialLibrary);
await fillIn('[name="name"]', 'random string');
await click('.rose-form-actions [type="button"]');
assert.equal(currentURL(), urls.credentialLibraries);
assert.equal(getCredentialLibraryCount(), count);
});
test('saving a new credential library with invalid fields displays error messasges', async function (assert) {
assert.expect(2);
this.server.post('/credential-libraries', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: 'The request was invalid',
details: {
request_fields: [
{
name: 'name',
description: 'Name is required.',
},
],
},
}
);
});
await visit(urls.newCredentialLibrary);
await click('[type="submit"]');
assert.ok(
find('[role="alert"]').textContent.trim(),
'The request was invalid.'
);
assert.ok(
find('.rose-form-error-message').textContent.trim(),
'Name is required.'
);
});
test('can save changes to existing credential library', async function (assert) {
assert.expect(3);
assert.notEqual(instances.credentialLibrary.name, 'random string');
await visit(urls.credentialLibrary);
await click('form [type="button"]', 'Activate edit mode');
await fillIn('[name="name"]', 'random string');
await click('.rose-form-actions [type="submit"]');
assert.equal(currentURL(), urls.credentialLibrary);
assert.equal(
this.server.schema.credentialLibraries.all().models[0].name,
'random string'
);
});
test('can cancel changes to existing credential library', async function (assert) {
assert.expect(2);
await visit(urls.credentialLibrary);
await click('form [type="button"]', 'Activate edit mode');
await fillIn('[name="name"]', 'random string');
await click('.rose-form-actions [type="button"]');
assert.notEqual(
this.server.schema.credentialLibraries.all().models[0].name,
'random string'
);
assert.equal(find('[name="name"]').value, instances.credentialLibrary.name);
});
test('saving an existing credential library with invalid fields displays error messages', async function (assert) {
assert.expect(2);
this.server.patch('/credential-libraries/:id', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: 'The request was invalid.',
details: {
request_fields: [
{
name: 'name',
description: 'Name is required',
},
],
},
}
);
});
await visit(urls.credentialLibrary);
await click('form [type="button"]', 'Activate edit mode');
await fillIn('[name="name"]', 'random string');
await click('[type="submit"]');
assert.ok(
find('[role="alert"]').textContent.trim(),
'The request was invalid.'
);
assert.ok(
find('.rose-form-error-message').textContent.trim(),
'Name is required.'
);
});
test('can discard unsaved credential library changes via dialog', async function (assert) {
assert.expect(5);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
assert.notEqual(instances.credentialLibrary.name, 'random string');
await visit(urls.credentialLibrary);
await click('form [type="button"]', 'Activate edit mode');
await fillIn('[name="name"]', 'random string');
assert.equal(currentURL(), urls.credentialLibrary);
try {
await visit(urls.credentialLibraries);
} catch (e) {
assert.ok(find('.rose-dialog'));
await click('.rose-dialog-footer button:first-child', 'Click Discard');
assert.equal(currentURL(), urls.credentialLibraries);
assert.notEqual(
this.server.schema.credentialLibraries.all().models[0].name,
'random string'
);
}
});
test('can cancel discard unsaved credential library via dialog', async function (assert) {
assert.expect(5);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
assert.notEqual(instances.credentialLibrary.name, 'random string');
await visit(urls.credentialLibrary);
await click('form [type="button"]', 'Activate edit mode');
await fillIn('[name="name"]', 'random string');
assert.equal(currentURL(), urls.credentialLibrary);
try {
await visit(urls.credentialLibraries);
} catch (e) {
assert.ok(find('.rose-dialog'));
await click('.rose-dialog-footer button:last-child');
assert.equal(currentURL(), urls.credentialLibrary);
assert.notEqual(
this.server.schema.credentialLibraries.all().models[0].name,
'random string'
);
}
});
test('can delete credential library', async function (assert) {
assert.expect(1);
const count = getCredentialLibraryCount();
await visit(urls.credentialLibrary);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(getCredentialLibraryCount(), count - 1);
});
test('can accept delete credential library via dialog', async function (assert) {
assert.expect(2);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
confirmService.confirm = sinon.fake.returns(resolve());
const count = getCredentialLibraryCount();
await visit(urls.credentialLibrary);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(getCredentialLibraryCount(), count - 1);
assert.ok(confirmService.confirm.calledOnce);
});
test('cannot cancel delete credential library via dialog', async function (assert) {
assert.expect(2);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
confirmService.confirm = sinon.fake.returns(reject());
const count = getCredentialLibraryCount();
await visit(urls.credentialLibrary);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(getCredentialLibraryCount(), count);
assert.ok(confirmService.confirm.calledOnce);
});
test('deleting a credential library which errors displays error messages', async function (assert) {
assert.expect(1);
this.server.del('/credential-libraries/:id', () => {
return new Response(
490,
{},
{
status: 490,
code: 'error',
message: 'Oops.',
}
);
});
await visit(urls.credentialLibrary);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.ok(find('[role="alert"]').textContent.trim(), 'Oops.');
});
});
@@ -5,6 +5,8 @@ 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 { resolve, reject } from 'rsvp';
import sinon from 'sinon';
module('Acceptance | credential-stores', function (hooks) {
setupApplicationTest(hooks);
@@ -236,4 +238,54 @@ module('Acceptance | credential-stores', function (hooks) {
);
}
});
test('can delete credential store', async function (assert) {
assert.expect(1);
const count = getCredentialStoresCount();
await visit(urls.credentialStore);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(getCredentialStoresCount(), count - 1);
});
test('can accept delete credential store via dialog', async function (assert) {
assert.expect(2);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
confirmService.confirm = sinon.fake.returns(resolve());
const count = getCredentialStoresCount();
await visit(urls.credentialStore);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(getCredentialStoresCount(), count - 1);
assert.ok(confirmService.confirm.calledOnce);
});
test('cannot cancel delete credential store via dialog', async function (assert) {
assert.expect(2);
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
confirmService.confirm = sinon.fake.returns(reject());
const count = getCredentialStoresCount();
await visit(urls.credentialStore);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.equal(getCredentialStoresCount(), count);
assert.ok(confirmService.confirm.calledOnce);
});
test('deleting a credential store which errors displays error messages', async function (assert) {
assert.expect(1);
this.server.del('/credential-stores/:id', () => {
return new Response(
490,
{},
{
status: 490,
code: 'error',
message: 'Oops.',
}
);
});
await visit(urls.credentialStore);
await click('.rose-layout-page-actions .rose-dropdown-button-danger');
assert.ok(find('[role="alert"]').textContent.trim(), 'Oops.');
});
});
+1 -1
View File
@@ -55,7 +55,7 @@ module('Acceptance | sessions', function (hooks) {
'target',
1,
{ scope: instances.scopes.project },
'withRandomHostSets'
'withAssociations'
);
instances.sessions = this.server.createList(
'session',
@@ -0,0 +1,183 @@
import { module, test } from 'qunit';
import { visit, find, findAll, click, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { Response } from 'miragejs';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import {
authenticateSession,
// These are left here intentionally for future reference.
//currentSession,
//invalidateSession,
} from 'ember-simple-auth/test-support';
module('Acceptance | targets | credential-libraries', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let getCredentialLibraryCount;
const instances = {
scopes: {
global: null,
org: null,
project: null,
},
credentialStore: null,
credentialLibraries: null,
credentialLibrary: null,
};
const urls = {
globalScope: null,
orgScope: null,
projectScope: null,
targets: null,
target: null,
credentialLibraries: null,
credentialLibrary: null,
addCredentialLibraries: null,
};
hooks.beforeEach(function () {
// Generate resources
instances.scopes.global = this.server.create('scope', { id: 'global' });
instances.scopes.org = this.server.create('scope', {
type: 'org',
scope: { id: 'global', type: 'global' },
});
instances.scopes.project = this.server.create('scope', {
type: 'project',
scope: { id: instances.scopes.org.id, type: 'org' },
});
instances.credentialStore = this.server.create('credential-store', {
type: 'vault',
scope: instances.scopes.project,
});
instances.credentialLibraries = this.server.createList(
'credential-library',
2,
{
scope: instances.scopes.project,
credentialStore: instances.credentialStore,
}
);
instances.credentialLibrary = instances.credentialLibraries[0];
instances.target = this.server.create('target', {
scope: instances.scopes.project,
credentialLibraries: instances.credentialLibraries,
});
// Generate route URLs for resources
urls.globalScope = `/scopes/global/scopes`;
urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`;
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
urls.targets = `${urls.projectScope}/targets`;
urls.target = `${urls.targets}/${instances.target.id}`;
urls.credentialLibraries = `${urls.target}/credential-libraries`;
urls.credentialLibrary = `${urls.projectScope}/credential-stores/${instances.credentialLibrary.credentialStoreId}/credential-libraries/${instances.credentialLibrary.id}`;
urls.addCredentialLibraries = `${urls.target}/add-credential-libraries`;
// Generate resource counter
getCredentialLibraryCount = () =>
this.server.schema.credentialLibraries.all().models.length;
authenticateSession({});
});
test('visiting target credential libraries', async function (assert) {
assert.expect(2);
await visit(urls.credentialLibraries);
await a11yAudit();
assert.equal(currentURL(), urls.credentialLibraries);
assert.equal(findAll('tbody tr').length, getCredentialLibraryCount());
});
test('can navigate to a credential library', async function (assert) {
assert.expect(1);
await visit(urls.credentialLibraries);
await click('main tbody .rose-table-header-cell:nth-child(1) a');
await a11yAudit();
assert.equal(currentURL(), urls.credentialLibrary);
});
test('visiting add credential libraries', async function (assert) {
assert.expect(1);
await visit(urls.addCredentialLibraries);
await a11yAudit();
assert.equal(currentURL(), urls.addCredentialLibraries);
});
test('can select and save credential libraries to add', async function (assert) {
assert.expect(4);
instances.target.update({ credentialLibraries: [] });
await visit(urls.credentialLibraries);
assert.equal(findAll('tbody tr').length, 0);
await visit(urls.addCredentialLibraries);
assert.equal(findAll('tbody tr').length, getCredentialLibraryCount());
await click('tbody label');
await click('form [type="submit"]');
assert.equal(currentURL(), urls.credentialLibraries);
assert.equal(findAll('tbody tr').length, 1);
});
test('can select and cancel credential libraries to add', async function (assert) {
assert.expect(4);
instances.target.update({ credentialLibraries: [] });
await visit(urls.credentialLibraries);
assert.equal(findAll('tbody tr').length, 0);
await visit(urls.addCredentialLibraries);
assert.equal(findAll('tbody tr').length, getCredentialLibraryCount());
await click('tbody label');
await click('form [type="button"]');
assert.equal(currentURL(), urls.credentialLibraries);
assert.equal(findAll('tbody tr').length, 0);
});
test('adding credential libraries which errors displays error message', async function (assert) {
assert.expect(1);
this.server.post('/targets/:idMethod', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: 'The request was invalid.',
details: {},
}
);
});
instances.target.update({ credentialLibraries: [] });
await visit(urls.addCredentialLibraries);
await click('tbody label');
await click('form [type="submit"]');
assert.ok(find('[role="alert"]'));
});
test('can remove a credential library', async function (assert) {
assert.expect(2);
const count = getCredentialLibraryCount();
await visit(urls.credentialLibraries);
assert.equal(findAll('tbody tr').length, count);
await click('tbody tr .rose-dropdown-button-danger');
assert.equal(findAll('tbody tr').length, count - 1);
});
test('removing a target credential library which errors displays error messages', async function (assert) {
assert.expect(2);
this.server.post('/targets/:idMethod', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: 'The request was invalid.',
details: {},
}
);
});
const count = getCredentialLibraryCount();
await visit(urls.credentialLibraries);
assert.equal(findAll('tbody tr').length, count);
await click('tbody tr .rose-dropdown-button-danger');
assert.ok(find('[role="alert"]'));
});
});
@@ -0,0 +1,17 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module(
'Unit | Controller | scopes/scope/credential-stores/credential-store/credential-libraries',
function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/credential-stores/credential-store/credential-libraries'
);
assert.ok(controller);
});
}
);
@@ -0,0 +1,17 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module(
'Unit | Controller | scopes/scope/credential-stores/credential-store/credential-libraries/new',
function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/credential-stores/credential-store/credential-libraries/new'
);
assert.ok(controller);
});
}
);
@@ -0,0 +1,17 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module(
'Unit | Controller | scopes/scope/targets/target/credential-libraries',
function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/targets/target/credential-libraries'
);
assert.ok(controller);
});
}
);
@@ -0,0 +1,16 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module(
'Unit | Route | scopes/scope/credential-stores/credential-store/credential-libraries/credential-library',
function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup(
'route:scopes/scope/credential-stores/credential-store/credential-libraries/credential-library'
);
assert.ok(route);
});
}
);
@@ -0,0 +1,16 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module(
'Unit | Route | scopes/scope/credential-stores/credential-store/credential-libraries/credential-library/index',
function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup(
'route:scopes/scope/credential-stores/credential-store/credential-libraries/credential-library/index'
);
assert.ok(route);
});
}
);
@@ -0,0 +1,16 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module(
'Unit | Route | scopes/scope/credential-stores/credential-store/credential-libraries/new',
function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup(
'route:scopes/scope/credential-stores/credential-store/credential-libraries/new'
);
assert.ok(route);
});
}
);
@@ -0,0 +1,16 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module(
'Unit | Route | scopes/scope/targets/target/add-credential-libraries',
function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup(
'route:scopes/scope/targets/target/add-credential-libraries'
);
assert.ok(route);
});
}
);
@@ -0,0 +1,16 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module(
'Unit | Route | scopes/scope/targets/target/credential-libraries',
function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup(
'route:scopes/scope/targets/target/credential-libraries'
);
assert.ok(route);
});
}
);
+8
View File
@@ -2,6 +2,14 @@
Canonical reference for changes, improvements, and bugfixes for Boundary Desktop.
## Next
## v1.2.1 [CLI 0.5.0] (2021.08.03)
### New
- Add secret toggle capability to connection details dialog. ([PR](https://github.com/hashicorp/boundary-ui/pull/637))
## v1.2.0 [CLI 0.4.0] (2021.06.29)
### New
-13
View File
@@ -1,5 +1,4 @@
import Route from '@ember/routing/route';
import jQuery from 'jquery';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { A } from '@ember/array';
@@ -31,18 +30,6 @@ export default class ApplicationRoute extends Route {
return this.origin.updateOrigin();
}
/**
* Adds listener on target=_blank links so that they may be opened in an
* external browser.
*/
afterModel() {
const ipc = this.ipc;
/* eslint-disable-next-line ember/no-jquery */
jQuery(document).on('click', 'a[href][target="_blank"]', function () {
ipc.invoke('openExternal', this.href);
});
}
/**
* Add OS config on controller
*/
+16 -6
View File
@@ -5,7 +5,6 @@ const https = require('https');
const decompress = require('decompress');
const { isMac, isWindows } = require('../src/helpers/platform.js');
const artifactVersion = '0.4.0';
const artifactDestination = path.resolve(__dirname, '..', 'cli');
const downloadArtifact = (version) => {
@@ -28,11 +27,13 @@ const downloadArtifact = (version) => {
console.log('Create tmp artifact directory: ', tmpDir);
const artifactFileName = url.split('/').pop();
if (!artifactFileName) reject('Could not find artifact filename in: ', url);
if (!artifactFileName)
reject(`Could not find artifact filename in: ${url}.`);
const artifactPath = path.resolve(tmpDir, artifactFileName);
console.log('Download artifact url: ', url);
https.get(url, (response) => {
if (response.statusCode >= 400) reject('Could not access artifact url.');
const stream = response.pipe(fs.createWriteStream(artifactPath));
stream.on('close', () => resolve(artifactPath));
stream.on('error', reject);
@@ -52,7 +53,16 @@ module.exports = {
console.warn('WARNING: Bypassing cli setup');
return;
}
const artifactPath = await downloadArtifact(artifactVersion);
await extract(artifactPath, artifactDestination);
}
}
try {
const artifactVersion = await fs.promises.readFile(
path.resolve(__dirname, 'cli', 'VERSION'),
'utf8'
);
const artifactPath = await downloadArtifact(artifactVersion);
await extract(artifactPath, artifactDestination);
} catch (e) {
console.error('ERROR: Failed setting up CLI.', e);
process.exit(1);
}
},
};
@@ -0,0 +1 @@
0.4.0
+7 -1
View File
@@ -12,6 +12,7 @@ const {
BrowserWindow,
Menu,
MenuItem,
shell,
} = require('electron');
require('./ipc/handlers.js');
@@ -166,9 +167,14 @@ app.on('ready', async () => {
/* eng-disable LIMIT_NAVIGATION_JS_CHECK */
if (!url.startsWith('serve://boundary')) event.preventDefault();
});
// Opens external links in the host default browser.
// We just allow boundaryproject.io domain to open on external window (for now).
mainWindow.webContents.on('new-window', (event, url) => {
/* eng-disable LIMIT_NAVIGATION_JS_CHECK */
if (!url.startsWith('serve://boundary')) event.preventDefault();
event.preventDefault();
if (url.startsWith('https://boundaryproject.io/')) {
shell.openExternal(url);
}
});
mainWindow.on('unresponsive', () => {
+4 -5
View File
@@ -49,7 +49,6 @@
},
"devDependencies": {
"@doyensec/electronegativity": "^1.8.1",
"@ember/jquery": "^1.1.0",
"@ember/optional-features": "^2.0.0",
"@ember/render-modifiers": "^1.0.2",
"@ember/test-helpers": "^2.2.8",
@@ -67,7 +66,7 @@
"ember-auto-import": "^1.10.1",
"ember-autofocus-modifier": "^1.1.0",
"ember-cli": "~3.27.0",
"ember-cli-app-version": "^4.0.0",
"ember-cli-app-version": "^5.0.0",
"ember-cli-babel": "^7.23.0",
"ember-cli-code-coverage": "^1.0.2",
"ember-cli-dependency-checker": "^3.2.0",
@@ -92,8 +91,8 @@
"ember-source": "~3.24.0",
"ember-template-lint": "^2.15.0",
"ember-template-lint-plugin-prettier": "^2.0.0",
"eslint": "^7.17.0",
"eslint-config-prettier": "^7.1.0",
"eslint": "^7.31.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-ember": "^10.1.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.3.1",
@@ -104,7 +103,7 @@
"qunit": "^2.13.0",
"qunit-dom": "^1.6.0",
"rose": "*",
"sass": "^1.29.0",
"sass": "^1.36.0",
"sinon": "^11.1.1",
"stylelint": "^13.8.0",
"stylelint-config-sass-guidelines": "^7.1.0"
+1 -1
View File
@@ -97,7 +97,7 @@ module('Acceptance | origin', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
'withRandomHostSets'
'withAssociations'
);
urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
+1 -1
View File
@@ -92,7 +92,7 @@ module('Acceptance | projects', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
'withRandomHostSets'
'withAssociations'
);
urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
@@ -97,7 +97,7 @@ module('Acceptance | projects | sessions', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
'withRandomHostSets'
'withAssociations'
);
instances.session = this.server.create(
'session',
@@ -101,7 +101,7 @@ module('Acceptance | projects | targets', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
'withRandomHostSets'
'withAssociations'
);
instances.session = this.server.create(
@@ -106,7 +106,7 @@ module('Acceptance | projects | targets | sessions', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
'withRandomHostSets'
'withAssociations'
);
instances.session = this.server.create(
+1 -1
View File
@@ -110,7 +110,7 @@ module('Acceptance | scopes', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
'withRandomHostSets'
'withAssociations'
);
instances.session = this.server.create(
+1 -1
View File
@@ -94,7 +94,7 @@ module('Acceptance | targets', function (hooks) {
instances.target = this.server.create(
'target',
{ scope: instances.scopes.project },
'withRandomHostSets'
'withAssociations'
);
urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
+2158 -2703
View File
File diff suppressed because it is too large. Load diff