Merge pull request #285 from hashicorp/merge-from-main

Merge from main
This commit is contained in:
Zhihe Li authored and GitHub committed 2023-05-24 17:37:38 -04:00
commit 67a23a50fe
67 files changed
+1782 -368

No files matched your search

+24
View File
@@ -11,6 +11,30 @@ import ModelAbility from './model';
export default class AccountAbility extends ModelAbility {
// =permissions
/**
* Only "known" account types may be read.
* @type {boolean}
*/
get canRead() {
return !this.model.isUnknown && this.hasAuthorizedAction('read');
}
/**
* Only "known" account types may be updated.
* @type {boolean}
*/
get canUpdate() {
return !this.model.isUnknown && this.hasAuthorizedAction('update');
}
/**
* Only "known" account types may be deleted.
* @type {boolean}
*/
get canDelete() {
return !this.model.isUnknown && this.hasAuthorizedAction('delete');
}
/**
* @type {boolean}
*/
+34
View File
@@ -0,0 +1,34 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import ModelAbility from './model';
/**
* Provides abilities for auth method.
*/
export default class AuthMethodAbility extends ModelAbility {
// =permissions
/**
* Only "known" auth method types may be read.
* @type {boolean}
*/
get canRead() {
return !this.model.isUnknown && this.hasAuthorizedAction('read');
}
/**
* Only "known" auth method types may be updated.
*/
get canUpdate() {
return !this.model.isUnknown && this.hasAuthorizedAction('update');
}
/**
* Only "known" auth method types may be deleted.
*/
get canDelete() {
return !this.model.isUnknown && this.hasAuthorizedAction('delete');
}
}
+16
View File
@@ -24,4 +24,20 @@ export default class UserAbility extends ModelAbility {
get canRemoveAccounts() {
return this.hasAuthorizedAction('remove-accounts');
}
/**
* @type {boolean}
*/
get canAddAccount() {
const { account, canAddAccounts } = this;
return canAddAccounts && !account.isUnknown;
}
/**
* @type {boolean}
*/
get canRemoveAccount() {
const { account, canRemoveAccounts } = this;
return canRemoveAccounts && !account.isUnknown;
}
}
+26 -5
View File
@@ -4,8 +4,12 @@
*/
import GeneratedAccountModel from '../generated/models/account';
import { computed } from '@ember/object';
import { equal } from '@ember/object/computed';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
TYPES_AUTH_METHOD,
} from './auth-method';
export default class AccountModel extends GeneratedAccountModel {
// =attributes
@@ -13,18 +17,35 @@ export default class AccountModel extends GeneratedAccountModel {
/**
* @type {boolean}
*/
@equal('type', 'password') isPassword;
get isPassword() {
return this.type === TYPE_AUTH_METHOD_PASSWORD;
}
/**
* @type {boolean}
*/
@equal('type', 'oidc') isOIDC;
get isOIDC() {
return this.type === TYPE_AUTH_METHOD_OIDC;
}
/**
* @type {boolean}
*/
get isLDAP() {
return this.type === TYPE_AUTH_METHOD_LDAP;
}
/**
* @type {boolean}
*/
get isUnknown() {
return !TYPES_AUTH_METHOD.includes(this.type);
}
/**
* Convenience for getting username in account.
* @type {string}
*/
@computed('{email,full_name,login_name}')
get accountName() {
const { email, full_name, login_name } = this;
return email || full_name || login_name;
+26 -2
View File
@@ -6,6 +6,16 @@
import GeneratedAuthMethodModel from '../generated/models/auth-method';
import { equal } from '@ember/object/computed';
export const TYPE_AUTH_METHOD_PASSWORD = 'password';
export const TYPE_AUTH_METHOD_OIDC = 'oidc';
export const TYPE_AUTH_METHOD_LDAP = 'ldap';
export const TYPES_AUTH_METHOD = Object.freeze([
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
]);
/**
* Enum options per auth method type and field.
*/
@@ -44,14 +54,28 @@ export default class AuthMethodModel extends GeneratedAuthMethodModel {
* @type {boolean}
*/
get isPassword() {
return this.type === 'password';
return this.type === TYPE_AUTH_METHOD_PASSWORD;
}
/**
* @type {boolean}
*/
get isOIDC() {
return this.type === 'oidc';
return this.type === TYPE_AUTH_METHOD_OIDC;
}
/**
* @type {boolean}
*/
get isLDAP() {
return this.type === TYPE_AUTH_METHOD_LDAP;
}
/**
* @type {boolean}
*/
get isUnknown() {
return !TYPES_AUTH_METHOD.includes(this.type);
}
/**
+2 -1
View File
@@ -7,6 +7,7 @@ import GeneratedRoleModel from '../generated/models/role';
import { attr } from '@ember-data/model';
import { inject as service } from '@ember/service';
import { all, resolve } from 'rsvp';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
export default class RoleModel extends GeneratedRoleModel {
// =services
@@ -133,7 +134,7 @@ export default class RoleModel extends GeneratedRoleModel {
// Collect all auth methods.
const authMethods = this.resourceFilterStore.queryBy(
'auth-method',
{ type: 'oidc' },
{ type: TYPE_AUTH_METHOD_OIDC },
{ scope_id: 'global', recursive: true }
);
+3 -1
View File
@@ -4,6 +4,8 @@
*/
import ApplicationSerializer from './application';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
export default class AuthMethodSerializer extends ApplicationSerializer {
// =methods
@@ -15,7 +17,7 @@ export default class AuthMethodSerializer extends ApplicationSerializer {
*/
serialize(snapshot) {
switch (snapshot.record.type) {
case 'oidc':
case TYPE_AUTH_METHOD_OIDC:
return this.serializeOIDC(...arguments);
default:
return this.serializeDefault(...arguments);
+6
View File
@@ -0,0 +1,6 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
export { default } from 'api/abilities/auth-method';
+8 -2
View File
@@ -7,6 +7,11 @@ import factory from '../generated/factories/account';
import { faker } from '@faker-js/faker';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
export default factory.extend({
authorized_actions: () =>
@@ -22,9 +27,10 @@ export default factory.extend({
attributes() {
switch (this.type) {
case 'password':
case TYPE_AUTH_METHOD_PASSWORD:
case TYPE_AUTH_METHOD_LDAP:
return { login_name: faker.random.words() };
case 'oidc':
case TYPE_AUTH_METHOD_OIDC:
return {
issuer: faker.internet.ip(),
subject: 'sub',
+6 -3
View File
@@ -8,7 +8,10 @@ import { trait } from 'miragejs';
import permissions from '../helpers/permissions';
import generateId from '../helpers/id';
const types = ['password', 'oidc'];
import {
TYPE_AUTH_METHOD_OIDC,
TYPES_AUTH_METHOD,
} from 'api/models/auth-method';
export default factory.extend({
authorized_actions: () =>
@@ -29,7 +32,7 @@ export default factory.extend({
id: () => generateId('am_'),
// Cycle through available types
type: (i) => types[i % types.length],
type: (i) => TYPES_AUTH_METHOD[i % TYPES_AUTH_METHOD.length],
/**
* Adds accounts (with associated users) to auth method and managed groups.
@@ -43,7 +46,7 @@ export default factory.extend({
user.update({ accountIds: [id] });
});
if (type === 'oidc') {
if (type === TYPE_AUTH_METHOD_OIDC) {
server
.createList('managed-group', 2, { scope, authMethod })
.map((managedGroup) => {
@@ -5,12 +5,13 @@
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
import { TYPE_AUTH_METHOD_PASSWORD } from 'api/models/auth-method';
/**
* GeneratedAccountFactory
*/
export default Factory.extend({
type: 'password',
type: TYPE_AUTH_METHOD_PASSWORD,
created_time: () => faker.date.recent(),
updated_time: () => faker.date.recent(),
version: () => faker.datatype.number(),
@@ -6,11 +6,16 @@
import { Factory } from 'miragejs';
import { faker } from '@faker-js/faker';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
} from 'api/models/auth-method';
/**
* GeneratedAuthMethodModelFactory
*/
export default Factory.extend({
type: 'password',
type: TYPE_AUTH_METHOD_PASSWORD,
name: () => faker.random.words(),
description: () => faker.random.words(),
created_time: () => faker.date.recent(),
@@ -20,7 +25,7 @@ export default Factory.extend({
attributes: function () {
let attrs;
switch (this.type) {
case 'oidc':
case TYPE_AUTH_METHOD_OIDC:
attrs = {
api_url_prefix: 'http://127.0.0.1:9200',
callback_url:
@@ -1,22 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Abilities | Account', function (hooks) {
setupTest(hooks);
test('it reflects when a given account may set password based on authorized_actions', function (assert) {
assert.expect(2);
const service = this.owner.lookup('service:can');
const model = {
authorized_actions: ['set-password'],
};
assert.ok(service.can('setPassword target', model));
model.authorized_actions = [];
assert.notOk(service.can('setPassword target', model));
});
});
@@ -0,0 +1,124 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
module('Unit | Abilities | Account', function (hooks) {
setupTest(hooks);
let canService;
let store;
hooks.beforeEach(function () {
canService = this.owner.lookup('service:can');
store = this.owner.lookup('service:store');
});
test('it reflects when a given account may set password based on authorized_actions', function (assert) {
assert.expect(2);
const account = store.createRecord('auth-method', {
authorized_actions: ['set-password'],
});
assert.true(canService.can('setPassword account', account));
account.authorized_actions = [];
assert.false(canService.can('setPassword account', account));
});
test('can read known account types when authorized', function (assert) {
assert.expect(4);
const account = store.createRecord('account', {
authorized_actions: ['read'],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.true(canService.can('read account', account));
account.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(canService.can('read account', account));
account.type = TYPE_AUTH_METHOD_LDAP;
assert.true(canService.can('read account', account));
account.type = 'no-such-type';
assert.false(canService.can('read account', account));
});
test('cannot read accounts when unauthorized', function (assert) {
assert.expect(4);
const account = store.createRecord('account', {
authorized_actions: [],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.false(canService.can('read account', account));
account.type = TYPE_AUTH_METHOD_PASSWORD;
assert.false(canService.can('read account', account));
account.type = TYPE_AUTH_METHOD_LDAP;
assert.false(canService.can('read account', account));
account.type = 'no-such-type';
assert.false(canService.can('read account', account));
});
test('can delete known account types when authorized', function (assert) {
assert.expect(4);
const account = store.createRecord('account', {
authorized_actions: ['delete'],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.true(canService.can('delete account', account));
account.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(canService.can('delete account', account));
account.type = TYPE_AUTH_METHOD_LDAP;
assert.true(canService.can('delete account', account));
account.type = 'no-such-type';
assert.false(canService.can('delete account', account));
});
test('cannot delete accounts when unauthorized', function (assert) {
assert.expect(4);
const account = store.createRecord('account', {
authorized_actions: ['delete'],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.true(canService.can('delete account', account));
account.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(canService.can('delete account', account));
account.type = TYPE_AUTH_METHOD_LDAP;
assert.true(canService.can('delete account', account));
account.type = 'no-such-type';
assert.false(canService.can('delete account', account));
});
test('can update known account types when authorized', function (assert) {
assert.expect(4);
const account = store.createRecord('account', {
authorized_actions: ['update'],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.true(canService.can('update account', account));
account.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(canService.can('update account', account));
account.type = TYPE_AUTH_METHOD_LDAP;
assert.true(canService.can('update account', account));
account.type = 'no-such-type';
assert.false(canService.can('update account', account));
});
test('cannot update accounts when unauthorized', function (assert) {
assert.expect(4);
const account = store.createRecord('account', {
authorized_actions: [],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.false(canService.can('update account', account));
account.type = TYPE_AUTH_METHOD_PASSWORD;
assert.false(canService.can('update account', account));
account.type = TYPE_AUTH_METHOD_LDAP;
assert.false(canService.can('update account', account));
account.type = 'no-such-type';
assert.false(canService.can('update account', account));
});
});
@@ -0,0 +1,114 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
module('Unit | Abilities | Auth Method', function (hooks) {
setupTest(hooks);
let canService;
let store;
hooks.beforeEach(function () {
canService = this.owner.lookup('service:can');
store = this.owner.lookup('service:store');
});
test('can read known auth method types when authorized', function (assert) {
assert.expect(4);
const authMethod = store.createRecord('auth-method', {
authorized_actions: ['read'],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.true(canService.can('read auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(canService.can('read auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_LDAP;
assert.true(canService.can('read auth-method', authMethod));
authMethod.type = 'no-such-type';
assert.false(canService.can('read auth-method', authMethod));
});
test('cannot read auth methods when unauthorized', function (assert) {
assert.expect(4);
const authMethod = store.createRecord('auth-method', {
authorized_actions: [],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.false(canService.can('read auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_PASSWORD;
assert.false(canService.can('read auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_LDAP;
assert.false(canService.can('read auth-method', authMethod));
authMethod.type = 'no-such-type';
assert.false(canService.can('read auth-method', authMethod));
});
test('can delete known auth method types when authorized', function (assert) {
assert.expect(4);
const authMethod = store.createRecord('auth-method', {
authorized_actions: ['delete'],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.true(canService.can('delete auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(canService.can('delete auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_LDAP;
assert.true(canService.can('delete auth-method', authMethod));
authMethod.type = 'no-such-type';
assert.false(canService.can('delete auth-method', authMethod));
});
test('cannot delete auth methods when unauthorized', function (assert) {
assert.expect(4);
const authMethod = store.createRecord('auth-method', {
authorized_actions: ['delete'],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.true(canService.can('delete auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(canService.can('delete auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_LDAP;
assert.true(canService.can('delete auth-method', authMethod));
authMethod.type = 'no-such-type';
assert.false(canService.can('delete auth-method', authMethod));
});
test('can update known auth method types when authorized', function (assert) {
assert.expect(4);
const authMethod = store.createRecord('auth-method', {
authorized_actions: ['update'],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.true(canService.can('update auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(canService.can('update auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_LDAP;
assert.true(canService.can('update auth-method', authMethod));
authMethod.type = 'no-such-type';
assert.false(canService.can('update auth-method', authMethod));
});
test('cannot update auth methods when unauthorized', function (assert) {
assert.expect(4);
const authMethod = store.createRecord('auth-method', {
authorized_actions: [],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.false(canService.can('update auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_PASSWORD;
assert.false(canService.can('update auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_LDAP;
assert.false(canService.can('update auth-method', authMethod));
authMethod.type = 'no-such-type';
assert.false(canService.can('update auth-method', authMethod));
});
});
+136 -18
View File
@@ -5,29 +5,147 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
TYPE_AUTH_METHOD_LDAP,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_PASSWORD,
} from 'api/models/auth-method';
module('Unit | Abilities | User', function (hooks) {
setupTest(hooks);
test('it reflects when a given user resource may add accounts based on authorized_actions', function (assert) {
assert.expect(2);
const service = this.owner.lookup('service:can');
const model = {
authorized_actions: ['add-accounts'],
};
assert.ok(service.can('addAccounts user', model));
model.authorized_actions = [];
assert.notOk(service.can('addAccounts user', model));
let canService;
let store;
const instances = {
account: null,
user: null,
};
hooks.beforeEach(function () {
canService = this.owner.lookup('service:can');
store = this.owner.lookup('service:store');
instances.user = store.createRecord('user');
instances.account = store.createRecord('account');
});
test('it reflects when a given user resource may remove accounts based on authorized_actions', function (assert) {
assert.expect(2);
const service = this.owner.lookup('service:can');
const model = {
authorized_actions: ['remove-accounts'],
};
assert.ok(service.can('removeAccounts user', model));
model.authorized_actions = [];
assert.notOk(service.can('removeAccounts user', model));
test('can add accounts to user when authorized and given a known account type', function (assert) {
assert.expect(5);
instances.user.authorized_actions = ['add-accounts'];
assert.true(canService.can('addAccounts user', instances.user));
instances.account.type = TYPE_AUTH_METHOD_OIDC;
assert.true(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = TYPE_AUTH_METHOD_LDAP;
assert.true(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = 'no-such-type';
assert.false(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
});
test('cannot add accounts to user when unauthorized', function (assert) {
assert.expect(5);
instances.user.authorized_actions = [];
assert.false(canService.can('addAccounts user', instances.user));
instances.account.type = TYPE_AUTH_METHOD_OIDC;
assert.false(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = TYPE_AUTH_METHOD_PASSWORD;
assert.false(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = TYPE_AUTH_METHOD_LDAP;
assert.false(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = 'no-such-type';
assert.false(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
});
test('can remove accounts from user when authorized and given a known account type', function (assert) {
assert.expect(5);
instances.user.authorized_actions = ['remove-accounts'];
assert.true(canService.can('removeAccounts user', instances.user));
instances.account.type = TYPE_AUTH_METHOD_OIDC;
assert.true(
canService.can('removeAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(
canService.can('removeAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = TYPE_AUTH_METHOD_LDAP;
assert.true(
canService.can('removeAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = 'no-such-type';
assert.false(
canService.can('removeAccount user', instances.user, {
account: instances.account,
})
);
});
test('cannot remove accounts from user when unauthorized', function (assert) {
assert.expect(5);
instances.user.authorized_actions = [];
assert.false(canService.can('removeAccounts user', instances.user));
instances.account.type = TYPE_AUTH_METHOD_OIDC;
assert.false(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = TYPE_AUTH_METHOD_PASSWORD;
assert.false(
canService.can('removeAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = TYPE_AUTH_METHOD_LDAP;
assert.false(
canService.can('removeAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = 'no-such-type';
assert.false(
canService.can('removeAccount user', instances.user, {
account: instances.account,
})
);
});
});
+82 -3
View File
@@ -5,24 +5,103 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
module('Unit | Model | account', function (hooks) {
setupTest(hooks);
let store;
hooks.beforeEach(function () {
store = this.owner.lookup('service:store');
});
test('it exists', function (assert) {
let store = this.owner.lookup('service:store');
store = this.owner.lookup('service:store');
let model = store.createRecord('account', {});
assert.ok(model);
});
test('it has an accountName attribute for password account', function (assert) {
assert.expect(2);
const store = this.owner.lookup('service:store');
store = this.owner.lookup('service:store');
let model = store.createRecord('account', {
type: 'password',
type: TYPE_AUTH_METHOD_PASSWORD,
});
assert.notOk(model.accountName);
model.login_name = 'foobar';
assert.strictEqual(model.accountName, 'foobar');
});
test('it has isPassword property and returns the expected values', function (assert) {
assert.expect(3);
const modelA = store.createRecord('account', {
type: TYPE_AUTH_METHOD_PASSWORD,
});
const modelB = store.createRecord('account', {
type: TYPE_AUTH_METHOD_OIDC,
});
const modelC = store.createRecord('account', {
type: TYPE_AUTH_METHOD_LDAP,
});
assert.true(modelA.isPassword);
assert.false(modelB.isPassword);
assert.false(modelC.isPassword);
});
test('it has isOIDC property and returns the expected values', function (assert) {
assert.expect(3);
const modelA = store.createRecord('account', {
type: TYPE_AUTH_METHOD_OIDC,
});
const modelB = store.createRecord('account', {
type: TYPE_AUTH_METHOD_PASSWORD,
});
const modelC = store.createRecord('account', {
type: TYPE_AUTH_METHOD_LDAP,
});
assert.true(modelA.isOIDC);
assert.false(modelB.isOIDC);
assert.false(modelC.isOIDC);
});
test('it has isLDAP property and returns the expected values', function (assert) {
assert.expect(3);
const modelA = store.createRecord('account', {
type: TYPE_AUTH_METHOD_LDAP,
});
const modelB = store.createRecord('account', {
type: TYPE_AUTH_METHOD_PASSWORD,
});
const modelC = store.createRecord('account', {
type: TYPE_AUTH_METHOD_OIDC,
});
assert.true(modelA.isLDAP);
assert.false(modelB.isLDAP);
assert.false(modelC.isLDAP);
});
test('it has isUnknown property and returns the expected values', function (assert) {
assert.expect(4);
const modelA = store.createRecord('account', {
type: TYPE_AUTH_METHOD_LDAP,
});
const modelB = store.createRecord('account', {
type: TYPE_AUTH_METHOD_PASSWORD,
});
const modelC = store.createRecord('account', {
type: TYPE_AUTH_METHOD_OIDC,
});
const modelD = store.createRecord('account', {
type: 'no-such-type',
});
assert.false(modelA.isUnknown);
assert.false(modelB.isUnknown);
assert.false(modelC.isUnknown);
assert.true(modelD.isUnknown);
});
});
@@ -6,13 +6,23 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
module('Unit | Model | auth method', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
let store;
hooks.beforeEach(function () {
store = this.owner.lookup('service:store');
});
test('it exists', function (assert) {
let store = this.owner.lookup('service:store');
let model = store.createRecord('auth-method', {});
assert.ok(model);
});
@@ -32,13 +42,12 @@ module('Unit | Model | auth method', function (hooks) {
return { id: '123abc' };
}
);
const store = this.owner.lookup('service:store');
store.push({
data: {
id: '123abc',
type: 'auth-method',
attributes: {
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
state: 'foobar',
version: 1,
scope: {
@@ -51,4 +60,72 @@ module('Unit | Model | auth method', function (hooks) {
const model = store.peekRecord('auth-method', '123abc');
await model.changeState('foobar');
});
test('it has isPassword property and returns the expected values', function (assert) {
assert.expect(3);
const modelA = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_PASSWORD,
});
const modelB = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_OIDC,
});
const modelC = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_LDAP,
});
assert.true(modelA.isPassword);
assert.false(modelB.isPassword);
assert.false(modelC.isPassword);
});
test('it has isOIDC property and returns the expected values', function (assert) {
assert.expect(3);
const modelA = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_OIDC,
});
const modelB = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_PASSWORD,
});
const modelC = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_LDAP,
});
assert.true(modelA.isOIDC);
assert.false(modelB.isOIDC);
assert.false(modelC.isOIDC);
});
test('it has isLDAP property and returns the expected values', function (assert) {
assert.expect(3);
const modelA = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_LDAP,
});
const modelB = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_PASSWORD,
});
const modelC = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_OIDC,
});
assert.true(modelA.isLDAP);
assert.false(modelB.isLDAP);
assert.false(modelC.isLDAP);
});
test('it has isUnknown property and returns the expected values', function (assert) {
assert.expect(4);
const modelA = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_LDAP,
});
const modelB = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_PASSWORD,
});
const modelC = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_OIDC,
});
const modelD = store.createRecord('auth-method', {
type: 'no-such-type',
});
assert.false(modelA.isUnknown);
assert.false(modelB.isUnknown);
assert.false(modelC.isUnknown);
assert.true(modelD.isUnknown);
});
});
@@ -5,6 +5,10 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
} from 'api/models/auth-method';
module('Unit | Serializer | account', function (hooks) {
setupTest(hooks);
@@ -14,7 +18,7 @@ module('Unit | Serializer | account', function (hooks) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('account');
const record = store.createRecord('account', {
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
name: 'Name',
auth_method_id: '1',
description: 'Description',
@@ -27,7 +31,7 @@ module('Unit | Serializer | account', function (hooks) {
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
name: 'Name',
auth_method_id: '1',
description: 'Description',
@@ -48,7 +52,7 @@ module('Unit | Serializer | account', function (hooks) {
id: '1',
type: 'account',
attributes: {
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
name: 'Name',
auth_method_id: '1',
description: 'Description',
@@ -65,7 +69,7 @@ module('Unit | Serializer | account', function (hooks) {
const serializedRecord = serializer.serialize(snapshot);
assert.notOk(serializedRecord.attributes);
assert.deepEqual(serializedRecord, {
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
name: 'Name',
auth_method_id: '1',
description: 'Description',
@@ -78,7 +82,7 @@ module('Unit | Serializer | account', function (hooks) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('account');
const record = store.createRecord('account', {
type: 'password',
type: TYPE_AUTH_METHOD_PASSWORD,
name: 'Name',
auth_method_id: '1',
description: 'Description',
@@ -88,7 +92,7 @@ module('Unit | Serializer | account', function (hooks) {
const snapshot = record._createSnapshot();
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
type: 'password',
type: TYPE_AUTH_METHOD_PASSWORD,
name: 'Name',
auth_method_id: '1',
description: 'Description',
@@ -104,7 +108,7 @@ module('Unit | Serializer | account', function (hooks) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('account');
const record = store.createRecord('account', {
type: 'password',
type: TYPE_AUTH_METHOD_PASSWORD,
name: 'Name',
auth_method_id: '1',
description: 'Description',
@@ -117,7 +121,7 @@ module('Unit | Serializer | account', function (hooks) {
};
const serializedRecord = serializer.serialize(snapshot);
assert.deepEqual(serializedRecord, {
type: 'password',
type: TYPE_AUTH_METHOD_PASSWORD,
name: 'Name',
auth_method_id: '1',
description: 'Description',
@@ -138,7 +142,7 @@ module('Unit | Serializer | account', function (hooks) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('account');
const record = store.createRecord('account', {
type: 'password',
type: TYPE_AUTH_METHOD_PASSWORD,
name: 'Name',
description: 'Description',
login_name: 'Login Name',
@@ -162,7 +166,7 @@ module('Unit | Serializer | account', function (hooks) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('account');
const record = store.createRecord('account', {
type: 'password',
type: TYPE_AUTH_METHOD_PASSWORD,
name: 'Name',
description: 'Description',
login_name: 'Login Name',
@@ -7,6 +7,11 @@ import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
} from 'api/models/auth-method';
module('Unit | Serializer | auth method', function (hooks) {
setupTest(hooks);
setupMirage(hooks);
@@ -21,7 +26,9 @@ module('Unit | Serializer | auth method', function (hooks) {
test('it serializes password records', function (assert) {
assert.expect(2);
let store = this.owner.lookup('service:store');
let record = store.createRecord('auth-method', { type: 'password' });
let record = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_PASSWORD,
});
let serializedRecord = record.serialize();
@@ -36,7 +43,7 @@ module('Unit | Serializer | auth method', function (hooks) {
assert.expect(1);
let store = this.owner.lookup('service:store');
let record = store.createRecord('auth-method', {
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
name: 'OIDC Auth Method',
state: 'foo',
account_claim_maps: [{ from: 'foo', to: 'bar' }],
@@ -62,7 +69,7 @@ module('Unit | Serializer | auth method', function (hooks) {
let serializedRecord = record.serialize();
assert.deepEqual(serializedRecord, {
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
name: 'OIDC Auth Method',
description: null,
attributes: {
@@ -87,7 +94,7 @@ module('Unit | Serializer | auth method', function (hooks) {
const store = this.owner.lookup('service:store');
const serializer = store.serializerFor('auth-method');
const record = store.createRecord('auth-method', {
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
attributes: {
state: 'foo',
},
@@ -175,7 +182,7 @@ module('Unit | Serializer | auth method', function (hooks) {
max_age: maxAge,
},
version: 1,
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
id: 'oidc123',
}));
+13
View File
@@ -0,0 +1,13 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import PasswordAuthenticator from './password';
/**
* A simple login_name/password authenticator that authenticates with
* a scope/auth-method endpoint. Authentication occurs on an auth method
* endpoint, while deauthentication occurs on a scope endpoint.
*/
export default class LDAPAuthenticator extends PasswordAuthenticator {}
@@ -0,0 +1,41 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<Rose::Form
class='full-width'
@onSubmit={{fn
this.submit
@onSubmit
(hash identification=this.identification password=this.password)
}}
@disabled={{@disabled}}
as |form|
>
<Hds::Form::TextInput::Field
name='identification'
@value={{this.identification}}
@type='text'
{{on 'input' (set-from-event this 'identification')}}
as |F|
>
<F.Label>{{t 'form.login_name.label'}}</F.Label>
</Hds::Form::TextInput::Field>
<Hds::Form::TextInput::Field
name='password'
@value={{this.password}}
@type='password'
{{on 'input' (set-from-event this 'password')}}
as |F|
>
<F.Label>{{t 'form.password.label'}}</F.Label>
</Hds::Form::TextInput::Field>
<form.actions
@submitText={{t 'actions.authenticate'}}
@showCancel={{false}}
/>
</Rose::Form>
@@ -8,7 +8,7 @@ import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { run } from '@ember/runloop';
export default class FormAuthenticatePasswordComponent extends Component {
export default class FormAuthenticateDetailsComponent extends Component {
// =properties
/**
@@ -9,7 +9,6 @@
{{component
(concat 'form/authenticate/' @model.type)
submit=@submit
cancel=false
disabled=@disabled
}}
{{/if}}
@@ -0,0 +1,6 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<Form::Authenticate::Details @onSubmit={{@submit}} @disabled={{@disabled}} />
@@ -6,7 +6,6 @@
<Rose::Form
class='full-width'
@onSubmit={{@submit}}
@cancel={{false}}
@disabled={{@disabled}}
as |form|
>
@@ -3,35 +3,4 @@
SPDX-License-Identifier: MPL-2.0
}}
<Rose::Form
class='full-width'
@onSubmit={{fn
this.submit
@submit
(hash identification=this.identification password=this.password)
}}
@cancel={{false}}
@disabled={{@disabled}}
as |form|
>
<form.input
@name='identification'
@type='text'
@value={{this.identification}}
@label={{t 'form.login_name.label'}}
/>
<form.input
@name='password'
@type='password'
@value={{this.password}}
@label={{t 'form.password.label'}}
/>
<form.actions
@submitText={{t 'actions.authenticate'}}
@showCancel={{false}}
/>
</Rose::Form>
<Form::Authenticate::Details @onSubmit={{@submit}} @disabled={{@disabled}} />
+6
View File
@@ -0,0 +1,6 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
export { default } from 'core/authenticators/ldap';
@@ -0,0 +1,6 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
export { default } from 'core/components/form/authenticate/details';
@@ -0,0 +1,6 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
export { default } from 'core/components/form/authenticate/ldap';
@@ -0,0 +1,89 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupIntl } from 'ember-intl/test-support';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
module('Integration | Component | form | authenticate', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
test('it shows login name and password when LDAP is selected from the list', async function (assert) {
assert.expect(3);
const store = this.owner.lookup('service:store');
this.model = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_LDAP,
});
this.submit = () => {};
this.disabled = () => {};
await render(hbs`<Form::Authenticate
@model={{this.model}}
@submit={{this.submit}}
@disabled={{this.disabled}}/>`);
assert.ok(find('.full-width'));
assert.ok(find('.hds-form-text-input'));
assert.strictEqual(
find('button[type="submit"]').textContent.trim(),
'Sign In'
);
});
test('it shows login name and password when password is selected from the list', async function (assert) {
assert.expect(3);
const store = this.owner.lookup('service:store');
this.model = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_PASSWORD,
});
this.submit = () => {};
this.disabled = () => {};
await render(hbs`<Form::Authenticate
@model={{this.model}}
@submit={{this.submit}}
@disabled={{this.disabled}}/>`);
assert.ok(find('.full-width'));
assert.ok(find('.hds-form-text-input'));
assert.strictEqual(
find('button[type="submit"]').textContent.trim(),
'Sign In'
);
});
test('it should only show sign in button when OIDC is selected from the list', async function (assert) {
assert.expect(3);
const store = this.owner.lookup('service:store');
this.model = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_OIDC,
});
this.submit = () => {};
this.disabled = () => {};
await render(hbs`<Form::Authenticate
@model={{this.model}}
@submit={{this.submit}}
@disabled={{this.disabled}}/>`);
assert.ok(find('.full-width'));
assert.notOk(find('.hds-form-text-input'));
assert.strictEqual(
find('button[type="submit"]').textContent.trim(),
'Sign In'
);
});
});
@@ -47,6 +47,7 @@ auth-method:
types:
password: Password
oidc: OIDC
ldap: LDAP
actions:
create: Create Auth Method
delete: Delete Auth Method
@@ -24,6 +24,10 @@
[class^='hds-form-group'] {
margin-bottom: 0;
}
.hds-table__td p {
color: var(--token-form-indicator-optional-color);
}
}
.hds-dropdown {
+2 -4
View File
@@ -50,9 +50,6 @@
},
"resolutions": {
"request": "^2.88.0",
"cryptiles": "~4.1.2",
"prismjs": "^1.27.0",
"npm-user-validate": "^1.0.1",
"caniuse-lite": "^1.0.30001157",
"immer": "^9.0.6",
"node-notifier": "^8.0.1",
@@ -101,7 +98,8 @@
"loader-utils": "^2.0.4",
"**/@storybook/**/minimatch": "^3.0.5",
"**/ember-electron/**/minimatch": "^3.0.5",
"**/babel-core/json5": "^2.2.2"
"**/babel-core/json5": "^2.2.2",
"**/react-syntax-highlighter/refractor/prismjs": "^1.27.0"
},
"config": {
"commitizen": {
+27
View File
@@ -0,0 +1,27 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import AuthMethodAbility from 'api/abilities/auth-method';
import { inject as service } from '@ember/service';
export default class OverrideAuthMethodAbility extends AuthMethodAbility {
//service
@service features;
/**
* These override abilities are temporary and just part of Phase 1
* of LDAP auth method implementation.
*/
get canRead() {
return this.features.isEnabled('ldap-auth-methods') || !this.model.isLDAP
? super.canRead
: false;
}
get canMakePrimary() {
return this.features.isEnabled('ldap-auth-methods') || !this.model.isLDAP;
}
}
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import UserAbility from 'api/abilities/user';
import { inject as service } from '@ember/service';
export default class OverrideUserAbility extends UserAbility {
// =service
@service features;
/**
* These override abilities are temporary and just part of Phase 1
* of LDAP auth method implementation.
*/
get canAddAccount() {
return this.features.isEnabled('ldap-auth-methods') || !this.account.isLDAP
? super.canAddAccount
: false;
}
get canRemoveAccount() {
return this.features.isEnabled('ldap-auth-methods') || !this.account.isLDAP
? super.canRemoveAccount
: false;
}
}
@@ -3,7 +3,7 @@
SPDX-License-Identifier: MPL-2.0
}}
{{#if (can 'delete model' @model)}}
{{#if (can 'delete account' @model)}}
<Rose::Dropdown
@text={{t 'actions.manage'}}
@dropdownRight={{true}}
@@ -81,7 +81,7 @@
{{/if}}
{{/if}}
{{#if (can 'delete model' @model)}}
{{#if (can 'delete auth-method' @model)}}
<dropdown.separator />
<dropdown.button
@style='danger'
@@ -30,11 +30,13 @@
{{#each this.filteredAccounts as |account|}}
<body.row as |row|>
<row.cell>
<form.checkbox
@label={{account.id}}
@description={{account.description}}
@onChange={{fn this.toggleAccount account}}
/>
<Hds::Form::Checkbox::Field
{{on 'change' (fn this.toggleAccount account)}}
as |F|
>
<F.Label>{{account.id}}</F.Label>
<F.HelperText>{{account.description}}</F.HelperText>
</Hds::Form::Checkbox::Field>
</row.cell>
<row.cell>{{account.accountName}}</row.cell>
<row.cell>
@@ -4,12 +4,13 @@
*/
import Component from '@glimmer/component';
import { computed, action } from '@ember/object';
import { action } from '@ember/object';
import { A } from '@ember/array';
import { inject as service } from '@ember/service';
export default class FormUserAddAccountsComponent extends Component {
// =properties
@service can;
/**
* Array of selected account IDs.
* @type {EmberArray}
@@ -21,7 +22,6 @@ export default class FormUserAddAccountsComponent extends Component {
* @param {[AccountModel]} filteredAccounts
* @type {boolean}
*/
@computed('filteredAccounts.length')
get hasAvailableAccounts() {
return this.filteredAccounts.length > 0;
}
@@ -30,12 +30,13 @@ export default class FormUserAddAccountsComponent extends Component {
* Accounts not already added to the user.
* @type {[AccountModel]}
*/
@computed('args.{accounts.[],model.account_ids.[]}')
get filteredAccounts() {
// Get IDs for accounts already added to the current user
const alreadyAddedAccountIDs = this.args.model.account_ids;
const notAddedAccounts = this.args.accounts.filter(
({ id }) => !alreadyAddedAccountIDs.includes(id)
(account) =>
!alreadyAddedAccountIDs.includes(account.id) &&
this.can.can('addAccount user', this.args.model, { account })
);
return notAddedAccounts;
}
@@ -10,6 +10,7 @@ import { loading } from 'ember-loading';
import { confirm } from 'core/decorators/confirm';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { resourceFilter } from 'core/decorators/resource-filter';
import { TYPES_AUTH_METHOD } from 'api/models/auth-method';
export default class ScopesScopeAuthMethodsRoute extends Route {
// =services
@@ -23,7 +24,7 @@ export default class ScopesScopeAuthMethodsRoute extends Route {
// =attributes
@resourceFilter({ allowed: ['password', 'oidc'] }) type;
@resourceFilter({ allowed: TYPES_AUTH_METHOD }) type;
// =methods
@@ -9,6 +9,11 @@ import { getOwner } from '@ember/application';
import { action } from '@ember/object';
import { loading } from 'ember-loading';
import { notifyError } from 'core/decorators/notify';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_LDAP,
} from 'api/models/auth-method';
export default class ScopesScopeAuthenticateMethodRoute extends Route {
// =services
@@ -62,7 +67,8 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
const authenticatorName = `authenticator:${authMethod.type}`;
const requestCookies = false;
switch (authMethod.type) {
case 'password':
case TYPE_AUTH_METHOD_PASSWORD:
case TYPE_AUTH_METHOD_LDAP:
await this.session.authenticate(
authenticatorName,
creds,
@@ -71,7 +77,7 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
);
this.router.transitionTo('index');
break;
case 'oidc':
case TYPE_AUTH_METHOD_OIDC:
await this.startOIDCAuthentication(authenticatorName, {
scope,
authMethod,
@@ -10,6 +10,7 @@ import { hash, all } from 'rsvp';
import { loading } from 'ember-loading';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { resourceFilter } from 'core/decorators/resource-filter';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
export default class ScopesScopeRolesRoleAddPrincipalsRoute extends Route {
// =services
@@ -75,12 +76,12 @@ export default class ScopesScopeRolesRoleAddPrincipalsRoute extends Route {
const oidcAuthMethods = scopeIDs?.length
? await this.resourceFilterStore.queryBy(
'auth-method',
{ scope_id: scopeIDs, type: 'oidc' },
{ scope_id: scopeIDs, type: TYPE_AUTH_METHOD_OIDC },
{ scope_id: 'global', recursive: true }
)
: await this.resourceFilterStore.queryBy(
'auth-method',
{ type: 'oidc' },
{ type: TYPE_AUTH_METHOD_OIDC },
{ scope_id: 'global', recursive: true }
);
//extract oidc authMethod IDs
@@ -27,8 +27,8 @@ export default class ScopesScopeUsersUserAddAccountsRoute extends Route {
}
/**
* Returns the current target, all host catalogs, and all host sets.
* @return {{target: TargetModel, hostCatalogs: [HostCatalogModel], hostSets: [HostSetModel]}}
* Returns the current user, all auth methods, and all accounts.
* @return {{user: UserModel, authMethods: [AuthMethodModel], accounts: [AccountModel]}}
*/
async model() {
const user = this.modelFor('scopes.scope.users.user');
+8
View File
@@ -739,6 +739,14 @@
/* stylelint-enable selector-class-pattern */
}
.auth-methods-list-alert {
margin-bottom: 1rem;
}
.auth-methods-list-table {
margin-top: 1rem;
}
// secret input field
.secret-input {
display: flex;
@@ -41,7 +41,7 @@
{{#each @model.accounts as |account|}}
<body.row as |row|>
<row.headerCell scope='row'>
{{#if (can 'read model' account)}}
{{#if (can 'read account' account)}}
<LinkTo
@route='scopes.scope.auth-methods.auth-method.accounts.account'
@model={{account.id}}
@@ -95,7 +95,7 @@
{{#each @model.accounts as |account|}}
<body.row as |row|>
<row.headerCell>
{{#if (can 'read model' account)}}
{{#if (can 'read account' account)}}
<LinkTo
@route='scopes.scope.auth-methods.auth-method.accounts.account'
@model={{account.id}}
@@ -41,6 +41,25 @@
<page.body>
{{#if @model}}
{{#unless this.hasPrimaryAuthMethod}}
<Hds::Alert
class='auth-methods-list-alert'
@type='inline'
@color='warning'
as |A|
>
<A.Title>
{{t 'resources.auth-method.messages.no-primary-auth-method.title'}}
</A.Title>
<A.Description>
{{t
'resources.auth-method.messages.no-primary-auth-method.description'
}}
</A.Description>
</Hds::Alert>
{{/unless}}
{{/if}}
{{#if
(or
@model
@@ -92,96 +111,80 @@
{{/if}}
{{#if @model}}
{{#unless this.hasPrimaryAuthMethod}}
<Rose::Notification
@style='warning'
@heading={{t
'resources.auth-method.messages.no-primary-auth-method.title'
}}
>
{{t
'resources.auth-method.messages.no-primary-auth-method.description'
}}
</Rose::Notification>
{{/unless}}
<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.cell />
<row.headerCell>{{t 'titles.actions'}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
{{#each @model as |authMethod|}}
<body.row as |row|>
<row.headerCell>
{{#if (can 'read model' authMethod)}}
<LinkTo
@route='scopes.scope.auth-methods.auth-method'
@model={{authMethod.id}}
>
{{authMethod.displayName}}
</LinkTo>
{{else}}
{{authMethod.displayName}}
{{/if}}
<p>{{authMethod.description}}</p>
</row.headerCell>
<row.cell>
<Hds::Badge
@text={{t
(concat 'resources.auth-method.types.' authMethod.type)
}}
/>
</row.cell>
<row.cell>
<Copyable
@text={{authMethod.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
<Hds::Table
class='auth-methods-list-table'
@model={{@model}}
@columns={{array
(hash label=(t 'form.name.label'))
(hash label=(t 'form.type.label'))
(hash label=(t 'form.id.label'))
(hash label='')
(hash label=(t 'titles.actions'))
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
{{#if (can 'read auth-method' B.data)}}
<LinkTo
@route='scopes.scope.auth-methods.auth-method'
@model={{B.data.id}}
>
<code>{{authMethod.id}}</code>
</Copyable>
</row.cell>
<row.cell>
{{#if authMethod.isPrimary}}
<Hds::Badge @text={{t 'states.primary'}} @color='success' />
{{/if}}
</row.cell>
<row.cell>
<Rose::Dropdown
@text={{t 'actions.manage'}}
@dropdownRight={{true}}
@iconOnly={{true}}
@showCaret={{false}}
@icon='flight-icons/svg/more-horizontal-16'
as |dropdown|
>
{{#unless authMethod.isPrimary}}
<dropdown.button
{{on 'click' (route-action 'makePrimary' authMethod)}}
>
{{t 'resources.auth-method.actions.make-primary'}}
</dropdown.button>
{{B.data.displayName}}
</LinkTo>
{{else}}
{{B.data.displayName}}
{{/if}}
<p>{{B.data.description}}</p>
</B.Td>
<B.Td>
<Hds::Badge
@text={{t (concat 'resources.auth-method.types.' B.data.type)}}
/>
</B.Td>
<B.Td>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<code>{{B.data.id}}</code>
</Copyable>
</B.Td>
<B.Td>
{{#if B.data.isPrimary}}
<Hds::Badge @text={{t 'states.primary'}} @color='success' />
{{/if}}
</B.Td>
<B.Td>
{{#if (can 'makePrimary auth-method' B.data)}}
<Hds::Dropdown as |dd|>
<dd.ToggleIcon
@icon='more-horizontal'
@text={{t 'actions.manage'}}
@hasChevron={{false}}
/>
{{#unless B.data.isPrimary}}
<dd.Interactive
@text={{t 'resources.auth-method.actions.make-primary'}}
{{on 'click' (route-action 'makePrimary' B.data)}}
/>
{{/unless}}
{{#if authMethod.isPrimary}}
<dropdown.button
{{on 'click' (route-action 'removeAsPrimary' authMethod)}}
>
{{t 'resources.auth-method.actions.remove-as-primary'}}
</dropdown.button>
{{#if B.data.isPrimary}}
<dd.Interactive
@text={{t
'resources.auth-method.actions.remove-as-primary'
}}
{{on 'click' (route-action 'removeAsPrimary' B.data)}}
/>
{{/if}}
</Rose::Dropdown>
</row.cell>
</body.row>
{{/each}}
</table.body>
</Rose::Table>
</Hds::Dropdown>
{{/if}}
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{else if (has-resource-filter-selections
'scopes.scope.auth-methods' 'type'
@@ -59,7 +59,7 @@
</Copyable>
</row.cell>
<row.cell>
{{#if (can 'removeAccounts user' @model.user)}}
{{#if (can 'removeAccount user' @model.user account=account)}}
<Rose::Dropdown
@icon='flight-icons/svg/more-horizontal-16'
@iconOnly={{true}}
+2
View File
@@ -28,6 +28,7 @@ const baseEdition = {
'target-worker-filters-v2-hcp': false,
'target-network-address': false,
'vault-worker-filter': false,
'ldap-auth-methods': false,
};
// Editions maps edition keys to their associated featuresets.
const featureEditions = {};
@@ -38,6 +39,7 @@ featureEditions.oss = {
'static-credentials': true,
'target-worker-filters-v2': true,
'target-network-address': true,
'ldap-auth-methods': false,
};
featureEditions.enterprise = {
...featureEditions.oss,
+1 -1
View File
@@ -10,7 +10,7 @@
"test": "tests"
},
"scripts": {
"doc:toc": "doctoc README.md",
"doc:toc": "doctoc README.md tests/e2e/README.md",
"build:development": "ember build",
"build": "ember build --environment=production",
"build:oss": "EDITION=oss ember build --environment=production",
@@ -22,10 +22,13 @@ import {
// //invalidateSession,
} from 'ember-simple-auth/test-support';
module('Acceptance | auth-methods | create ', function (hooks) {
module('Acceptance | auth-methods | create', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
const DROPDOWN_SELECTOR_ICON =
'tbody .hds-table__tr:nth-child(1) .hds-table__td:last-child .hds-dropdown-toggle-icon';
const DROPDOWN_SELECTOR_OPTION =
'.hds-dropdown__content .hds-dropdown-list-item [type=button]';
let getAuthMethodsCount;
const instances = {
scopes: {
@@ -255,24 +258,27 @@ module('Acceptance | auth-methods | create ', function (hooks) {
});
test('user can make and remove primary auth methods from index', async function (assert) {
assert.expect(3);
assert.expect(4);
assert.notOk(
instances.orgScope.primaryAuthMethodId,
'Primary auth method is not yet set.'
);
await visit(urls.authMethods);
await click(
'.rose-table-body .rose-table-row:first-child .rose-dropdown-content [type="button"]:first-child'
);
await click(DROPDOWN_SELECTOR_ICON);
assert.dom(DROPDOWN_SELECTOR_OPTION).exists();
await click(DROPDOWN_SELECTOR_OPTION);
let scope = this.server.schema.scopes.find(instances.orgScope.id);
assert.strictEqual(
scope.primaryAuthMethodId,
instances.authMethod.id,
'Primary auth method is set.'
);
await click(
'.rose-table-body .rose-table-row:first-child .rose-dropdown-content [type="button"]:first-child'
);
await click(DROPDOWN_SELECTOR_ICON);
await click(DROPDOWN_SELECTOR_OPTION);
scope = this.server.schema.scopes.find(instances.orgScope.id);
assert.notOk(scope.primaryAuthMethodId, 'Primary auth method is unset.');
});
@@ -23,6 +23,8 @@ import {
//invalidateSession,
} from 'ember-simple-auth/test-support';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
module('Acceptance | auth methods | oidc', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
@@ -49,7 +51,7 @@ module('Acceptance | auth methods | oidc', function (hooks) {
});
instances.authMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
});
urls.orgScope = `/scopes/${instances.scopes.org.id}`;
@@ -4,7 +4,7 @@
*/
import { module, test } from 'qunit';
import { visit, currentURL, click, find } from '@ember/test-helpers';
import { visit, currentURL, click } 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';
@@ -14,23 +14,43 @@ import {
// //currentSession,
// //invalidateSession,
} from 'ember-simple-auth/test-support';
import {
TYPE_AUTH_METHOD_LDAP,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_PASSWORD,
} from 'api/models/auth-method';
module('Acceptance | auth methods | read', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
const AUTH_LINK_SELECTOR = 'main tbody .hds-table__td:nth-child(1) a';
const AUTH_TYPE_SELECTOR =
'tbody .hds-table__tr:nth-child(1) .hds-table__td:nth-child(2)';
const AUTH_ACTIONS_SELECTOR =
'tbody .hds-table__tr:nth-child(1) .hds-table__td:last-child .hds-dropdown';
const LDAP_AUTH_LINK_SELECTOR = 'tbody .hds-table__tr:nth-child(2) a';
const LDAP_AUTH_TYPE_SELECTOR =
'tbody .hds-table__tr:nth-child(2) .hds-table__td:nth-child(2)';
const LDAP_AUTH_ACTIONS_SELECTOR =
'tbody .hds-table__tr:nth-child(2) .hds-table__td:last-child .hds-dropdown';
const instances = {
scopes: {
global: null,
org: null,
},
authMethod: null,
passwordAuthMethodOrg: null,
oidcAuthMethodGlobal: null,
ldapAuthMethod: null,
};
const urls = {
globalScope: null,
orgScope: null,
authMethods: null,
newAuthMethod: null,
authMethod: null,
orgAuthMethods: null,
globalAuthMethods: null,
passwordAuthMethodOrg: null,
oidcAuthMethodGlobal: null,
};
hooks.beforeEach(function () {
@@ -41,37 +61,126 @@ module('Acceptance | auth methods | read', function (hooks) {
type: 'org',
scope: { id: 'global', type: 'global' },
});
instances.authMethod = this.server.create('auth-method', {
instances.passwordAuthMethodOrg = this.server.create('auth-method', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_PASSWORD,
});
instances.oidcAuthMethodGlobal = this.server.create('auth-method', {
scope: instances.scopes.global,
type: TYPE_AUTH_METHOD_OIDC,
});
// Generate route URLs for resources
urls.globalScope = `/scopes/global`;
urls.orgScope = `/scopes/${instances.scopes.org.id}`;
urls.authMethods = `${urls.orgScope}/auth-methods`;
urls.newAuthMethod = `${urls.authMethods}/new?type=password`;
urls.authMethod = `${urls.authMethods}/${instances.authMethod.id}`;
urls.orgAuthMethods = `${urls.orgScope}/auth-methods`;
urls.globalAuthMethods = `${urls.globalScope}/auth-methods`;
urls.passwordAuthMethodOrg = `${urls.orgAuthMethods}/${instances.passwordAuthMethodOrg.id}`;
urls.oidcAuthMethodGlobal = `${urls.globalAuthMethods}/${instances.oidcAuthMethodGlobal.id}`;
});
test('visiting auth methods', async function (assert) {
test('visiting auth methods in org scope', async function (assert) {
assert.expect(1);
await visit(urls.authMethods);
await visit(urls.orgScope);
await click(`[href="${urls.orgAuthMethods}"]`);
await a11yAudit();
assert.strictEqual(currentURL(), urls.authMethods);
assert.strictEqual(currentURL(), urls.orgAuthMethods);
});
test('can navigate to an auth method form', async function (assert) {
test('visiting auth methods in global scope', async function (assert) {
assert.expect(1);
await visit(urls.authMethods);
await click('main tbody .rose-table-header-cell:nth-child(1) a');
await visit(urls.globalScope);
await click(`[href="${urls.globalAuthMethods}"]`);
await a11yAudit();
assert.strictEqual(currentURL(), urls.authMethod);
assert.strictEqual(currentURL(), urls.globalAuthMethods);
});
test('cannot navigate to an auth method form without proper authorization', async function (assert) {
test('can navigate to an auth method form in org scope', async function (assert) {
assert.expect(3);
await visit(urls.orgScope);
await click(`[href="${urls.orgAuthMethods}"]`);
assert.dom(AUTH_TYPE_SELECTOR).hasText('Password');
assert.dom(AUTH_ACTIONS_SELECTOR).exists();
await click(AUTH_LINK_SELECTOR);
await a11yAudit();
assert.strictEqual(currentURL(), urls.passwordAuthMethodOrg);
});
test('can navigate to an auth method form in global scope', async function (assert) {
assert.expect(3);
await visit(urls.globalScope);
await click(`[href="${urls.globalAuthMethods}"]`);
assert.dom(AUTH_TYPE_SELECTOR).hasText('OIDC');
assert.dom(AUTH_ACTIONS_SELECTOR).exists();
await click(AUTH_LINK_SELECTOR);
await a11yAudit();
assert.strictEqual(currentURL(), urls.oidcAuthMethodGlobal);
});
test('cannot navigate to an auth method form without proper authorization in org scope', async function (assert) {
assert.expect(1);
instances.authMethod.authorized_actions =
instances.authMethod.authorized_actions.filter((item) => item !== 'read');
await visit(urls.authMethod);
assert.notOk(find('main tbody .rose-table-header-cell:nth-child(1) a'));
instances.passwordAuthMethodOrg.authorized_actions =
instances.passwordAuthMethodOrg.authorized_actions.filter(
(item) => item !== 'read'
);
await visit(urls.orgScope);
await click(`[href="${urls.orgAuthMethods}"]`);
assert.dom(AUTH_LINK_SELECTOR).doesNotExist();
});
test('cannot navigate to an auth method form without proper authorization in global scope', async function (assert) {
assert.expect(1);
instances.oidcAuthMethodGlobal.authorized_actions =
instances.oidcAuthMethodGlobal.authorized_actions.filter(
(item) => item !== 'read'
);
await visit(urls.globalScope);
await click(`[href="${urls.globalAuthMethods}"]`);
assert.dom(AUTH_LINK_SELECTOR).doesNotExist();
});
test('cannot navigate to an ldap auth method form in global scope', async function (assert) {
assert.expect(3);
instances.ldapAuthMethod = this.server.create('auth-method', {
scope: instances.scopes.global,
type: TYPE_AUTH_METHOD_LDAP,
});
await visit(urls.globalScope);
await click(`[href="${urls.globalAuthMethods}"]`);
assert.dom(LDAP_AUTH_LINK_SELECTOR).doesNotExist();
assert.dom(LDAP_AUTH_TYPE_SELECTOR).hasText('LDAP');
assert.dom(LDAP_AUTH_ACTIONS_SELECTOR).doesNotExist();
});
test('cannot navigate to an ldap auth method form in org scope', async function (assert) {
assert.expect(3);
instances.ldapAuthMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_LDAP,
});
await visit(urls.orgScope);
await click(`[href="${urls.orgAuthMethods}"]`);
assert.dom(LDAP_AUTH_LINK_SELECTOR).doesNotExist();
assert.dom(LDAP_AUTH_TYPE_SELECTOR).hasText('LDAP');
assert.dom(LDAP_AUTH_ACTIONS_SELECTOR).doesNotExist();
});
});
@@ -22,6 +22,7 @@ import {
// //currentSession,
// //invalidateSession,
} from 'ember-simple-auth/test-support';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
module('Acceptance | auth methods | update', function (hooks) {
setupApplicationTest(hooks);
@@ -73,7 +74,7 @@ module('Acceptance | auth methods | update', function (hooks) {
assert.expect(12);
instances.authMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
});
await visit(`${urls.authMethods}/${instances.authMethod.id}`);
await click('form [type="button"]:not(:disabled)', 'Activate edit mode');
@@ -25,6 +25,10 @@ import {
invalidateSession,
} from 'ember-simple-auth/test-support';
import Service from '@ember/service';
import {
TYPE_AUTH_METHOD_PASSWORD,
TYPE_AUTH_METHOD_OIDC,
} from 'api/models/auth-method';
module('Acceptance | authentication', function (hooks) {
setupApplicationTest(hooks);
@@ -76,15 +80,15 @@ module('Acceptance | authentication', function (hooks) {
scope = { id: orgScope.id, type: orgScope.type };
globalAuthMethod = this.server.create('auth-method', {
scope: globalScope,
type: 'password',
type: TYPE_AUTH_METHOD_PASSWORD,
});
authMethod = this.server.create('auth-method', {
scope: orgScope,
type: 'password',
type: TYPE_AUTH_METHOD_PASSWORD,
});
authMethodOIDC = this.server.create('auth-method', {
scope: orgScope,
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
});
orgScopeID = orgScope.id;
globalAuthMethodID = globalAuthMethod.id;
@@ -10,6 +10,7 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { authenticateSession } from 'ember-simple-auth/test-support';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { Response } from 'miragejs';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
module('Acceptance | managed-groups | create', function (hooks) {
setupApplicationTest(hooks);
@@ -40,7 +41,7 @@ module('Acceptance | managed-groups | create', function (hooks) {
});
instances.authMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
});
instances.managedGroup = this.server.create('managed-group', {
scope: instances.scopes.org,
@@ -10,6 +10,7 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { authenticateSession } from 'ember-simple-auth/test-support';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { Response } from 'miragejs';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
module('Acceptance | managed-groups | delete', function (hooks) {
setupApplicationTest(hooks);
@@ -40,7 +41,7 @@ module('Acceptance | managed-groups | delete', function (hooks) {
});
instances.authMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
});
instances.managedGroup = this.server.create('managed-group', {
scope: instances.scopes.org,
@@ -8,6 +8,7 @@ import { visit, click, find } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
module('Acceptance | managed-groups | list', function (hooks) {
setupApplicationTest(hooks);
@@ -38,7 +39,7 @@ module('Acceptance | managed-groups | list', function (hooks) {
});
instances.authMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
});
instances.managedGroup = this.server.create('managed-group', {
scope: instances.scopes.org,
@@ -14,6 +14,7 @@ import {
//currentSession,
//invalidateSession,
} from 'ember-simple-auth/test-support';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
module('Acceptance | managed-groups | members', function (hooks) {
setupApplicationTest(hooks);
@@ -47,7 +48,7 @@ module('Acceptance | managed-groups | members', function (hooks) {
'auth-method',
{
scope: instances.scopes.org,
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
},
'withAccountsAndUsersAndManagedGroups'
);
@@ -9,6 +9,7 @@ import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { authenticateSession } from 'ember-simple-auth/test-support';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method';
module('Acceptance | managed-groups | read', function (hooks) {
setupApplicationTest(hooks);
@@ -39,7 +40,7 @@ module('Acceptance | managed-groups | read', function (hooks) {
});
instances.authMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: 'oidc',
type: TYPE_AUTH_METHOD_OIDC,
});
instances.managedGroup = this.server.create('managed-group', {
scope: instances.scopes.org,
+170 -42
View File
@@ -4,7 +4,7 @@
*/
import { module, test } from 'qunit';
import { visit, currentURL, click, find, findAll } from '@ember/test-helpers';
import { visit, currentURL, click } 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';
@@ -15,11 +15,27 @@ import {
//currentSession,
//invalidateSession,
} from 'ember-simple-auth/test-support';
import { TYPE_AUTH_METHOD_LDAP } from 'api/models/auth-method';
module('Acceptance | users | accounts', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let accountsCount;
let features;
const ACCOUNTS_ACTION_SELECTOR =
'tbody .rose-table-row:nth-child(1) .rose-table-cell:last-child .rose-dropdown';
const ACCOUNTS_TYPE_SELECTOR =
'tbody .rose-table-row:nth-child(1) .rose-table-cell';
const ADD_ACCOUNTS_ACTION_SELECTOR = '.rose-layout-page-actions a';
const ERROR_MSG_SELECTOR = '[role="alert"]';
const TABLE_ROWS_SELECTOR = 'tbody tr';
const CHECKBOX_SELECTOR = 'tbody label';
const SUBMIT_BTN_SELECTOR = 'form [type="submit"]';
const CANCEL_BTN_SELECTOR = 'form [type="button"]';
const REMOVE_ACTION_SELECTOR = 'tbody tr .rose-dropdown-button-danger';
const instances = {
scopes: {
global: null,
@@ -27,14 +43,13 @@ module('Acceptance | users | accounts', function (hooks) {
},
user: null,
};
const urls = {
orgScope: null,
users: null,
user: null,
accounts: null,
addAccounts: null,
};
let accountsCount;
hooks.beforeEach(function () {
authenticateSession({});
@@ -50,28 +65,35 @@ module('Acceptance | users | accounts', function (hooks) {
},
'withAccountsAndUsersAndManagedGroups'
);
instances.user = this.server.schema.users.all().models[0];
instances.user = this.server.schema.users.first();
accountsCount = instances.user.accountIds.length;
urls.users = `/scopes/${instances.scopes.org.id}/users`;
urls.user = `${urls.users}/${instances.user.id}`;
urls.accounts = `${urls.user}/accounts`;
urls.addAccounts = `${urls.user}/add-accounts`;
features = this.owner.lookup('service:features');
});
test('visiting user accounts', async function (assert) {
assert.expect(2);
await visit(urls.accounts);
await visit(urls.user);
await click(`[href="${urls.accounts}"]`);
await a11yAudit();
assert.strictEqual(currentURL(), urls.accounts);
assert.strictEqual(findAll('tbody tr').length, accountsCount);
assert.dom(TABLE_ROWS_SELECTOR).exists({ count: accountsCount });
});
test('can remove an account', async function (assert) {
assert.expect(2);
await visit(urls.accounts);
assert.strictEqual(findAll('tbody tr').length, accountsCount);
await click('tbody tr .rose-dropdown-button-danger');
assert.strictEqual(findAll('tbody tr').length, accountsCount - 1);
await visit(urls.user);
await click(`[href="${urls.accounts}"]`);
assert.dom(TABLE_ROWS_SELECTOR).exists({ count: accountsCount });
await click(REMOVE_ACTION_SELECTOR);
assert.dom(TABLE_ROWS_SELECTOR).exists({ count: accountsCount - 1 });
});
test('cannot remove an account without proper authorization', async function (assert) {
@@ -80,8 +102,52 @@ module('Acceptance | users | accounts', function (hooks) {
(item) => item !== 'remove-accounts'
);
instances.user.update({ authorized_actions });
await visit(urls.accounts);
assert.notOk(find('tbody tr .rose-dropdown-button-danger'));
await visit(urls.user);
await click(`[href="${urls.accounts}"]`);
assert.dom(REMOVE_ACTION_SELECTOR).doesNotExist();
});
test('cannot remove an ldap account when feature flag disabled', async function (assert) {
assert.expect(2);
const authMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_LDAP,
});
const { id } = this.server.create('account', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_LDAP,
authMethod,
});
instances.user.update({ accountIds: [id] });
await visit(urls.user);
await click(`[href="${urls.accounts}"]`);
assert.dom(ACCOUNTS_ACTION_SELECTOR).doesNotExist();
assert.dom(ACCOUNTS_TYPE_SELECTOR).hasText('LDAP');
});
test('can remove an ldap account when feature flag enabled', async function (assert) {
assert.expect(2);
features.enable('ldap-auth-methods');
const authMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_LDAP,
});
const { id } = this.server.create('account', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_LDAP,
authMethod,
});
instances.user.update({ accountIds: [id] });
await visit(urls.user);
await click(`[href="${urls.accounts}"]`);
assert.dom(ACCOUNTS_ACTION_SELECTOR).exists();
assert.dom(ACCOUNTS_TYPE_SELECTOR).hasText('LDAP');
});
test('shows error message on account remove', async function (assert) {
@@ -98,24 +164,31 @@ module('Acceptance | users | accounts', function (hooks) {
}
);
});
await visit(urls.accounts);
assert.strictEqual(findAll('tbody tr').length, accountsCount);
await click('tbody tr .rose-dropdown-button-danger');
assert.ok(find('[role="alert"]'));
await visit(urls.user);
await click(`[href="${urls.accounts}"]`);
assert.dom(TABLE_ROWS_SELECTOR).exists({ count: accountsCount });
await click(REMOVE_ACTION_SELECTOR);
assert.dom(ERROR_MSG_SELECTOR).isVisible();
});
test('visiting account add accounts', async function (assert) {
assert.expect(1);
await visit(urls.addAccounts);
await visit(urls.addAccounts);
await visit(urls.accounts);
await click(ADD_ACCOUNTS_ACTION_SELECTOR);
await a11yAudit();
assert.strictEqual(currentURL(), urls.addAccounts);
});
test('can navigate to add accounts with proper authorization', async function (assert) {
assert.expect(1);
await visit(urls.accounts);
assert.ok(find(`[href="${urls.addAccounts}"]`));
await visit(urls.user);
await click(`[href="${urls.accounts}"]`);
assert.dom(`[href="${urls.addAccounts}"]`).exists();
});
test('cannot navigate to add accounts without proper authorization', async function (assert) {
@@ -124,38 +197,93 @@ module('Acceptance | users | accounts', function (hooks) {
(item) => item !== 'add-accounts'
);
instances.user.update({ authorized_actions });
await visit(urls.accounts);
assert.notOk(find(`[href="${urls.addAccounts}"]`));
await visit(urls.user);
await click(`[href="${urls.accounts}"]`);
assert.dom(ADD_ACCOUNTS_ACTION_SELECTOR).doesNotExist();
});
test('cannot add ldap accounts to user when feature flag is disabled', async function (assert) {
assert.expect(1);
const authMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_LDAP,
});
this.server.create('account', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_LDAP,
authMethod,
});
const accountsAvailableCount =
this.server.schema.accounts.all().length -
instances.user.accountIds.length;
await visit(urls.user);
await click(`[href="${urls.accounts}"]`);
await click(ADD_ACCOUNTS_ACTION_SELECTOR);
assert
.dom(TABLE_ROWS_SELECTOR)
.exists({ count: accountsAvailableCount - 1 });
});
test('can add ldap accounts to user when feature flag is enabled', async function (assert) {
assert.expect(1);
features.enable('ldap-auth-methods');
const authMethod = this.server.create('auth-method', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_LDAP,
});
this.server.create('account', {
scope: instances.scopes.org,
type: TYPE_AUTH_METHOD_LDAP,
authMethod,
});
const accountsAvailableCount =
this.server.schema.accounts.all().length -
instances.user.accountIds.length;
await visit(urls.user);
await click(`[href="${urls.accounts}"]`);
await click(ADD_ACCOUNTS_ACTION_SELECTOR);
assert.dom(TABLE_ROWS_SELECTOR).exists({ count: accountsAvailableCount });
});
test('select and save accounts to add', async function (assert) {
assert.expect(3);
assert.expect(4);
instances.user.update({ accountIds: [] });
await visit(urls.accounts);
assert.strictEqual(findAll('tbody tr').length, 0);
await click('.rose-layout-page-actions a');
await visit(urls.user);
await click(`[href="${urls.accounts}"]`);
assert.dom(TABLE_ROWS_SELECTOR).exists({ count: 0 });
await click(ADD_ACCOUNTS_ACTION_SELECTOR);
assert.strictEqual(currentURL(), urls.addAccounts);
// Click three times to select, unselect, then reselect (for coverage)
await click('tbody label');
await click('tbody label');
await click('tbody label');
await click('form [type="submit"]');
await click(CHECKBOX_SELECTOR);
await click(CHECKBOX_SELECTOR);
await click(CHECKBOX_SELECTOR);
await click(SUBMIT_BTN_SELECTOR);
await visit(urls.accounts);
assert.strictEqual(findAll('tbody tr').length, 1);
assert.dom(TABLE_ROWS_SELECTOR).exists({ count: 1 });
assert.strictEqual(instances.user.accountIds.length, 1);
});
test('select and cancel accounts to add', async function (assert) {
assert.expect(4);
await visit(urls.accounts);
assert.strictEqual(findAll('tbody tr').length, accountsCount);
await click('tbody tr .rose-dropdown-button-danger');
assert.strictEqual(findAll('tbody tr').length, accountsCount - 1);
await click('.rose-layout-page-actions a');
await visit(urls.user);
await click(`[href="${urls.accounts}"]`);
assert.dom(TABLE_ROWS_SELECTOR).exists({ count: accountsCount });
await click(REMOVE_ACTION_SELECTOR);
assert.dom(TABLE_ROWS_SELECTOR).exists({ count: accountsCount - 1 });
await click(ADD_ACCOUNTS_ACTION_SELECTOR);
assert.strictEqual(currentURL(), urls.addAccounts);
await click('tbody label');
await click('form [type="button"]');
await click(CHECKBOX_SELECTOR);
await click(CANCEL_BTN_SELECTOR);
await visit(urls.accounts);
assert.strictEqual(findAll('tbody tr').length, accountsCount - 1);
assert.dom(TABLE_ROWS_SELECTOR).exists({ count: accountsCount - 1 });
});
test('shows error message on account add', async function (assert) {
@@ -174,8 +302,8 @@ module('Acceptance | users | accounts', function (hooks) {
});
instances.user.update({ accountIds: [] });
await visit(urls.addAccounts);
await click('tbody label');
await click('form [type="submit"]');
assert.ok(find('[role="alert"]'));
await click(CHECKBOX_SELECTOR);
await click(SUBMIT_BTN_SELECTOR);
assert.dom(ERROR_MSG_SELECTOR).isVisible();
});
});
+121 -14
View File
@@ -1,19 +1,59 @@
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
# Admin UI e2e tests
- [Admin UI e2e tests](#admin-ui-e2e-tests)
- [Prerequisites:](#prerequisites)
- [Accesses](#accesses)
- [Software](#software)
- [Getting Started](#getting-started)
- [Set up Boundary CLI](#set-up-boundary-cli)
- [Setup Boundary UI](#setup-boundary-ui)
- [Setup AWS:](#setup-aws)
- [Setup HCP Terraform (Terraform cloud):](#setup-hcp-terraform-terraform-cloud)
- [Setup Enos:](#setup-enos)
- [Run tests:](#run-tests)
- [Launch Enos Scenario](#launch-enos-scenario)
- [Run tests](#run-tests)
- [Destroy Enos Scenario](#destroy-enos-scenario)
- [Developing Tests](#developing-tests)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
# Admin UI e2e tests
This test suite tests the Boundary Admin UI in an end-to-end setting. It was designed to be run in a
variety of environments as long as the appropriate environment variables are set. You can choose to
spin up your own infrastructure or use [Enos](https://github.com/hashicorp/boundary/tree/main/enos)
to generate it for you.
to generate it for you. More information about [Enos available here.](https://github.com/hashicorp/Enos-Docs)
The test suite uses the [Playwright](https://playwright.dev/) framework.
## Prerequisites:
You will need [Homebrew](https://brew.sh/) install. For secure persisting your SSH keys and tokens we recommend using 1Password.
### Accesses
If you are missing any acccess, requested [through the IT service catalog](https://hashicorp.freshservice.com/support/catalog/items).
- Doormat account: `boundary_team_acctest_dev`.
- HCP account: you will need to use Terraform cloud to spin up enos.
### Software
- Vault: [documentation how to install](https://developer.hashicorp.com/vault/tutorials/getting-started/getting-started-install).
- Terraform: [documentation how to install](https://developer.hashicorp.com/terraform/downloads).
- Github CLI: [documentation how to install](https://cli.github.com/manual/installation).
- Install Enos: [documentation how to install using homebrew](https://github.com/hashicorp/Enos-Docs/blob/main/installation.md), also binaries available [here](https://github.com/hashicorp/enos/releases).
- Install Doormat CLI: [documentation how to install](https://docs.prod.secops.hashicorp.services/doormat/cli/).
- Install AWS CLI (Optional): [documentation how to install](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html).
## Getting Started
Setup...
```bash
cd boundary-ui/ui/admin
yarn install
npx playwright install # this installs the browsers used by Playwright
```
We will cover everything you need to set up before being able to run Enos + the tests.
### Set up Boundary CLI
Ensure the `boundary` CLI is available on the path. You can validate this by making sure this
command returns something.
@@ -21,16 +61,73 @@ command returns something.
which boundary
```
Run tests...
Otherwise, follow instructions to [install Boundary](https://developer.hashicorp.com/boundary/downloads?product_intent=boundary) or install it locally running `$ make install` within [Boundary repository](https://github.com/hashicorp/boundary/tree/main).
### Setup Boundary UI
Ensure you install project dependencies and playwright needs.
```bash
export BOUNDARY_ADDR=
export E2E_PASSWORD_ADMIN_LOGIN_NAME=
export E2E_PASSWORD_ADMIN_PASSWORD=
export E2E_PASSWORD_AUTH_METHOD_ID=
... # Tests may have additional variables that it needs
cd boundary-ui/ui/admin
yarn run e2e
yarn install
npx playwright install # this installs the browsers used by Playwright
```
### Setup AWS:
**Region awareness:** take note of the AWS region you are setting up because we will need it later to configure enos. [More information about AWS regions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html).
**SSH Key pair:**
You need to provide an SSH Key pair for the EC2 instance. We recommend creating specific e2e SSH keys and not re-use existing ones. To differentiate them, we recommend adding `enos` to the name, i.e: `name_enos`.
[How to create EC2 SSH key pair](https://docs.aws.amazon.com/ground-station/latest/ug/create-ec2-ssh-key-pair.html).
### Setup HCP Terraform (Terraform cloud):
Log in to [HCP Production env](https://portal.cloud.hashicorp.com/). Through HCP Terraform, open Terraform cloud, or [access here](https://app.terraform.io/). Then create an API token [following this documentation](https://developer.hashicorp.com/terraform/cloud-docs/users-teams-organizations/users#tokens).
**Token awareness:** Copy your token from the box and save it in a secure location. Terraform Cloud only displays the token once, right after you create it. And we will need the token later for enos configuration.
### Setup Enos:
Enos needs some configuration variables to run the scenario successfully. [See the configuration file](https://github.com/hashicorp/boundary/blob/main/enos/enos.vars.hcl). The file has comments per each variable, but some awareness:
- `aws_region`: The AWS region you are using. Very important as mentioned within the EC2 Setup.
- `aws_ssh_keypair_name`: The name of the AWS keypair.
- `aws_ssh_private_key_path`: The path to the private key associated with your keypair.
- `enos_user`: The user name to use for Boundary.
- `tfc_api_token`: you need to provide the previously created token in Terraform cloud, there is no shared token within Boundary team.
- `local_boundary_dir`: The directory that contains Boundary binary.
- `local_boundary_ui_dir`: The directory that contains the copy of boundary-ui you want to use for UI tests.
- `e2e_debug_no_run`: make sure this is set to true.
More documentation about [scenario variables](https://github.com/hashicorp/boundary/tree/main/enos#scenarios-variables).
## Run tests:
Before running the e2e test locally, we need to launch Enos Scenario, make sure you followed all the steps within the [Getting started section](#getting-started).
It is not necessary, but from this point we recommend having 2 terminals open. 
- Terminal 1: Will be use to run enos (Boundary).
- Terminal 2: Will be use to run e2e UI tests (Boundary UI).
### Launch Enos Scenario
Using Terminal 1:
- `$ cd boundary/enos`.
- `$ doormat login`. Login with Doormat.
- `$ eval "$(doormat aws export --account boundary_team_acctest_dev)"`. Exporting AWS env variables from doormat to your terminal.
- `$ enos scenario launch e2e_ui builder:local`. Launch enos scenario, this will take from 5 to 10 minutes. When its done, you will see a Enos Operations finished! within your terminal.
- `$ bash scripts/test_e2e_env.sh`. Prints all the env variables within Enos scenario. Copy the output and paste it within your Terminal 2 (Boundary UI). These env variables are need within Boundary UI to run the test against the enos scenario.
*Be aware once the scenario is launch you will create and run resources within AWS, once you are done using the scenario, [you should destroy it](#destroy-enos-scenario).*
### Run tests
Using Terminal 2:
- Set the env varibales `test_e2e_env.sh` script output within this terminal.
- `$ cd boundary-ui/ui/admin`.
- `$ yarn run e2e`.
Here are some additional commands to assist with debugging.
```bash
@@ -39,6 +136,16 @@ PWDEBUG=console yarn playwright test --headed --config ./tests/e2e/playwright.co
PWDEBUG=console yarn playwright test --headed --config ./tests/e2e/playwright.config.js login.spec.js --debug
```
[Playwright documentation about running tests](https://playwright.dev/docs/running-tests).
### Destroy Enos Scenario
Using Terminal 1:
- `$ enos scenario destroy`.
- After all the steps pass, you should see a `Enos operations finished!`.
## Developing Tests
It is recommended to use locators that resemble how users interact with the application. `getByRole`
@@ -0,0 +1,98 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
TYPE_AUTH_METHOD_LDAP,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_PASSWORD,
} from 'api/models/auth-method';
module('Unit | Abilities | auth-method', function (hooks) {
setupTest(hooks);
let canService;
let store;
let features;
hooks.beforeEach(function () {
canService = this.owner.lookup('service:can');
store = this.owner.lookup('service:store');
features = this.owner.lookup('service:features');
});
test('can read LDAP auth-method when authorized and feature flag enabled', function (assert) {
assert.expect(2);
features.enable('ldap-auth-methods');
const authMethod = store.createRecord('auth-method', {
authorized_actions: ['read'],
type: TYPE_AUTH_METHOD_LDAP,
});
assert.true(canService.can('read auth-method', authMethod));
authMethod.authorized_actions = [];
assert.false(canService.can('read auth-method', authMethod));
});
test('cannot read LDAP auth-method when authorized and feature flag disabled', function (assert) {
assert.expect(2);
const authMethod = store.createRecord('auth-method', {
authorized_actions: ['read'],
type: TYPE_AUTH_METHOD_LDAP,
});
assert.false(canService.can('read auth-method', authMethod));
authMethod.authorized_actions = [];
assert.false(canService.can('read auth-method', authMethod));
});
test('can read non-LDAP auth-method when authorized', function (assert) {
assert.expect(2);
const authMethod = store.createRecord('auth-method', {
authorized_actions: ['read'],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.true(canService.can('read auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(canService.can('read auth-method', authMethod));
});
test('cannot read non-LDAP auth-method when unauthorized', function (assert) {
assert.expect(2);
const authMethod = store.createRecord('auth-method', {
authorized_actions: [],
type: TYPE_AUTH_METHOD_OIDC,
});
assert.false(canService.can('read auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_PASSWORD;
assert.false(canService.can('read auth-method', authMethod));
});
test('cannot make LDAP auth-method primary when feature flag disabled', function (assert) {
assert.expect(1);
const authMethod = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_LDAP,
});
assert.false(canService.can('makePrimary auth-method', authMethod));
});
test('can make LDAP auth-method primary when feature flag enabled', function (assert) {
assert.expect(1);
features.enable('ldap-auth-methods');
const authMethod = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_LDAP,
});
assert.true(canService.can('makePrimary auth-method', authMethod));
});
test('can make non-LDAP auth-method primary', function (assert) {
assert.expect(2);
const authMethod = store.createRecord('auth-method', {
type: TYPE_AUTH_METHOD_OIDC,
});
assert.true(canService.can('makePrimary auth-method', authMethod));
authMethod.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(canService.can('makePrimary auth-method', authMethod));
});
});
+109
View File
@@ -0,0 +1,109 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import {
TYPE_AUTH_METHOD_LDAP,
TYPE_AUTH_METHOD_OIDC,
TYPE_AUTH_METHOD_PASSWORD,
} from 'api/models/auth-method';
module('Unit | Abilities | user', function (hooks) {
setupTest(hooks);
let canService;
let store;
let features;
const instances = {
account: null,
user: null,
};
hooks.beforeEach(function () {
canService = this.owner.lookup('service:can');
store = this.owner.lookup('service:store');
features = this.owner.lookup('service:features');
instances.user = store.createRecord('user', {
authorized_actions: ['add-accounts', 'remove-accounts'],
});
instances.account = store.createRecord('account');
});
test('can add ldap account to user when feature flag enabled', function (assert) {
assert.expect(1);
features.enable('ldap-auth-methods');
instances.account.type = TYPE_AUTH_METHOD_LDAP;
assert.true(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
});
test('can add non-ldap account to user', function (assert) {
assert.expect(2);
instances.account.type = TYPE_AUTH_METHOD_OIDC;
assert.true(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
});
test('cannot add ldap account to user when feature flag disabled', function (assert) {
assert.expect(1);
instances.account.type = TYPE_AUTH_METHOD_LDAP;
assert.false(
canService.can('addAccount user', instances.user, {
account: instances.account,
})
);
});
test('can remove ldap account from user when feature flag enabled', function (assert) {
assert.expect(1);
features.enable('ldap-auth-methods');
instances.account.type = TYPE_AUTH_METHOD_LDAP;
assert.true(
canService.can('removeAccount user', instances.user, {
account: instances.account,
})
);
});
test('cannot remove ldap account from user when feature flag disabled', function (assert) {
assert.expect(1);
instances.account.type = TYPE_AUTH_METHOD_LDAP;
assert.false(
canService.can('removeAccount user', instances.user, {
account: instances.account,
})
);
});
test('can remove non-ldap account from user', function (assert) {
assert.expect(2);
instances.account.type = TYPE_AUTH_METHOD_OIDC;
assert.true(
canService.can('removeAccount user', instances.user, {
account: instances.account,
})
);
instances.account.type = TYPE_AUTH_METHOD_PASSWORD;
assert.true(
canService.can('removeAccount user', instances.user, {
account: instances.account,
})
);
});
});
@@ -70,6 +70,7 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
const requestCookies = false;
switch (authMethod.type) {
case 'password':
case 'ldap':
await this.session.authenticate(
authenticatorName,
creds,
@@ -121,10 +121,10 @@
{{else}}
{{model.displayName}}
{{/if}}
{{#if model.target.description}}
<p>{{model.target.description}}</p>
{{#if model.description}}
<p>{{model.description}}</p>
{{/if}}
{{#if model.target.isActive}}
{{#if model.isActive}}
<Hds::Badge
@text={{t 'states.active'}}
@color='success'
+24 -41
View File
@@ -9360,13 +9360,6 @@ boolean@^3.1.4:
resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b"
integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
boom@7.x.x:
version "7.3.0"
resolved "https://registry.yarnpkg.com/boom/-/boom-7.3.0.tgz#733a6d956d33b0b1999da3fe6c12996950d017b9"
integrity sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A==
dependencies:
hoek "6.x.x"
bower-config@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/bower-config/-/bower-config-1.4.3.tgz#3454fecdc5f08e7aa9cc6d556e492be0669689ae"
@@ -11416,13 +11409,6 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5:
shebang-command "^1.2.0"
which "^1.2.9"
cryptiles@~4.1.2:
version "4.1.3"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-4.1.3.tgz#2461d3390ea0b82c643a6ba79f0ed491b0934c25"
integrity sha512-gT9nyTMSUC1JnziQpPbxKGBbUg8VL7Zn2NB4E1cJYvuXdElHrwxrV9bmltZGDzet45zSDGyYceueke1TjynGzw==
dependencies:
boom "7.x.x"
crypto-browserify@^3.11.0:
version "3.12.0"
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
@@ -13954,10 +13940,10 @@ engine.io-parser@~5.0.3:
resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0"
integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
engine.io@~6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.1.tgz#e3f7826ebc4140db9bbaa9021ad6b1efb175878f"
integrity sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==
engine.io@~6.4.1:
version "6.4.2"
resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.4.2.tgz#ffeaf68f69b1364b0286badddf15ff633476473f"
integrity sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg==
dependencies:
"@types/cookie" "^0.4.1"
"@types/cors" "^2.8.12"
@@ -13968,7 +13954,7 @@ engine.io@~6.2.1:
cors "~2.8.5"
debug "~4.3.1"
engine.io-parser "~5.0.3"
ws "~8.2.3"
ws "~8.11.0"
enhanced-resolve@^4.0.0, enhanced-resolve@^4.5.0:
version "4.5.0"
@@ -16446,7 +16432,7 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoek@6.x.x, hoek@~4.2.0:
hoek@~4.2.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb"
integrity sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==
@@ -20307,11 +20293,6 @@ npm-run-path@^5.1.0:
dependencies:
path-key "^4.0.0"
npm-user-validate@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/npm-user-validate/-/npm-user-validate-1.0.1.tgz#31428fc5475fe8416023f178c0ab47935ad8c561"
integrity sha512-uQwcd/tY+h1jnEaze6cdX/LrhWhoBxfSknxentoqmIuStxUExxjWd3ULMLFPiFUrZKbOVMowH6Jq2FRWfmhcEw==
npmlog@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
@@ -23468,29 +23449,31 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
socket.io-adapter@~2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz#b50a4a9ecdd00c34d4c8c808224daa1a786152a6"
integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==
socket.io-adapter@~2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12"
integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==
dependencies:
ws "~8.11.0"
socket.io-parser@~4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5"
integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==
version "4.2.3"
resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.3.tgz#926bcc6658e2ae0883dc9dee69acbdc76e4e3667"
integrity sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.1"
socket.io@^4.1.2:
version "4.5.4"
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.5.4.tgz#a4513f06e87451c17013b8d13fdfaf8da5a86a90"
integrity sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==
version "4.6.1"
resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.6.1.tgz#62ec117e5fce0692fa50498da9347cfb52c3bc70"
integrity sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA==
dependencies:
accepts "~1.3.4"
base64id "~2.0.0"
debug "~4.3.2"
engine.io "~6.2.1"
socket.io-adapter "~2.4.0"
engine.io "~6.4.1"
socket.io-adapter "~2.5.2"
socket.io-parser "~4.2.1"
socks-proxy-agent@^6.0.0:
@@ -26150,7 +26133,7 @@ write-file-atomic@^4.0.2:
imurmurhash "^0.1.4"
signal-exit "^3.0.7"
ws@^6.1.0, ws@^7.4.6, ws@~8.2.3:
ws@^6.1.0, ws@^7.4.6, ws@~8.11.0:
version "7.5.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.3.tgz#160835b63c7d97bfab418fc1b8a9fced2ac01a74"
integrity sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==
@@ -26226,9 +26209,9 @@ yaml@^1.10.0, yaml@^1.7.2:
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
yaml@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.1.tgz#1e06fb4ca46e60d9da07e4f786ea370ed3c3cfec"
integrity sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==
version "2.2.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.2.tgz#ec551ef37326e6d42872dad1970300f8eb83a073"
integrity sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==
yargs-parser@^20.0.0, yargs-parser@^20.2.2, yargs-parser@^20.2.3, yargs-parser@^21.0.0, yargs-parser@~20.2.7:
version "20.2.9"