diff --git a/addons/api/addon/abilities/managed-group.js b/addons/api/addon/abilities/managed-group.js
new file mode 100644
index 000000000..dd02a34e9
--- /dev/null
+++ b/addons/api/addon/abilities/managed-group.js
@@ -0,0 +1,8 @@
+import ModelAbility from './model';
+
+/**
+ * Provides abilities for managed groups.
+ */
+export default class ManagedGroupAbility extends ModelAbility {
+ // no op
+}
diff --git a/addons/api/addon/abilities/role.js b/addons/api/addon/abilities/role.js
index f0c6d62ec..b944fa548 100644
--- a/addons/api/addon/abilities/role.js
+++ b/addons/api/addon/abilities/role.js
@@ -1,9 +1,18 @@
import ModelAbility from './model';
+import { inject as service } from '@ember/service';
+
+class InvalidRolePrincipalTypeError extends Error {
+ name = 'InvalidRolePrincipalTypeError';
+}
/**
* Provides abilities for roles.
*/
export default class RoleAbility extends ModelAbility {
+ // =services
+
+ @service can;
+
// =permissions
/**
@@ -26,4 +35,22 @@ export default class RoleAbility extends ModelAbility {
get canRemovePrincipals() {
return this.hasAuthorizedAction('remove-principals');
}
+
+ /**
+ * Returns the ability check for a principal model instance.
+ * Valid role principal models are: User, Group, and ManagedGroup.
+ * Throws an error if the principal type is invalid.
+ * @type {boolean}
+ * @throws {InvalidRolePrincipalTypeError}
+ */
+ get canReadPrincipal() {
+ const type = this.model.constructor.modelName;
+ const typeIsValid =
+ type === 'user' || type === 'group' || type === 'managed-group';
+ if (!typeIsValid)
+ throw new InvalidRolePrincipalTypeError(
+ `Expected a role principal of type 'user', 'group', or 'managed-group'. Got '${type}'.`
+ );
+ return this.can.can(`read ${type}`, this.model);
+ }
}
diff --git a/addons/api/addon/models/credential.js b/addons/api/addon/models/credential.js
index d7339b719..a280ae071 100644
--- a/addons/api/addon/models/credential.js
+++ b/addons/api/addon/models/credential.js
@@ -3,7 +3,7 @@ import GeneratedCredentialModel from '../generated/models/credential';
/**
* Supported Credential types.
*/
-export const types = ['username_password', 'ssh_private_key'];
+export const types = ['username_password', 'ssh_private_key', 'json'];
export default class CredentialModel extends GeneratedCredentialModel {
// =attributes
@@ -18,6 +18,14 @@ export default class CredentialModel extends GeneratedCredentialModel {
return this.id.startsWith('cred');
}
+ /**
+ * True if type is `json`.
+ * @type {boolean}
+ */
+ get isJSON() {
+ return this.type === 'json';
+ }
+
/**
* True if credential is an unknown type.
* @type {boolean}
diff --git a/addons/api/addon/models/host-catalog.js b/addons/api/addon/models/host-catalog.js
index 08e1a7d82..e4b78e95c 100644
--- a/addons/api/addon/models/host-catalog.js
+++ b/addons/api/addon/models/host-catalog.js
@@ -1,6 +1,7 @@
import GeneratedHostCatalogModel from '../generated/models/host-catalog';
-export const types = ['aws', 'azure'];
+export const types = ['static', 'plugin'];
+export const pluginTypes = ['aws', 'azure'];
export default class HostCatalogModel extends GeneratedHostCatalogModel {
// =attributes
@@ -26,7 +27,7 @@ export default class HostCatalogModel extends GeneratedHostCatalogModel {
* @type {boolean}
*/
get isUnknown() {
- return this.isPlugin && !types.includes(this.plugin?.name);
+ return this.isPlugin && !pluginTypes.includes(this.plugin?.name);
}
/**
diff --git a/addons/api/addon/models/host-set.js b/addons/api/addon/models/host-set.js
index ed1c0127f..77c3c6c7b 100644
--- a/addons/api/addon/models/host-set.js
+++ b/addons/api/addon/models/host-set.js
@@ -1,5 +1,5 @@
import GeneratedHostSetModel from '../generated/models/host-set';
-import { types } from './host-catalog';
+import { pluginTypes } from './host-catalog';
export default class HostSetModel extends GeneratedHostSetModel {
// =attributes
@@ -25,7 +25,7 @@ export default class HostSetModel extends GeneratedHostSetModel {
* @type {boolean}
*/
get isUnknown() {
- return this.isPlugin && !types.includes(this.plugin?.name);
+ return this.isPlugin && !pluginTypes.includes(this.plugin?.name);
}
/**
diff --git a/addons/api/addon/models/role.js b/addons/api/addon/models/role.js
index 4f2efa720..226c27b2f 100644
--- a/addons/api/addon/models/role.js
+++ b/addons/api/addon/models/role.js
@@ -1,11 +1,13 @@
import GeneratedRoleModel from '../generated/models/role';
import { attr } from '@ember-data/model';
import { inject as service } from '@ember/service';
+import { all, resolve } from 'rsvp';
export default class RoleModel extends GeneratedRoleModel {
// =services
@service store;
+ @service resourceFilterStore;
// =attributes
@@ -37,6 +39,123 @@ export default class RoleModel extends GeneratedRoleModel {
: null;
}
+ /**
+ * A list of IDs for principals of type `user`.
+ * @type {string[]}
+ */
+ get userIDs() {
+ return this.principals
+ .filter(({ type }) => type === 'user')
+ .map(({ principal_id }) => principal_id);
+ }
+
+ /**
+ * A list of IDs for principals of type `group`.
+ * @type {string[]}
+ */
+ get groupIDs() {
+ return this.principals
+ .filter(({ type }) => type === 'group')
+ .map(({ principal_id }) => principal_id);
+ }
+
+ /**
+ * A list of IDs for principals of type `managed group`.
+ * @type {string[]}
+ */
+ get managedGroupIDs() {
+ return this.principals
+ .filter(({ type }) => type === 'managed group')
+ .map(({ principal_id }) => principal_id);
+ }
+
+ /**
+ * A promise that resolves to an array of User instances.
+ * When calling this getter, be sure to await resolution
+ * before interacting with the results.
+ * @type {Promise[UserModel]}
+ */
+ get users() {
+ const ids = this.userIDs;
+
+ // Role has prinicipal IDs,
+ // return a promise which resolves model instances for those IDs
+ if (ids?.length) {
+ return this.resourceFilterStore
+ .queryBy('user', { id: ids }, { scope_id: 'global', recursive: true })
+ .then((models) => models.map((model) => model));
+ }
+
+ // No principal IDs,
+ // return a promise resolving to an empty array
+ return resolve([]);
+ }
+
+ /**
+ * A promise that resolves to an array of Group instances.
+ * When calling this getter, be sure to await resolution
+ * before interacting with the results.
+ * @type {Promise[GroupModel]}
+ */
+ get groups() {
+ const ids = this.groupIDs;
+
+ // Role has prinicipal IDs,
+ // return a promise which resolves model instances for those IDs
+ if (ids?.length) {
+ return this.resourceFilterStore
+ .queryBy('group', { id: ids }, { scope_id: 'global', recursive: true })
+ .then((models) => models.map((model) => model));
+ }
+
+ // No principal IDs,
+ // return a promise resolving to an empty array
+ return resolve([]);
+ }
+
+ /**
+ * A promise that resolves to an array of ManagedGroup instances.
+ * When calling this getter, be sure to await resolution
+ * before interacting with the results.
+ * @type {Promise[ManagedGroupModel]}
+ */
+ get managedGroups() {
+ const ids = this.managedGroupIDs;
+
+ // Role has prinicipal IDs
+ // return a promise which resolves model instances for those IDs
+ if (ids?.length) {
+ // Collect all auth methods.
+ const authMethods = this.resourceFilterStore.queryBy(
+ 'auth-method',
+ { type: 'oidc' },
+ { scope_id: 'global', recursive: true }
+ );
+
+ // For each auth method, query all managed groups with IDs
+ // that match the role's manage group principal IDs.
+ const managedGroups = authMethods
+ .then((methods) =>
+ all(
+ methods.map(({ id: auth_method_id }) =>
+ this.resourceFilterStore
+ .queryBy('managed-group', { id: ids }, { auth_method_id })
+ .then((models) => models.map((model) => model))
+ )
+ )
+ )
+ // The result is an array of arrays of model instances (grouped by
+ // auth methods), so these must be flattened.
+ .then((grouped) => grouped.flat());
+
+ return managedGroups;
+ }
+
+ // No principal IDs,
+ // return a promise resolving to an empty array
+ return resolve([]);
+ }
+
// =methods
/**
diff --git a/addons/api/addon/models/target.js b/addons/api/addon/models/target.js
index 08db9715c..5a77ce566 100644
--- a/addons/api/addon/models/target.js
+++ b/addons/api/addon/models/target.js
@@ -2,7 +2,9 @@ import GeneratedTargetModel from '../generated/models/target';
import { attr } from '@ember-data/model';
import { inject as service } from '@ember/service';
-export const types = ['tcp', 'ssh'];
+export const TYPE_TARGET_TCP = 'tcp';
+export const TYPE_TARGET_SSH = 'ssh';
+export const TYPES_TARGET = Object.freeze([TYPE_TARGET_TCP, TYPE_TARGET_SSH]);
export default class TargetModel extends GeneratedTargetModel {
// =services
@@ -321,7 +323,7 @@ export default class TargetModel extends GeneratedTargetModel {
* @type {boolean}
*/
get isTCP() {
- return this.type === 'tcp';
+ return this.type === TYPE_TARGET_TCP;
}
/**
@@ -329,6 +331,6 @@ export default class TargetModel extends GeneratedTargetModel {
* @type {boolean}
*/
get isSSH() {
- return this.type === 'ssh';
+ return this.type === TYPE_TARGET_SSH;
}
}
diff --git a/addons/api/addon/serializers/credential.js b/addons/api/addon/serializers/credential.js
index 5c61d93dc..78878b38c 100644
--- a/addons/api/addon/serializers/credential.js
+++ b/addons/api/addon/serializers/credential.js
@@ -20,7 +20,9 @@ export default class CredentialSerializer extends ApplicationSerializer {
delete serialized.attributes.username;
if (serialized?.attributes?.json_object) {
- serialized.attributes.object = JSON.parse(serialized.attributes.json_object);
+ serialized.attributes.object = JSON.parse(
+ serialized.attributes.json_object
+ );
delete serialized.attributes.json_object;
}
}
diff --git a/addons/api/app/abilities/managed-group.js b/addons/api/app/abilities/managed-group.js
new file mode 100644
index 000000000..bf7e304a5
--- /dev/null
+++ b/addons/api/app/abilities/managed-group.js
@@ -0,0 +1 @@
+export { default } from 'api/abilities/managed-group';
diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js
index 95bbe9ac7..d18bc0676 100644
--- a/addons/api/mirage/config.js
+++ b/addons/api/mirage/config.js
@@ -267,14 +267,17 @@ export default function () {
this.del('/roles/:id');
this.post(
'/roles/:idMethod',
- function ({ roles, users, groups }, { params: { idMethod } }) {
+ function (
+ { roles, users, groups, managedGroups },
+ { params: { idMethod } }
+ ) {
const attrs = this.normalizedRequestAttrs();
const id = idMethod.split(':')[0];
const method = idMethod.split(':')[1];
const role = roles.find(id);
let updatedAttrs = {};
- // Principals is a combined list of users and groups, but in Mirage we've
+ // Principals is a combined list of users, groups and managed groups, but in Mirage we've
// internally modelled them as separate lists. Therefore we must check
// the type by looking up the user or group first, then determine which
// list to add the principal to.
@@ -283,25 +286,31 @@ export default function () {
version: attrs.version,
userIds: role.userIds,
groupIds: role.groupIds,
+ managedGroupIds: role.managedGroupIds,
};
attrs.principalIds.forEach((id) => {
const isUser = users.find(id);
const isGroup = groups.find(id);
+ const isManagedGroup = managedGroups.find(id);
if (isUser && !updatedAttrs.userIds.includes(id)) {
updatedAttrs.userIds.push(id);
}
if (isGroup && !updatedAttrs.groupIds.includes(id)) {
updatedAttrs.groupIds.push(id);
}
+ if (isManagedGroup && !updatedAttrs.managedGroupIds.includes(id)) {
+ updatedAttrs.managedGroupIds.push(id);
+ }
});
}
- // If deleting principals, filter them out of both users and groups,
+ // If deleting principals, filter the ids out of users, groups and managed group lists,
// and in this case don't care about the type
if (method === 'remove-principals') {
updatedAttrs = {
version: attrs.version,
userIds: role.userIds,
groupIds: role.groupIds,
+ managedGroupIds: role.managedGroupIds,
};
updatedAttrs.userIds = updatedAttrs.userIds.filter((id) => {
return !attrs.principalIds.includes(id);
@@ -309,6 +318,11 @@ export default function () {
updatedAttrs.groupIds = updatedAttrs.groupIds.filter((id) => {
return !attrs.principalIds.includes(id);
});
+ updatedAttrs.managedGroupIds = updatedAttrs.managedGroupIds.filter(
+ (id) => {
+ return !attrs.principalIds.includes(id);
+ }
+ );
}
if (method === 'set-grants') {
@@ -580,8 +594,17 @@ export default function () {
// managed-groups
this.get(
'/managed-groups',
- ({ managedGroups }, { queryParams: { auth_method_id: authMethodId } }) => {
- return managedGroups.where({ authMethodId });
+ (
+ { managedGroups },
+ { queryParams: { auth_method_id: authMethodId, filter } }
+ ) => {
+ let resultSet;
+ if (authMethodId) {
+ resultSet = managedGroups.where({ authMethodId });
+ } else {
+ resultSet = managedGroups.all();
+ }
+ return resultSet.filter(makeBooleanFilter(filter));
}
);
this.post('/managed-groups');
diff --git a/addons/api/mirage/factories/role.js b/addons/api/mirage/factories/role.js
index 841558fde..72138f61c 100644
--- a/addons/api/mirage/factories/role.js
+++ b/addons/api/mirage/factories/role.js
@@ -28,9 +28,13 @@ export default factory.extend({
withPrincipals: trait({
afterCreate(role, server) {
const { scope } = role;
+ const { id: scopeId } = scope;
const users = server.createList('user', 2, { scope });
const groups = server.createList('group', 2, { scope });
- role.update({ users, groups });
+ const managedGroups = server.schema.managedGroups.where({
+ scopeId,
+ }).models;
+ role.update({ users, groups, managedGroups });
},
}),
diff --git a/addons/api/mirage/factories/target.js b/addons/api/mirage/factories/target.js
index 9ea10477e..8afdb93d3 100644
--- a/addons/api/mirage/factories/target.js
+++ b/addons/api/mirage/factories/target.js
@@ -3,10 +3,11 @@ import { trait } from 'ember-cli-mirage';
import { faker } from '@faker-js/faker';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
+import { TYPES_TARGET } from 'api/models/target';
const randomBoolean = (chance = 0.5) => Math.random() < chance;
const hostSetChance = 0.3;
-const types = ['tcp', 'ssh'];
+const types = [...TYPES_TARGET];
export default factory.extend({
authorized_actions: () =>
@@ -35,13 +36,11 @@ export default factory.extend({
* Generates attributes fields by type.
*/
afterCreate(target) {
- if (target.type === 'tcp' || target.type === 'ssh') {
- target.update({
- attributes: {
- default_port: faker.datatype.number(),
- },
- });
- }
+ target.update({
+ attributes: {
+ default_port: faker.datatype.number(),
+ },
+ });
},
/**
diff --git a/addons/api/mirage/generated/factories/target.js b/addons/api/mirage/generated/factories/target.js
index 35e072290..d714b6459 100644
--- a/addons/api/mirage/generated/factories/target.js
+++ b/addons/api/mirage/generated/factories/target.js
@@ -1,11 +1,12 @@
import { Factory } from 'ember-cli-mirage';
import { faker } from '@faker-js/faker';
+import { TYPE_TARGET_TCP } from 'api/models/target';
/**
* GeneratedTargetModelFactory
*/
export default Factory.extend({
- type: 'tcp',
+ type: TYPE_TARGET_TCP,
name: () => faker.random.words(),
session_max_seconds: () => faker.datatype.number(),
session_connection_limit: () => faker.datatype.number(),
diff --git a/addons/api/mirage/models/role.js b/addons/api/mirage/models/role.js
index 97be034f9..54acf6d50 100644
--- a/addons/api/mirage/models/role.js
+++ b/addons/api/mirage/models/role.js
@@ -5,4 +5,5 @@ export default Model.extend({
scope: belongsTo(),
users: hasMany(),
groups: hasMany(),
+ managedGroups: hasMany(),
});
diff --git a/addons/api/mirage/scenarios/default.js b/addons/api/mirage/scenarios/default.js
index 113dd3194..7ce918221 100644
--- a/addons/api/mirage/scenarios/default.js
+++ b/addons/api/mirage/scenarios/default.js
@@ -20,7 +20,7 @@ export default function (server) {
// Auth
const globalAuthMethods = server.createList(
'auth-method',
- 1,
+ 5,
{ scope: globalScope },
'withAccountsAndUsersAndManagedGroups'
);
@@ -51,8 +51,43 @@ export default function (server) {
server.createList('group', 1, { scope: globalScope }, 'withMembers');
server.createList('group', 5, { scope: orgScope }, 'withMembers');
// Role
- server.createList('role', 1, { scope: globalScope }, 'withPrincipals');
- server.createList('role', 5, { scope: orgScope }, 'withPrincipals');
+ const globalScopeRoles = server.createList(
+ 'role',
+ 1,
+ { scope: globalScope },
+ 'withPrincipals'
+ );
+ //create managed groups for role/principals in globalScope
+ globalScopeRoles.forEach((role) => {
+ const { scope } = role;
+ const oidcAuthMethod = globalAuthMethods.filter(
+ (auth) => auth.type === 'oidc'
+ )[0];
+ const managedGroups = server.createList('managed-group', 2, {
+ scope,
+ authMethodId: oidcAuthMethod.id,
+ });
+ role.update({ managedGroups });
+ });
+
+ const orgScopeRoles = server.createList(
+ 'role',
+ 5,
+ { scope: orgScope },
+ 'withPrincipals'
+ );
+ //create managed groups for role/principals in orgScope
+ orgScopeRoles.forEach((role) => {
+ const { scope } = role;
+ const oidcAuthMethod = orgAuthMethods.filter(
+ (auth) => auth.type === 'oidc'
+ )[0];
+ const managedGroups = server.createList('managed-group', 2, {
+ scope,
+ authMethodId: oidcAuthMethod.id,
+ });
+ role.update({ managedGroups });
+ });
// Other resources
server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => {
diff --git a/addons/api/mirage/serializers/role.js b/addons/api/mirage/serializers/role.js
index db04fe328..14b8d82ac 100644
--- a/addons/api/mirage/serializers/role.js
+++ b/addons/api/mirage/serializers/role.js
@@ -18,7 +18,19 @@ export default ApplicationSerializer.extend({
id: group.id,
type: 'group',
}));
- json.principals = [...serializedUsers, ...serializedGroups];
+ const serializedManagedGroups = model.managedGroups.models.map(
+ (managedGroup) => ({
+ scope_id: model.scope.id,
+ id: managedGroup.id,
+ type: 'managed group',
+ })
+ );
+ json.principals = [
+ ...serializedUsers,
+ ...serializedGroups,
+ ...serializedManagedGroups,
+ ];
+
if (!json.principals.length) delete json.principals;
// default grant scope
diff --git a/addons/api/tests/unit/abilities/role-test.js b/addons/api/tests/unit/abilities/role-test.js
index 462743446..b2e8675e5 100644
--- a/addons/api/tests/unit/abilities/role-test.js
+++ b/addons/api/tests/unit/abilities/role-test.js
@@ -10,9 +10,9 @@ module('Unit | Abilities | Role', function (hooks) {
const model = {
authorized_actions: ['set-grants'],
};
- assert.ok(service.can('setGrants role', model));
+ assert.true(service.can('setGrants role', model));
model.authorized_actions = [];
- assert.notOk(service.can('setGrants role', model));
+ assert.false(service.can('setGrants role', model));
});
test('it reflects when a given role may add principals based on authorized_actions', function (assert) {
@@ -21,9 +21,9 @@ module('Unit | Abilities | Role', function (hooks) {
const model = {
authorized_actions: ['add-principals'],
};
- assert.ok(service.can('addPrincipals role', model));
+ assert.true(service.can('addPrincipals role', model));
model.authorized_actions = [];
- assert.notOk(service.can('addPrincipals role', model));
+ assert.false(service.can('addPrincipals role', model));
});
test('it reflects when a given role may remove principals based on authorized_actions', function (assert) {
@@ -32,8 +32,47 @@ module('Unit | Abilities | Role', function (hooks) {
const model = {
authorized_actions: ['remove-principals'],
};
- assert.ok(service.can('removePrincipals role', model));
+ assert.true(service.can('removePrincipals role', model));
model.authorized_actions = [];
- assert.notOk(service.can('removePrincipals role', model));
+ assert.false(service.can('removePrincipals role', model));
+ });
+
+ test('it proxies role principal read ability to the appropriate ability type', function (assert) {
+ assert.expect(6);
+ const service = this.owner.lookup('service:can');
+ const user = {
+ constructor: { modelName: 'user' },
+ authorized_actions: [],
+ };
+ const group = {
+ constructor: { modelName: 'group' },
+ authorized_actions: [],
+ };
+ const managedGroup = {
+ constructor: { modelName: 'managed-group' },
+ authorized_actions: [],
+ };
+ assert.false(service.can('readPrincipal role', user));
+ assert.false(service.can('readPrincipal role', group));
+ assert.false(service.can('readPrincipal role', managedGroup));
+ user.authorized_actions.push('read');
+ group.authorized_actions.push('read');
+ managedGroup.authorized_actions.push('read');
+ assert.true(service.can('readPrincipal role', user));
+ assert.true(service.can('readPrincipal role', group));
+ assert.true(service.can('readPrincipal role', managedGroup));
+ });
+
+ test('it throws an error for unknown principal types on principal read check', function (assert) {
+ assert.expect(2);
+ const service = this.owner.lookup('service:can');
+ const account = {
+ constructor: { modelName: 'account' },
+ authorized_actions: ['read'],
+ };
+ assert.throws(() => {
+ service.can('readPrincipal role', account);
+ });
+ assert.true(service.can('read account', account));
});
});
diff --git a/addons/api/tests/unit/models/credential-test.js b/addons/api/tests/unit/models/credential-test.js
index 21b5b98ce..a0f0497e2 100644
--- a/addons/api/tests/unit/models/credential-test.js
+++ b/addons/api/tests/unit/models/credential-test.js
@@ -22,6 +22,22 @@ module('Unit | Model | credential', function (hooks) {
assert.false(modelC.isStatic);
});
+ test('it has isJSON property and returns the expected values', async function (assert) {
+ assert.expect(3);
+ const store = this.owner.lookup('service:store');
+ const modelA = store.createRecord('credential', { type: 'json' });
+ const modelB = store.createRecord('credential', {
+ type: 'username_password',
+ });
+ const modelC = store.createRecord('credential', {
+ type: 'ssh_private_key',
+ });
+
+ assert.true(modelA.isJSON);
+ assert.false(modelB.isJSON);
+ assert.false(modelC.isJSON);
+ });
+
test('it has isUnknown property and returns the expected values', async function (assert) {
assert.expect(3);
const store = this.owner.lookup('service:store');
diff --git a/addons/api/tests/unit/models/role-test.js b/addons/api/tests/unit/models/role-test.js
index 9d64e9ac2..fc0c5d8e8 100644
--- a/addons/api/tests/unit/models/role-test.js
+++ b/addons/api/tests/unit/models/role-test.js
@@ -64,6 +64,7 @@ module('Unit | Model | role', function (hooks) {
principals: [
{ id: '1', type: 'user' },
{ id: '3', type: 'group' },
+ { id: '5', type: 'managed group' },
],
version: 1,
scope: {
@@ -84,7 +85,7 @@ module('Unit | Model | role', function (hooks) {
(schema, request) => {
const body = JSON.parse(request.requestBody);
assert.deepEqual(body, {
- principal_ids: ['1', '3'],
+ principal_ids: ['1', '3', '5'],
version: 1,
});
return { id: '123abc' };
@@ -101,6 +102,7 @@ module('Unit | Model | role', function (hooks) {
principals: [
{ id: '1', type: 'user' },
{ id: '3', type: 'group' },
+ { id: '5', type: 'managed group' },
],
version: 1,
scope: {
@@ -111,7 +113,7 @@ module('Unit | Model | role', function (hooks) {
},
});
const model = store.peekRecord('role', '123abc');
- await model.removePrincipals(['1', '3']);
+ await model.removePrincipals(['1', '3', '5']);
});
test('it has a `removePrincipal` method that deletes a single principal using `removePrincipals` method', async function (assert) {
@@ -138,6 +140,7 @@ module('Unit | Model | role', function (hooks) {
principals: [
{ id: '1', type: 'user' },
{ id: '3', type: 'group' },
+ { id: '5', type: 'managed group' },
],
version: 1,
scope: {
diff --git a/addons/api/tests/unit/models/target-test.js b/addons/api/tests/unit/models/target-test.js
index eca4d54a5..86dd93c0f 100644
--- a/addons/api/tests/unit/models/target-test.js
+++ b/addons/api/tests/unit/models/target-test.js
@@ -1,6 +1,7 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
+import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
module('Unit | Model | target', function (hooks) {
setupTest(hooks);
@@ -383,7 +384,7 @@ module('Unit | Model | target', function (hooks) {
assert.expect(2);
const store = this.owner.lookup('service:store');
const modelSSH = store.createRecord('target', {
- type: 'ssh',
+ type: TYPE_TARGET_SSH,
});
assert.true(modelSSH.isSSH);
assert.false(modelSSH.isTCP);
@@ -393,7 +394,7 @@ module('Unit | Model | target', function (hooks) {
assert.expect(2);
const store = this.owner.lookup('service:store');
const modelTCP = store.createRecord('target', {
- type: 'tcp',
+ type: TYPE_TARGET_TCP,
});
assert.true(modelTCP.isTCP);
assert.false(modelTCP.isSSH);
diff --git a/addons/api/tests/unit/serializers/auth-method-test.js b/addons/api/tests/unit/serializers/auth-method-test.js
index 17043ffba..23c17fb7e 100644
--- a/addons/api/tests/unit/serializers/auth-method-test.js
+++ b/addons/api/tests/unit/serializers/auth-method-test.js
@@ -182,7 +182,6 @@ module('Unit | Serializer | auth method', function (hooks) {
signing_algorithms,
idp_ca_certs,
} = record;
- console.log(record);
assert.deepEqual(account_claim_maps, [
{ from: 'from', to: 'to' },
{ from: 'foo', to: 'bar' },
diff --git a/addons/api/tests/unit/serializers/target-test.js b/addons/api/tests/unit/serializers/target-test.js
index fde87e20d..29864c838 100644
--- a/addons/api/tests/unit/serializers/target-test.js
+++ b/addons/api/tests/unit/serializers/target-test.js
@@ -1,5 +1,6 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
+import { TYPE_TARGET_TCP, TYPE_TARGET_SSH } from 'api/models/target';
module('Unit | Serializer | target', function (hooks) {
setupTest(hooks);
@@ -26,7 +27,7 @@ module('Unit | Serializer | target', function (hooks) {
},
default_port: 1234,
version: 1,
- type: 'tcp',
+ type: TYPE_TARGET_TCP,
});
const snapshot = record._createSnapshot();
snapshot.adapterOptions = {};
@@ -35,7 +36,7 @@ module('Unit | Serializer | target', function (hooks) {
name: 'User',
description: 'Description',
version: 1,
- type: 'tcp',
+ type: TYPE_TARGET_TCP,
scope_id: 'org_1',
session_max_seconds: 28800,
session_connection_limit: null,
@@ -124,7 +125,7 @@ module('Unit | Serializer | target', function (hooks) {
name: 'User',
description: 'Description',
version: 1,
- type: 'ssh',
+ type: TYPE_TARGET_SSH,
worker_filter: 'worker',
default_port: 1234,
scope: {
@@ -137,7 +138,7 @@ module('Unit | Serializer | target', function (hooks) {
name: 'User',
description: 'Description',
version: 1,
- type: 'ssh',
+ type: TYPE_TARGET_SSH,
scope_id: 'org_1',
session_max_seconds: 28800,
session_connection_limit: null,
diff --git a/addons/core/translations/en-us.yaml b/addons/core/translations/en-us.yaml
index efa8a811c..35787d6d8 100644
--- a/addons/core/translations/en-us.yaml
+++ b/addons/core/translations/en-us.yaml
@@ -41,7 +41,6 @@ descriptions:
neither-list-nor-create: You do not have permission to list or create {resource}.
no-results: We found no matching records.
choose-a-provider: There are several built-in plugins for the most common providers. You can choose from one of the included plugins or use a custom plugin.
- choose-a-provider-azure: Boundary does not yet support rotating credentials.
provider: This is the provider for this host catalog. The format of the host set filters are specific to each provider.
questions:
delete-confirm: Are you sure you want to delete this resource?
diff --git a/addons/core/translations/form/en-us.yaml b/addons/core/translations/form/en-us.yaml
index 8a2286444..682bd73f8 100644
--- a/addons/core/translations/form/en-us.yaml
+++ b/addons/core/translations/form/en-us.yaml
@@ -150,6 +150,7 @@ secret:
help: The client secret value generated by Azure for Boundary to use for authentication.
disable_credential_rotation:
label: Disable credential rotation
+ help: Azure host catalogs do not yet support credential rotation.
dns_names:
label: Latest DNS Names
host:
@@ -163,4 +164,4 @@ tags:
label: Tags
secret-editor:
details: This secret is saved but won't be displayed
- edit: Click to replace this secret
\ No newline at end of file
+ edit: Click to replace this secret
diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml
index 32f934c43..eacbb9877 100644
--- a/addons/core/translations/resources/en-us.yaml
+++ b/addons/core/translations/resources/en-us.yaml
@@ -93,13 +93,13 @@ host-catalog:
delete: Delete Host Catalog
help:
static: Provide a list of hosts and host sets with static addresses
- dynamic: Automate discovery of target hosts and services on Azure or AWS
+ plugin: Automate discovery of target hosts and services on Azure or AWS
types:
static: Static
unknown: Plugin
aws: AWS
azure: Azure
- dynamic: Dynamic
+ plugin: Dynamic
form:
aws_region:
label: AWS Region
@@ -271,6 +271,7 @@ role:
types:
user: User
group: Group
+ managed-group: Managed Group
actions:
add-principals: Add Principals
grant:
@@ -304,11 +305,15 @@ target:
add-brokered-credential-sources: Add Brokered Credentials
add-injected-application-credential-sources: Add Injected Application Credentials
types:
- tcp: TCP
+ tcp: Generic TCP
ssh: SSH
help:
- tcp: Connections are made over TCP to support a broad range of protocols, including SSH, RDP, databases, and more. This is the most general-purpose type.
- ssh: Protocol-aware connections are made over SSH. Use when requiring SSH connections to remote hosts.
+ tcp: Generic TCP supports a broad range of connection types.
+ ssh: Protocol-aware SSH with support for credential injection.
+ form:
+ type:
+ label: Type
+ help: Target type is the protocol with which end users should connect to this target. Choose Generic TCP for broad support of common protocols including RDP, K8s, many databases, and more.
host-source:
title: Host Source
title_plural: Host Sources
@@ -455,6 +460,9 @@ credential:
private_key_passphrase:
label: Passphrase
help: Optional.
+ json:
+ label: JSON
+ help: This is the JSON blob that provides credentials to the host. After this data is saved, you will not be able to edit it again without replacing it.
scope:
title: Scope
title_plural: Scopes
diff --git a/addons/rose/addon/components/rose/form/radio/card/index.js b/addons/rose/addon/components/rose/form/radio/card/index.js
deleted file mode 100644
index f4aaab3bb..000000000
--- a/addons/rose/addon/components/rose/form/radio/card/index.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import RoseFormRadioRadioComponent from '../radio';
-import { deprecate } from '@ember/debug';
-
-export default class RoseFormRadioCardComponent extends RoseFormRadioRadioComponent {
- // =attributes
- layout = this.args.layout;
- className = `rose-form-radio-card ${this.layout}`;
-
- constructor() {
- super(...arguments);
- deprecate(
- `Rose Form/Radio/Card component is deprecated, use HDS Form/RadioCard instead. For more info, refer to `,
- false,
- {
- id: 'rose.form.radio.card',
- until: '4.8.0',
- for: 'rose',
- since: {
- enabled: '3.28.8',
- },
- url: 'https://boundary-ui-storybook.vercel.app/?path=/docs/rose-form-radio-radio-card--card',
- }
- );
- }
-}
diff --git a/addons/rose/addon/components/rose/form/radio/card/index.stories.mdx b/addons/rose/addon/components/rose/form/radio/card/index.stories.mdx
index 5cd8de8ab..9d284938c 100644
--- a/addons/rose/addon/components/rose/form/radio/card/index.stories.mdx
+++ b/addons/rose/addon/components/rose/form/radio/card/index.stories.mdx
@@ -1,15 +1,12 @@
import { Meta, Story, Preview } from '@storybook/addon-docs';
import { hbs } from 'ember-cli-htmlbars';
-
+
-# HDS::Form::RadioCard (Recommended)
+# RadioCard (removed)
-> **_NOTE:_** Replaces the deprecated Rose/Form/Radio/Card.
-
-Refer to the [HDS Form::RadioCard
-documentation](https://design-system-components-hashicorp.vercel.app/components/form/radio-card)
-for a complete overview
+Rose/Form/Select has been removed in favor of
+[HDS::Form::RadioCard](https://design-system-components-hashicorp.vercel.app/components/form/radio-card).
```handlebars
@@ -44,78 +41,3 @@ export const HDSTemplate = (args) => ({
{HDSTemplate.bind({})}
-
-# Rose/Form/Radio/Card (Deprecated)
-
-Rose/Form/Radio/Card has been deprecated in favor of [HDS::Form::RadioCard](https://design-system-components-hashicorp.vercel.app/components/form/radio-card)
-
-### usage:
-
-# Form Radiofield in a wide format
-
-Renders a form radiofield in a Wide format.
-
-```handlebars
-
-```
-
-Renders a form radiofield in a tile format.
-
-```handlebars
-
-```
-
-export const Template = (args) => ({
- template: hbs`
- `,
- context: args,
-});
-
-
-
- {Template.bind({})}
-
-
diff --git a/addons/rose/addon/components/rose/form/radio/group/index.hbs b/addons/rose/addon/components/rose/form/radio/group/index.hbs
index 8e090407d..e883eb431 100644
--- a/addons/rose/addon/components/rose/form/radio/group/index.hbs
+++ b/addons/rose/addon/components/rose/form/radio/group/index.hbs
@@ -8,13 +8,5 @@
changed=@changed
inline=@inline
)
- card=(component
- 'rose/form/radio/card'
- name=@name
- selectedValue=@selectedValue
- disabled=@disabled
- changed=@changed
- layout=@layout
- )
)
}}
\ No newline at end of file
diff --git a/addons/rose/addon/components/rose/form/radio/group/index.stories.mdx b/addons/rose/addon/components/rose/form/radio/group/index.stories.mdx
index a698fedda..dd0d5b106 100644
--- a/addons/rose/addon/components/rose/form/radio/group/index.stories.mdx
+++ b/addons/rose/addon/components/rose/form/radio/group/index.stories.mdx
@@ -75,9 +75,9 @@ export const Template = (args) => ({
-# HDS::Form::RadioCard::Group (Recommended)
+# HDS::Form::RadioCard::Group
-> **_NOTE:_** Replaces the deprecated Rose/Form/Radio/Group/Card.
+> **_NOTE:_** Replaces the removed Rose/Form/Radio/Group/Card.
```handlebars
({
{HDSTemplate.bind({})}
-
-# Rose::Form::Radio::Group::Card (Deprecated)
-
-```handlebars
-
-
-
-
-
-```
-
-export const RoseTemplate = (args) => ({
- template: hbs`
-
-
-
- `,
- context: args,
-});
-
-
-
- {RoseTemplate.bind({})}
-
-
diff --git a/addons/rose/addon/modifiers/code-mirror.js b/addons/rose/addon/modifiers/code-mirror.js
index 077d98d25..3a0e66aab 100644
--- a/addons/rose/addon/modifiers/code-mirror.js
+++ b/addons/rose/addon/modifiers/code-mirror.js
@@ -9,8 +9,8 @@ import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/edit/closebrackets';
import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/display/autorefresh';
-import 'codemirror/addon/lint/lint'
-import 'codemirror/addon/lint/json-lint'
+import 'codemirror/addon/lint/lint';
+import 'codemirror/addon/lint/json-lint';
// Here we define default options for the editor.
// These should follow the codemirror configuration types
diff --git a/addons/rose/app/styles/rose/components/form/_index.scss b/addons/rose/app/styles/rose/components/form/_index.scss
index b06e535f6..73137ac8d 100644
--- a/addons/rose/app/styles/rose/components/form/_index.scss
+++ b/addons/rose/app/styles/rose/components/form/_index.scss
@@ -6,4 +6,3 @@
@import 'select';
@import 'radio';
@import 'fieldset';
-@import 'radio-card';
diff --git a/addons/rose/app/styles/rose/components/form/_radio-card.scss b/addons/rose/app/styles/rose/components/form/_radio-card.scss
deleted file mode 100644
index 0c61fb51d..000000000
--- a/addons/rose/app/styles/rose/components/form/_radio-card.scss
+++ /dev/null
@@ -1,240 +0,0 @@
-@use '../../variables/sizing';
-@use '../../utilities/type';
-@use '../../variables/typography';
-
-$size-xxl: sizing.rems(xxl); // 48
-$size-xl: sizing.rems(xl); // 36
-$size-l: sizing.rems(l); // 24
-$size-m: sizing.rems(m); // 16
-$size-s: sizing.rems(s); // 12
-$size-xs: sizing.rems(xs); // 8
-$size-xxs: sizing.rems(xxs); // 4
-$size-xxxs: sizing.rems(xxxs); // 2
-$size-xxxxs: sizing.rems(xxxxs); // 1
-
-$radio-svg: (
- 'default':
- "",
- 'hover':
- "",
- 'focus':
- "",
- 'checked':
- "",
- 'disabled':
- "",
- 'disabled-checked':
- "",
- 'error':
- "",
- 'error-checked':
- "",
-);
-
-.rose-form-radio-card {
- $card-wide-width: $size-m * 10;
- $card-tile-width: $size-m * 7.25;
- $border-width: $size-xxxs * 2;
-
- &.wide {
- width: ($size-m) * 20;
- }
-
- &.tile {
- width: ($size-m) * 15;
-
- .rose-icon {
- --size: #{$size-xl};
-
- padding: $size-xxxs;
- }
-
- .rose-form-radio-label {
- display: flex;
- flex-direction: column;
- align-items: center;
- height: $size-m * 9;
-
- &::after {
- left: $card-tile-width - $border-width;
- }
- }
- }
-
- position: relative;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- margin: $size-m $size-xs $size-m 0;
-
- .radio-card-overlay {
- position: absolute;
- height: $size-l + $size-xxs;
- background-color: var(--ui-gray-subtler-5);
- width: 100%;
- bottom: 0;
- right: 0;
- border-bottom-left-radius: $size-xxxs;
- border-bottom-right-radius: $size-xxxs;
- }
-
- .rose-form-radio-label {
- @include type.type(s, bold);
-
- width: ($size-m) * 23;
- height: 100%;
- display: block;
- position: relative;
- text-align: left;
- border: $size-xxxxs solid var(--ui-gray-subtler-2);
- box-shadow: 0 $size-xxs $size-xxs
- rgba(var(--stark-components), var(--opacity-2));
- border-radius: $size-xxs;
- padding: $size-s $size-m ($size-xl + $size-m);
- box-sizing: border-box;
-
- .rose-form-helper-text {
- @include type.type(xs, normal);
-
- padding-top: $size-xs;
- }
-
- &::after {
- position: absolute;
- bottom: $size-xxs + $size-xxxxs;
- right: $card-wide-width - $border-width;
- display: block;
- width: $size-m;
- height: $size-m;
- transform: scale(2);
- pointer-events: none;
- content: ' ';
- background: no-repeat 50%/50% 50%;
- background-image: url($data-svg-prefix + map-get($radio-svg, 'default'));
- }
- }
-
- .rose-form-radio-field {
- position: absolute;
- z-index: 1;
- bottom: $size-xs;
- opacity: 0;
-
- &:not(:disabled) {
- &:focus {
- ~ .rose-form-radio-label {
- &::after {
- background-image: url($data-svg-prefix + map-get(
- $radio-svg,
- 'focus'
- ));
- }
- }
- }
-
- &:hover {
- ~ .rose-form-radio-label {
- border: $size-xxxxs solid var(--ui-gray);
- }
- }
-
- &:checked {
- ~ .rose-form-radio-label {
- &::after {
- background-image: url($data-svg-prefix + map-get(
- $radio-svg,
- 'checked'
- ));
- }
-
- .radio-card-overlay {
- background-color: var(--action-subtler-1h);
-
- .ember-application.rose-theme-light & {
- background-color: var(--action-subtler-1h);
- }
- @media (prefers-color-scheme: dark) {
- background-color: var(--action-subtler-1);
- }
-
- .ember-application.rose-theme-dark & {
- background-color: var(--action-subtler-1);
- }
- }
-
- border: $size-xxxxs solid var(--info-subtler-1);
- }
- }
- }
-
- &:disabled {
- ~ .rose-form-radio-label {
- border: $size-xxxxs solid var(--ui-border-subtler-1);
- background-color: var(--ui-gray-subtler-6);
- color: var(--ui-gray-starker-1);
-
- &::after {
- background-image: url($data-svg-prefix + map-get(
- $radio-svg,
- 'disabled'
- ));
- }
- }
-
- &:checked {
- ~ .rose-form-radio-label {
- &::after {
- background-image: url($data-svg-prefix + map-get(
- $radio-svg,
- 'disabled-checked'
- ));
- }
- }
- }
- }
- }
-
- &.error {
- .rose-form-radio-field {
- &:not(:disabled, :focus) {
- ~ .rose-form-radio-label {
- color: var(--ui-gray);
- border: $size-xxxxs solid var(--failure);
-
- .radio-card-overlay {
- background-color: var(--failure-subtler-2);
- }
-
- .rose-form-helper-text.error {
- color: var(--ui-gray);
- }
-
- &::after {
- background-image: url($data-svg-prefix + map-get(
- $radio-svg,
- 'error'
- ));
- }
- }
-
- &:checked {
- ~ .rose-form-radio-label {
- color: var(--ui-gray);
- border: $size-xxxxs solid var(--failure);
-
- .radio-card-overlay {
- background-color: var(--failure-subtler-2);
- }
-
- &::after {
- background-image: url($data-svg-prefix + map-get(
- $radio-svg,
- 'error-checked'
- ));
- }
- }
- }
- }
- }
- }
-}
diff --git a/addons/rose/package.json b/addons/rose/package.json
index b96762f51..343e908f0 100644
--- a/addons/rose/package.json
+++ b/addons/rose/package.json
@@ -42,7 +42,7 @@
"ember-cli-htmlbars": "^6.1.0",
"ember-cli-sass": "^11.0.1",
"ember-composable-helpers": "^5.0.0",
- "ember-focus-trap": "^0.7.0",
+ "ember-focus-trap": "^1.0.1",
"ember-named-blocks-polyfill": "^0.2.5",
"ember-radio-button": "github:yapplabs/ember-radio-button.git#b2ab65f",
"ember-uuid": "^2.1.0",
diff --git a/addons/rose/tests/integration/components/rose/form/radio/card-test.js b/addons/rose/tests/integration/components/rose/form/radio/card-test.js
deleted file mode 100644
index 5bc55dd0e..000000000
--- a/addons/rose/tests/integration/components/rose/form/radio/card-test.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import { module, test } from 'qunit';
-import { setupRenderingTest } from 'ember-qunit';
-import { render, find } from '@ember/test-helpers';
-import hbs from 'htmlbars-inline-precompile';
-
-module('Integration | Component | rose/form/radio/card', function (hooks) {
- setupRenderingTest(hooks);
-
- test('it renders', async function (assert) {
- await render(hbs``);
- assert.ok(await find('input'));
- assert.strictEqual(await find('input').type, 'radio');
- });
-
- test('it renders in a tile format when tile layout is applied', async function (assert) {
- await render(hbs``);
- assert.ok(await find('.tile'));
- });
-
- test('it supports an icon', async function (assert) {
- await render(
- hbs``
- );
- assert.ok(find('svg'));
- });
-
- test('it is checked when @value and @selectedValue values match', async function (assert) {
- await render(
- hbs``
- );
- assert.true(await find('input').checked);
- });
-
- test('it is not disabled by default', async function (assert) {
- await render(hbs``);
- assert.false(await find('input').disabled);
- });
-
- test('it is not in error state by default', async function (assert) {
- await render(hbs``);
- assert.notOk(await find('.error'));
- });
-
- test('it is disabled when @disabled={{true}}', async function (assert) {
- await render(hbs``);
- assert.true(await find('input').disabled);
- });
-
- test('it is in error state when @error={{true}}', async function (assert) {
- await render(hbs``);
- assert.ok(await find('.error'));
- });
-});
diff --git a/package.json b/package.json
index 67cce2a4a..feb715b38 100755
--- a/package.json
+++ b/package.json
@@ -105,7 +105,8 @@
"async": "^2.6.4",
"terser": "^5.14.2",
"jpeg-js": "^0.4.4",
- "got": "^11.8.5"
+ "got": "^11.8.5",
+ "loader-utils": "^2.0.4"
},
"config": {
"commitizen": {
diff --git a/ui/admin/.eslintrc.js b/ui/admin/.eslintrc.js
index 8ea8cb7b4..bd758f6a0 100644
--- a/ui/admin/.eslintrc.js
+++ b/ui/admin/.eslintrc.js
@@ -32,6 +32,7 @@ module.exports = {
'.eslintrc.js',
'.prettierrc.js',
'.template-lintrc.js',
+ '.stylelintrc.js',
'ember-cli-build.js',
'testem.js',
'blueprints/*/index.js',
diff --git a/ui/admin/.sasslintrc b/ui/admin/.sasslintrc
deleted file mode 100644
index 76ed8341e..000000000
--- a/ui/admin/.sasslintrc
+++ /dev/null
@@ -1,8 +0,0 @@
-files:
- include: 'app/styles/**/*.s+(a|c)ss'
-rules:
- leading-zero: 0
- mixins-before-declarations: 0
- nesting-depth: 0
- placeholder-in-extend: 0
- property-sort-order: 0
diff --git a/ui/admin/.stylelintrc.js b/ui/admin/.stylelintrc.js
new file mode 100644
index 000000000..cd5887c5f
--- /dev/null
+++ b/ui/admin/.stylelintrc.js
@@ -0,0 +1,33 @@
+'use strict';
+
+module.exports = {
+ extends: ['stylelint-config-standard-scss', 'stylelint-config-prettier-scss'],
+ rules: {
+ // We oftentimes want to have long form properties for readability
+ 'declaration-block-no-redundant-longhand-properties': null,
+
+ // This is just a personal preference unless the quotes are needed
+ // to escape special characters
+ 'function-url-quotes': null,
+
+ // This is often limited by the DOM and we want to order our declarations by our own standard
+ 'no-descending-specificity': null,
+
+ // -Webkit animation CSS for older browsers. Need a decision to remove `-webkit` syntax in
+ // favor of normal CSS
+ 'property-no-vendor-prefix': null,
+ 'at-rule-no-vendor-prefix': null,
+
+ // As a team we want to stick to using kebab-case as our selector class pattern, but there are a couple
+ // of exceptions in which we overrride CSS from addons like CodeMirror and HDS, in which they use
+ // pascal case and a double-kebab-case. This regex checks for all of those 3 patterns and won't fail
+ // lint check, but will fail if its any other pattern.
+ 'selector-class-pattern': [
+ '(^([a-z][a-z0-9]*)(-{1,2}[a-z0-9]+)*$)|(^CodeMirror)',
+ {
+ message: (selector) =>
+ `Expected class selector "${selector}" to be kebab-case`,
+ },
+ ],
+ },
+};
diff --git a/ui/admin/app/abilities/credential.js b/ui/admin/app/abilities/credential.js
new file mode 100644
index 000000000..825341313
--- /dev/null
+++ b/ui/admin/app/abilities/credential.js
@@ -0,0 +1,18 @@
+import CredentialAbility from 'api/abilities/credential';
+import { inject as service } from '@ember/service';
+
+export default class OverrideCredentialAbility extends CredentialAbility {
+ // =service
+ @service features;
+
+ /**
+ * This override ensures that JSON credentials may be read only if the
+ * json-credentials feature flag is enabled. All other types are subject
+ * to the standard logic found in the api addon.
+ */
+ get canRead() {
+ return this.features.isEnabled('json-credentials') || !this.model.isJSON
+ ? super.canRead
+ : false;
+ }
+}
diff --git a/ui/admin/app/components/form/credential-store/static/index.hbs b/ui/admin/app/components/form/credential-store/static/index.hbs
index 2d44e3dd5..1dbeec0cb 100644
--- a/ui/admin/app/components/form/credential-store/static/index.hbs
+++ b/ui/admin/app/components/form/credential-store/static/index.hbs
@@ -70,14 +70,13 @@
{{else}}
{{t 'form.type.label'}}
- {{t
- (concat 'resources.credential-store.types.' @model.type)
- }}
+
+ {{t (concat 'resources.credential-store.help.' @model.type)}}
{{/if}}
diff --git a/ui/admin/app/components/form/credential-store/vault/index.hbs b/ui/admin/app/components/form/credential-store/vault/index.hbs
index ed6e1f16c..3bdf964bc 100644
--- a/ui/admin/app/components/form/credential-store/vault/index.hbs
+++ b/ui/admin/app/components/form/credential-store/vault/index.hbs
@@ -70,14 +70,13 @@
{{else}}
{{t 'form.type.label'}}
- {{t
- (concat 'resources.credential-store.types.' @model.type)
- }}
+
+ {{t (concat 'resources.credential-store.help.' @model.type)}}
{{/if}}
diff --git a/ui/admin/app/components/form/credential/index.hbs b/ui/admin/app/components/form/credential/index.hbs
index a27830b5c..6d957e1b6 100644
--- a/ui/admin/app/components/form/credential/index.hbs
+++ b/ui/admin/app/components/form/credential/index.hbs
@@ -2,6 +2,7 @@
{{component
(concat 'form/credential/' @model.type)
model=@model
+ types=this.credentialTypes
submit=@submit
cancel=@cancel
changeType=@changeType
diff --git a/ui/admin/app/components/form/credential/index.js b/ui/admin/app/components/form/credential/index.js
new file mode 100644
index 000000000..fa281a953
--- /dev/null
+++ b/ui/admin/app/components/form/credential/index.js
@@ -0,0 +1,19 @@
+import Component from '@glimmer/component';
+import { inject as service } from '@ember/service';
+import { types } from 'api/models/credential';
+
+export default class FormCredentialComponent extends Component {
+ // =services
+ @service features;
+
+ // =attributes
+ /**
+ * Returns an array of available credential types the user can create
+ * @type {object}
+ */
+ get credentialTypes() {
+ return this.features.isEnabled('json-credentials')
+ ? types
+ : types.filter((type) => type !== 'json');
+ }
+}
diff --git a/ui/admin/app/components/form/credential/json/index.hbs b/ui/admin/app/components/form/credential/json/index.hbs
new file mode 100644
index 000000000..f1f77c226
--- /dev/null
+++ b/ui/admin/app/components/form/credential/json/index.hbs
@@ -0,0 +1,112 @@
+
+
+
+ {{#if @model.errors.name}}
+
+ {{#each @model.errors.name as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+
+ {{#if @model.errors.description}}
+
+ {{#each @model.errors.description as |error|}}
+ {{error.message}}
+ {{/each}}
+
+ {{/if}}
+
+
+ {{#if @model.isNew}}
+
+ <:body>
+
+ {{t 'form.type.label'}}
+ {{#each @types as |credentialType|}}
+
+ {{t
+ (concat 'resources.credential.types.' credentialType)
+ }}
+ {{t
+ (concat 'resources.credential.help.' credentialType)
+ }}
+
+ {{/each}}
+
+
+
+
+
+ <:title>{{t 'resources.credential.form.json.label'}}
+ <:description>{{t 'resources.credential.form.json.help'}}
+ <:body>
+
+
+
+ {{else}}
+
+ {{t 'form.type.label'}}
+ {{t (concat 'resources.credential.help.' @model.type)}}
+
+
+
+
+ <:title>{{t 'resources.credential.form.json.label'}}
+ <:description>{{t 'resources.credential.form.json.help'}}
+ <:body>
+
+
+
+ {{/if}}
+
+ {{#if (can 'save model' @model)}}
+
+ {{/if}}
+
+
\ No newline at end of file
diff --git a/ui/admin/app/components/form/credential/ssh_private_key/index.hbs b/ui/admin/app/components/form/credential/ssh_private_key/index.hbs
index c87c33efa..bcee4bed2 100644
--- a/ui/admin/app/components/form/credential/ssh_private_key/index.hbs
+++ b/ui/admin/app/components/form/credential/ssh_private_key/index.hbs
@@ -12,8 +12,6 @@
@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}}
@@ -31,8 +29,6 @@
@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}}
@@ -44,9 +40,9 @@
{{/if}}
{{#if @model.isNew}}
-
+
{{t 'form.type.label'}}
- {{#each this.credentialTypes as |credentialType|}}
+ {{#each @types as |credentialType|}}
{{else}}
{{t 'form.type.label'}}
- {{t (concat 'resources.credential.types.' @model.type)}}
+ {{t (concat 'resources.credential.help.' @model.type)}}
{{/if}}
@@ -82,8 +78,6 @@
@label={{t 'resources.credential.form.username.label'}}
@helperText={{t 'resources.credential.form.username.help'}}
@error={{@model.errors.username}}
- disabled={{@model.isSaving}}
- readonly={{false}}
as |field|
>
{{#if @model.errors.username}}
@@ -102,8 +96,6 @@
@label={{t 'resources.credential.form.private_key.label'}}
@helperText={{t 'resources.credential.form.private_key.help'}}
@error={{@model.errors.private_key}}
- disabled={{@model.isSaving}}
- readonly={{false}}
as |field|
>
{{#if @model.errors.private_key}}
@@ -121,8 +113,6 @@
@label={{t 'resources.credential.form.private_key_passphrase.label'}}
@helperText={{t 'resources.credential.form.private_key_passphrase.help'}}
@error={{@model.errors.private_key_passphrase}}
- disabled={{@model.isSaving}}
- readonly={{false}}
autocomplete='new-password'
as |field|
>
diff --git a/ui/admin/app/components/form/credential/ssh_private_key/index.js b/ui/admin/app/components/form/credential/ssh_private_key/index.js
deleted file mode 100644
index c2c5d5bcb..000000000
--- a/ui/admin/app/components/form/credential/ssh_private_key/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import Component from '@glimmer/component';
-import { types } from 'api/models/credential';
-
-export default class FormSSHPrivateKeyCredentialComponent extends Component {
- // =attributes
- /**
- * @type {Array}
- */
- credentialTypes = types;
-}
diff --git a/ui/admin/app/components/form/credential/username_password/index.hbs b/ui/admin/app/components/form/credential/username_password/index.hbs
index 731f4deb9..b0979a89d 100644
--- a/ui/admin/app/components/form/credential/username_password/index.hbs
+++ b/ui/admin/app/components/form/credential/username_password/index.hbs
@@ -12,8 +12,6 @@
@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}}
@@ -31,8 +29,6 @@
@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}}
@@ -44,9 +40,9 @@
{{/if}}
{{#if @model.isNew}}
-
+
{{t 'form.type.label'}}
- {{#each this.credentialTypes as |credentialType|}}
+ {{#each @types as |credentialType|}}
{{else}}
{{t 'form.type.label'}}
- {{t (concat 'resources.credential.types.' @model.type)}}
+ {{t (concat 'resources.credential.help.' @model.type)}}
{{/if}}
@@ -82,8 +78,6 @@
@label={{t 'resources.credential.form.username.label'}}
@helperText={{t 'resources.credential.form.username.help'}}
@error={{@model.errors.username}}
- disabled={{@model.isSaving}}
- readonly={{false}}
as |field|
>
{{#if @model.errors.username}}
@@ -103,8 +97,6 @@
@label={{t 'resources.credential.form.password.label'}}
@helperText={{t 'resources.credential.form.password.help'}}
@error={{@model.errors.password}}
- disabled={{@model.isSaving}}
- readonly={{false}}
autocomplete='new-password'
as |field|
>
diff --git a/ui/admin/app/components/form/credential/username_password/index.js b/ui/admin/app/components/form/credential/username_password/index.js
deleted file mode 100644
index 67d493c10..000000000
--- a/ui/admin/app/components/form/credential/username_password/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import Component from '@glimmer/component';
-import { types } from 'api/models/credential';
-
-export default class FormUserNamePasswordCredentialComponent extends Component {
- // =attributes
- /**
- * @type {Array}
- */
- credentialTypes = types;
-}
diff --git a/ui/admin/app/components/form/field/json-secret/index.js b/ui/admin/app/components/form/field/json-secret/index.js
index d70539cdb..d1e26ec09 100644
--- a/ui/admin/app/components/form/field/json-secret/index.js
+++ b/ui/admin/app/components/form/field/json-secret/index.js
@@ -41,4 +41,4 @@ export default class FormFieldJsonSecretComponent extends Component {
enableEditing() {
if (!this.args.disabled) this.editing = true;
}
-}
\ No newline at end of file
+}
diff --git a/ui/admin/app/components/form/host-catalog/aws/index.hbs b/ui/admin/app/components/form/host-catalog/aws/index.hbs
index 142ac44fd..601e8f1b7 100644
--- a/ui/admin/app/components/form/host-catalog/aws/index.hbs
+++ b/ui/admin/app/components/form/host-catalog/aws/index.hbs
@@ -44,62 +44,47 @@
{{#if @model.isNew}}
-
- <:title>{{t 'form.type.label'}}
- <:body>
-
+ {{t 'form.type.label'}}
+ {{#each this.hostCatalogTypes as |hostCatalogType|}}
+
-
-
-
-
-
+ {{t
+ (concat 'resources.host-catalog.types.' hostCatalogType)
+ }}
+ {{t
+ (concat 'resources.host-catalog.help.' hostCatalogType)
+ }}
+
+ {{/each}}
+
+
+ {{t 'titles.provider'}}
+ {{t 'titles.choose-a-provider'}}
+ {{t 'descriptions.choose-a-provider'}}
-
-
- <:title>{{t 'titles.provider'}}
-
-
-
- <:title>{{t 'titles.choose-a-provider'}}
- <:description>{{t 'descriptions.choose-a-provider'}}
-
- <:body>
-
-
-
-
-
-
+ {{t
+ (concat 'resources.host-catalog.types.' pluginType)
+ }}
+
+
+
+ {{/each-in}}
+
{{else}}
({ ...obj, [plugin]: icons[i] }),
+ {}
+ );
+ }
// =actions
@action
diff --git a/ui/admin/app/components/form/host-catalog/azure/index.hbs b/ui/admin/app/components/form/host-catalog/azure/index.hbs
index a09a3d411..807b18f84 100644
--- a/ui/admin/app/components/form/host-catalog/azure/index.hbs
+++ b/ui/admin/app/components/form/host-catalog/azure/index.hbs
@@ -44,67 +44,47 @@
{{#if @model.isNew}}
-
- <:title>{{t 'form.type.label'}}
- <:body>
-
+ {{t 'form.type.label'}}
+ {{#each this.hostCatalogTypes as |hostCatalogType|}}
+
-
-
-
-
-
-
-
-
- <:title>{{t 'titles.provider'}}
-
-
- <:title>{{t 'titles.choose-a-provider'}}
- <:description>
- {{concat
+ {{t
+ (concat 'resources.host-catalog.types.' hostCatalogType)
+ }}
+ {{t
+ (concat 'resources.host-catalog.help.' hostCatalogType)
+ }}
+
+ {{/each}}
+
+
+ {{t 'titles.provider'}}
+ {{t 'titles.choose-a-provider'}}
+ {{concat
(t 'descriptions.choose-a-provider')
- ' '
- (t 'descriptions.choose-a-provider-azure')
- }}
-
+ }}
- <:body>
-
-
-
-
-
-
+ {{t
+ (concat 'resources.host-catalog.types.' pluginType)
+ }}
+
+
+ {{/each-in}}
+
{{else}}
+
+
{{#if (can 'save model' @model)}}
({ ...obj, [plugin]: icons[i] }),
+ {}
+ );
+ }
+}
diff --git a/ui/admin/app/components/form/host-catalog/static/index.hbs b/ui/admin/app/components/form/host-catalog/static/index.hbs
index 2654c6608..3c2245981 100644
--- a/ui/admin/app/components/form/host-catalog/static/index.hbs
+++ b/ui/admin/app/components/form/host-catalog/static/index.hbs
@@ -46,60 +46,51 @@
{{#if @model.isNew}}
-
- <:title>{{t 'form.type.label'}}
- <:body>
-
+ {{t 'form.type.label'}}
+ {{#each this.hostCatalogTypes as |hostCatalogType|}}
+
-
-
-
-
-
+ {{t
+ (concat 'resources.host-catalog.types.' hostCatalogType)
+ }}
+ {{t
+ (concat 'resources.host-catalog.help.' hostCatalogType)
+ }}
+
+ {{/each}}
+
{{#if @model.isPlugin}}
-
-
- <:title>{{t 'titles.provider'}}
-
-
- <:title>{{t 'titles.choose-a-provider'}}
- <:description>{{t 'descriptions.choose-a-provider'}}
- <:body>
-
+ {{t 'titles.provider'}}
+ {{t 'titles.choose-a-provider'}}
+ {{t 'descriptions.choose-a-provider'}}
+
+ {{#each-in this.mapResourceTypeWithIcon as |pluginType|}}
+
-
-
-
-
-
+ {{t
+ (concat 'resources.host-catalog.types.' pluginType)
+ }}
+ {{t
+ (concat 'resources.host-catalog.help.' pluginType)
+ }}
+
+ {{/each-in}}
+
{{/if}}
{{/if}}
diff --git a/ui/admin/app/components/form/host-catalog/static/index.js b/ui/admin/app/components/form/host-catalog/static/index.js
new file mode 100644
index 000000000..c8f2e50f2
--- /dev/null
+++ b/ui/admin/app/components/form/host-catalog/static/index.js
@@ -0,0 +1,20 @@
+import Component from '@glimmer/component';
+import { types, pluginTypes } from 'api/models/host-catalog';
+
+//Note: this is a temporary solution till we have resource type helper in place
+const icons = ['aws-color', 'azure-color'];
+
+export default class FormStaticHostCatalogAwsComponent extends Component {
+ // =properties
+ hostCatalogTypes = types;
+ /**
+ * maps resource type with icon
+ * @type {object}
+ */
+ get mapResourceTypeWithIcon() {
+ return pluginTypes.reduce(
+ (obj, plugin, i) => ({ ...obj, [plugin]: icons[i] }),
+ {}
+ );
+ }
+}
diff --git a/ui/admin/app/components/form/host-set/aws/index.hbs b/ui/admin/app/components/form/host-set/aws/index.hbs
index cc131cd79..0f430dabb 100644
--- a/ui/admin/app/components/form/host-set/aws/index.hbs
+++ b/ui/admin/app/components/form/host-set/aws/index.hbs
@@ -53,26 +53,17 @@
@disabled={{true}}
/>
-
- <:title>{{t 'titles.provider'}}
- <:description>{{t 'descriptions.provider'}}
-
- <:body>
-
-
-
-
-
+
+ {{t 'titles.provider'}}
+
+ {{t 'resources.host-catalog.types.aws'}}
+
+
-
- <:title>{{t 'titles.provider'}}
- <:description>{{t 'descriptions.provider'}}
-
- <:body>
-
-
-
-
-
+
+ {{t 'titles.provider'}}
+
+ {{t 'resources.host-catalog.types.azure'}}
+
+
-
- <:title>{{t 'titles.provider'}}
- <:body>
-
-
-
-
-
+
+ {{t 'titles.provider'}}
+
+ {{t 'resources.host-catalog.types.aws'}}
+
+
<:title>{{t 'form.ip_addresses.label'}}
diff --git a/ui/admin/app/components/form/host/azure/index.hbs b/ui/admin/app/components/form/host/azure/index.hbs
index c87c94843..348fe2788 100644
--- a/ui/admin/app/components/form/host/azure/index.hbs
+++ b/ui/admin/app/components/form/host/azure/index.hbs
@@ -52,25 +52,17 @@
{{/if}}
-
- <:title>{{t 'titles.provider'}}
- <:body>
-
-
-
-
-
-
+
+ {{t 'titles.provider'}}
+
+ {{t 'resources.host-catalog.types.azure'}}
+
+
<:title>{{t 'form.ip_addresses.label'}}
<:body>
diff --git a/ui/admin/app/components/form/onboarding/choose-path/index.hbs b/ui/admin/app/components/form/onboarding/choose-path/index.hbs
index 9e91d69e9..1c4a59b96 100644
--- a/ui/admin/app/components/form/onboarding/choose-path/index.hbs
+++ b/ui/admin/app/components/form/onboarding/choose-path/index.hbs
@@ -5,26 +5,33 @@
@onSubmit={{fn @submit this.path}}
as |form|
>
-
-
-
-
-
-
+
+
+
+ {{t 'onboarding.quick-start.titles.quick-setup'}}
+ {{t
+ 'onboarding.quick-start.descriptions.quick-setup'
+ }}
+
+
+ {{t 'onboarding.quick-start.titles.manual'}}
+ {{t
+ 'onboarding.quick-start.descriptions.manual'
+ }}
+
+
+
\ No newline at end of file
diff --git a/ui/admin/app/components/form/role/index.hbs b/ui/admin/app/components/form/role/index.hbs
index 1a6a9a94e..a20c290f4 100644
--- a/ui/admin/app/components/form/role/index.hbs
+++ b/ui/admin/app/components/form/role/index.hbs
@@ -6,44 +6,47 @@
as |form|
>
-
+ {{t 'form.name.label'}}
+ {{t 'form.name.help'}}
{{#if @model.errors.name}}
-
+
{{#each @model.errors.name as |error|}}
- {{error.message}}
+ {{error.message}}
{{/each}}
-
+
{{/if}}
-
+
-
+ {{t 'form.description.label'}}
+ {{t 'form.description.help'}}
{{#if @model.errors.description}}
-
+
{{#each @model.errors.description as |error|}}
- {{error.message}}
+ {{error.message}}
{{/each}}
-
+
{{/if}}
-
+
{{#unless @model.isNew}}