chore: manual merge from main
This commit is contained in:
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.',
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -3,4 +3,5 @@ import { Model, belongsTo, hasMany } from 'ember-cli-mirage';
|
||||
export default Model.extend({
|
||||
scope: belongsTo(),
|
||||
hostSets: hasMany(),
|
||||
credentialLibraries: hasMany(),
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {},
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 |
@@ -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);
|
||||
|
||||
@@ -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'}}
|
||||
/>
|
||||
|
||||
|
||||
+18
@@ -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');
|
||||
}
|
||||
}
|
||||
+18
@@ -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
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
+56
@@ -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();
|
||||
}
|
||||
}
|
||||
+49
@@ -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',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeCredentialStoresCredentialStoreCredentialLibrariesCredentialLibraryIndexRoute extends Route {}
|
||||
+46
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{{page-title @model.displayName}}
|
||||
|
||||
{{outlet}}
|
||||
+13
@@ -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>
|
||||
+12
@@ -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>
|
||||
+7
@@ -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>
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
<Form::CredentialLibrary
|
||||
@model={{@model}}
|
||||
@submit={{route-action 'save' @model}}
|
||||
@cancel={{route-action 'cancel' @model}}
|
||||
/>
|
||||
+20
-1
@@ -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>
|
||||
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{{page-title (t 'titles.new')}}
|
||||
|
||||
<Form::CredentialLibrary
|
||||
@model={{@model}}
|
||||
@submit={{route-action 'save' @model}}
|
||||
@cancel={{route-action 'cancel' @model}}
|
||||
/>
|
||||
+3
@@ -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}}
|
||||
+4
@@ -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}}
|
||||
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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.');
|
||||
});
|
||||
});
|
||||
@@ -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"]'));
|
||||
});
|
||||
});
|
||||
+17
@@ -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);
|
||||
});
|
||||
}
|
||||
);
|
||||
+17
@@ -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);
|
||||
});
|
||||
}
|
||||
);
|
||||
+17
@@ -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);
|
||||
});
|
||||
}
|
||||
);
|
||||
+16
@@ -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);
|
||||
});
|
||||
}
|
||||
);
|
||||
+16
@@ -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);
|
||||
});
|
||||
}
|
||||
);
|
||||
+16
@@ -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);
|
||||
});
|
||||
}
|
||||
);
|
||||
+16
@@ -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);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
Reference in new issue
Block a user