diff --git a/addons/api/addon/abilities/account.js b/addons/api/addon/abilities/account.js index 8d741f157..03ab318a5 100644 --- a/addons/api/addon/abilities/account.js +++ b/addons/api/addon/abilities/account.js @@ -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} */ diff --git a/addons/api/addon/abilities/auth-method.js b/addons/api/addon/abilities/auth-method.js new file mode 100644 index 000000000..421fca156 --- /dev/null +++ b/addons/api/addon/abilities/auth-method.js @@ -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'); + } +} diff --git a/addons/api/addon/abilities/user.js b/addons/api/addon/abilities/user.js index bda0003cf..2e5d7a740 100644 --- a/addons/api/addon/abilities/user.js +++ b/addons/api/addon/abilities/user.js @@ -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; + } } diff --git a/addons/api/addon/models/account.js b/addons/api/addon/models/account.js index 87aacf331..4607433e7 100644 --- a/addons/api/addon/models/account.js +++ b/addons/api/addon/models/account.js @@ -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; diff --git a/addons/api/addon/models/auth-method.js b/addons/api/addon/models/auth-method.js index dc519cf47..8599160f1 100644 --- a/addons/api/addon/models/auth-method.js +++ b/addons/api/addon/models/auth-method.js @@ -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); } /** diff --git a/addons/api/addon/models/role.js b/addons/api/addon/models/role.js index dd55a78eb..83caa27d9 100644 --- a/addons/api/addon/models/role.js +++ b/addons/api/addon/models/role.js @@ -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 } ); diff --git a/addons/api/addon/serializers/auth-method.js b/addons/api/addon/serializers/auth-method.js index d21f54cff..01488e4e6 100644 --- a/addons/api/addon/serializers/auth-method.js +++ b/addons/api/addon/serializers/auth-method.js @@ -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); diff --git a/addons/api/app/abilities/auth-method.js b/addons/api/app/abilities/auth-method.js new file mode 100644 index 000000000..f81e174f4 --- /dev/null +++ b/addons/api/app/abilities/auth-method.js @@ -0,0 +1,6 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +export { default } from 'api/abilities/auth-method'; diff --git a/addons/api/mirage/factories/account.js b/addons/api/mirage/factories/account.js index ee46ac64e..176e0786d 100644 --- a/addons/api/mirage/factories/account.js +++ b/addons/api/mirage/factories/account.js @@ -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', diff --git a/addons/api/mirage/factories/auth-method.js b/addons/api/mirage/factories/auth-method.js index 96589133f..dee5481a3 100644 --- a/addons/api/mirage/factories/auth-method.js +++ b/addons/api/mirage/factories/auth-method.js @@ -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) => { diff --git a/addons/api/mirage/generated/factories/account.js b/addons/api/mirage/generated/factories/account.js index 96eb86e80..55bd0df37 100644 --- a/addons/api/mirage/generated/factories/account.js +++ b/addons/api/mirage/generated/factories/account.js @@ -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(), diff --git a/addons/api/mirage/generated/factories/auth-method.js b/addons/api/mirage/generated/factories/auth-method.js index bc6b5c79b..d0cdd71c3 100644 --- a/addons/api/mirage/generated/factories/auth-method.js +++ b/addons/api/mirage/generated/factories/auth-method.js @@ -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: diff --git a/addons/api/tests/unit/abilities/account-test-.js b/addons/api/tests/unit/abilities/account-test-.js deleted file mode 100644 index a631d7518..000000000 --- a/addons/api/tests/unit/abilities/account-test-.js +++ /dev/null @@ -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)); - }); -}); diff --git a/addons/api/tests/unit/abilities/account-test.js b/addons/api/tests/unit/abilities/account-test.js new file mode 100644 index 000000000..8c7824ff8 --- /dev/null +++ b/addons/api/tests/unit/abilities/account-test.js @@ -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)); + }); +}); diff --git a/addons/api/tests/unit/abilities/auth-method-test.js b/addons/api/tests/unit/abilities/auth-method-test.js new file mode 100644 index 000000000..ade783e69 --- /dev/null +++ b/addons/api/tests/unit/abilities/auth-method-test.js @@ -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)); + }); +}); diff --git a/addons/api/tests/unit/abilities/user-test.js b/addons/api/tests/unit/abilities/user-test.js index 06c551fdf..e15f94994 100644 --- a/addons/api/tests/unit/abilities/user-test.js +++ b/addons/api/tests/unit/abilities/user-test.js @@ -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, + }) + ); }); }); diff --git a/addons/api/tests/unit/models/account-test.js b/addons/api/tests/unit/models/account-test.js index 9dfc427a4..946b7362a 100644 --- a/addons/api/tests/unit/models/account-test.js +++ b/addons/api/tests/unit/models/account-test.js @@ -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); + }); }); diff --git a/addons/api/tests/unit/models/auth-method-test.js b/addons/api/tests/unit/models/auth-method-test.js index 1e084b9ae..3ff01039d 100644 --- a/addons/api/tests/unit/models/auth-method-test.js +++ b/addons/api/tests/unit/models/auth-method-test.js @@ -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); + }); }); diff --git a/addons/api/tests/unit/serializers/account-test.js b/addons/api/tests/unit/serializers/account-test.js index 2a9003649..326298a26 100644 --- a/addons/api/tests/unit/serializers/account-test.js +++ b/addons/api/tests/unit/serializers/account-test.js @@ -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', diff --git a/addons/api/tests/unit/serializers/auth-method-test.js b/addons/api/tests/unit/serializers/auth-method-test.js index 4ecf4d08c..f9989a5f3 100644 --- a/addons/api/tests/unit/serializers/auth-method-test.js +++ b/addons/api/tests/unit/serializers/auth-method-test.js @@ -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', })); diff --git a/addons/core/addon/authenticators/ldap.js b/addons/core/addon/authenticators/ldap.js new file mode 100644 index 000000000..9fc6bc734 --- /dev/null +++ b/addons/core/addon/authenticators/ldap.js @@ -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 {} diff --git a/addons/core/addon/components/form/authenticate/details/index.hbs b/addons/core/addon/components/form/authenticate/details/index.hbs new file mode 100644 index 000000000..177e9a2ee --- /dev/null +++ b/addons/core/addon/components/form/authenticate/details/index.hbs @@ -0,0 +1,41 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + + + {{t 'form.login_name.label'}} + + + + {{t 'form.password.label'}} + + + + + \ No newline at end of file diff --git a/addons/core/addon/components/form/authenticate/password/index.js b/addons/core/addon/components/form/authenticate/details/index.js similarity index 90% rename from addons/core/addon/components/form/authenticate/password/index.js rename to addons/core/addon/components/form/authenticate/details/index.js index 55c931da6..7b1306c64 100644 --- a/addons/core/addon/components/form/authenticate/password/index.js +++ b/addons/core/addon/components/form/authenticate/details/index.js @@ -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 /** diff --git a/addons/core/addon/components/form/authenticate/index.hbs b/addons/core/addon/components/form/authenticate/index.hbs index 49ee937a1..5a454b6c8 100644 --- a/addons/core/addon/components/form/authenticate/index.hbs +++ b/addons/core/addon/components/form/authenticate/index.hbs @@ -9,7 +9,6 @@ {{component (concat 'form/authenticate/' @model.type) submit=@submit - cancel=false disabled=@disabled }} {{/if}} \ No newline at end of file diff --git a/addons/core/addon/components/form/authenticate/ldap/index.hbs b/addons/core/addon/components/form/authenticate/ldap/index.hbs new file mode 100644 index 000000000..9cb8405e7 --- /dev/null +++ b/addons/core/addon/components/form/authenticate/ldap/index.hbs @@ -0,0 +1,6 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + + \ No newline at end of file diff --git a/addons/core/addon/components/form/authenticate/oidc/index.hbs b/addons/core/addon/components/form/authenticate/oidc/index.hbs index f83a4b2ba..61ca08cfe 100644 --- a/addons/core/addon/components/form/authenticate/oidc/index.hbs +++ b/addons/core/addon/components/form/authenticate/oidc/index.hbs @@ -6,7 +6,6 @@ diff --git a/addons/core/addon/components/form/authenticate/password/index.hbs b/addons/core/addon/components/form/authenticate/password/index.hbs index 6ca551628..9cb8405e7 100644 --- a/addons/core/addon/components/form/authenticate/password/index.hbs +++ b/addons/core/addon/components/form/authenticate/password/index.hbs @@ -3,35 +3,4 @@ SPDX-License-Identifier: MPL-2.0 }} - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/addons/core/app/authenticators/ldap.js b/addons/core/app/authenticators/ldap.js new file mode 100644 index 000000000..7bd0705ab --- /dev/null +++ b/addons/core/app/authenticators/ldap.js @@ -0,0 +1,6 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +export { default } from 'core/authenticators/ldap'; diff --git a/addons/core/app/components/form/authenticate/details.js b/addons/core/app/components/form/authenticate/details.js new file mode 100644 index 000000000..c5bd5a8d4 --- /dev/null +++ b/addons/core/app/components/form/authenticate/details.js @@ -0,0 +1,6 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +export { default } from 'core/components/form/authenticate/details'; diff --git a/addons/core/app/components/form/authenticate/ldap.js b/addons/core/app/components/form/authenticate/ldap.js new file mode 100644 index 000000000..26787de39 --- /dev/null +++ b/addons/core/app/components/form/authenticate/ldap.js @@ -0,0 +1,6 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +export { default } from 'core/components/form/authenticate/ldap'; diff --git a/addons/core/tests/integration/components/form/authenticate-test.js b/addons/core/tests/integration/components/form/authenticate-test.js new file mode 100644 index 000000000..9ae56b9c6 --- /dev/null +++ b/addons/core/tests/integration/components/form/authenticate-test.js @@ -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``); + 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``); + 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``); + assert.ok(find('.full-width')); + assert.notOk(find('.hds-form-text-input')); + assert.strictEqual( + find('button[type="submit"]').textContent.trim(), + 'Sign In' + ); + }); +}); diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index e125579c1..03845d899 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -47,6 +47,7 @@ auth-method: types: password: Password oidc: OIDC + ldap: LDAP actions: create: Create Auth Method delete: Delete Auth Method diff --git a/addons/rose/addon/styles/hds/overrides.scss b/addons/rose/addon/styles/hds/overrides.scss index 354862697..7f862403b 100644 --- a/addons/rose/addon/styles/hds/overrides.scss +++ b/addons/rose/addon/styles/hds/overrides.scss @@ -24,6 +24,10 @@ [class^='hds-form-group'] { margin-bottom: 0; } + + .hds-table__td p { + color: var(--token-form-indicator-optional-color); + } } .hds-dropdown { diff --git a/package.json b/package.json index b6a3145ff..33c0d9a83 100755 --- a/package.json +++ b/package.json @@ -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": { diff --git a/ui/admin/app/abilities/auth-method.js b/ui/admin/app/abilities/auth-method.js new file mode 100644 index 000000000..61ce714ea --- /dev/null +++ b/ui/admin/app/abilities/auth-method.js @@ -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; + } +} diff --git a/ui/admin/app/abilities/user.js b/ui/admin/app/abilities/user.js new file mode 100644 index 000000000..80e46d66d --- /dev/null +++ b/ui/admin/app/abilities/user.js @@ -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; + } +} diff --git a/ui/admin/app/components/auth-methods/auth-method/accounts/account/actions/index.hbs b/ui/admin/app/components/auth-methods/auth-method/accounts/account/actions/index.hbs index 00e985e8b..e98e75aa7 100644 --- a/ui/admin/app/components/auth-methods/auth-method/accounts/account/actions/index.hbs +++ b/ui/admin/app/components/auth-methods/auth-method/accounts/account/actions/index.hbs @@ -3,7 +3,7 @@ SPDX-License-Identifier: MPL-2.0 }} -{{#if (can 'delete model' @model)}} +{{#if (can 'delete account' @model)}} - + + {{account.id}} + {{account.description}} + {{account.accountName}} diff --git a/ui/admin/app/components/form/user/add-accounts/index.js b/ui/admin/app/components/form/user/add-accounts/index.js index 66797a6a8..a111f71d2 100644 --- a/ui/admin/app/components/form/user/add-accounts/index.js +++ b/ui/admin/app/components/form/user/add-accounts/index.js @@ -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; } diff --git a/ui/admin/app/routes/scopes/scope/auth-methods.js b/ui/admin/app/routes/scopes/scope/auth-methods.js index e4c2b9312..b5522e3db 100644 --- a/ui/admin/app/routes/scopes/scope/auth-methods.js +++ b/ui/admin/app/routes/scopes/scope/auth-methods.js @@ -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 diff --git a/ui/admin/app/routes/scopes/scope/authenticate/method.js b/ui/admin/app/routes/scopes/scope/authenticate/method.js index 84732cc40..f7cc6b680 100644 --- a/ui/admin/app/routes/scopes/scope/authenticate/method.js +++ b/ui/admin/app/routes/scopes/scope/authenticate/method.js @@ -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, diff --git a/ui/admin/app/routes/scopes/scope/roles/role/add-principals.js b/ui/admin/app/routes/scopes/scope/roles/role/add-principals.js index 8d2cefeb8..e3b96bae0 100644 --- a/ui/admin/app/routes/scopes/scope/roles/role/add-principals.js +++ b/ui/admin/app/routes/scopes/scope/roles/role/add-principals.js @@ -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 diff --git a/ui/admin/app/routes/scopes/scope/users/user/add-accounts.js b/ui/admin/app/routes/scopes/scope/users/user/add-accounts.js index c36ddae17..d16152ae6 100644 --- a/ui/admin/app/routes/scopes/scope/users/user/add-accounts.js +++ b/ui/admin/app/routes/scopes/scope/users/user/add-accounts.js @@ -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'); diff --git a/ui/admin/app/styles/app.scss b/ui/admin/app/styles/app.scss index 266e305a0..dc35c0260 100644 --- a/ui/admin/app/styles/app.scss +++ b/ui/admin/app/styles/app.scss @@ -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; diff --git a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs index 14841ac19..022c16030 100644 --- a/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs +++ b/ui/admin/app/templates/scopes/scope/auth-methods/auth-method/accounts/index.hbs @@ -41,7 +41,7 @@ {{#each @model.accounts as |account|}} - {{#if (can 'read model' account)}} + {{#if (can 'read account' account)}} - {{#if (can 'read model' account)}} + {{#if (can 'read account' account)}} + {{#if @model}} + {{#unless this.hasPrimaryAuthMethod}} + + + {{t 'resources.auth-method.messages.no-primary-auth-method.title'}} + + + {{t + 'resources.auth-method.messages.no-primary-auth-method.description' + }} + + + {{/unless}} + {{/if}} {{#if (or @model @@ -92,96 +111,80 @@ {{/if}} {{#if @model}} - {{#unless this.hasPrimaryAuthMethod}} - - {{t - 'resources.auth-method.messages.no-primary-auth-method.description' - }} - - {{/unless}} - - - - - {{t 'form.name.label'}} - {{t 'form.type.label'}} - {{t 'form.id.label'}} - - {{t 'titles.actions'}} - - - - - {{#each @model as |authMethod|}} - - - {{#if (can 'read model' authMethod)}} - - {{authMethod.displayName}} - - {{else}} - {{authMethod.displayName}} - {{/if}} -

{{authMethod.description}}

-
- - - - - + <:body as |B|> + + + {{#if (can 'read auth-method' B.data)}} + - {{authMethod.id}} - - - - {{#if authMethod.isPrimary}} - - {{/if}} - - - - {{#unless authMethod.isPrimary}} - - {{t 'resources.auth-method.actions.make-primary'}} - + {{B.data.displayName}} +
+ {{else}} + {{B.data.displayName}} + {{/if}} +

{{B.data.description}}

+ + + + + + + {{B.data.id}} + + + + {{#if B.data.isPrimary}} + + {{/if}} + + + {{#if (can 'makePrimary auth-method' B.data)}} + + + {{#unless B.data.isPrimary}} + {{/unless}} - {{#if authMethod.isPrimary}} - - {{t 'resources.auth-method.actions.remove-as-primary'}} - + {{#if B.data.isPrimary}} + {{/if}} -
- - - {{/each}} - - + + {{/if}} + + + + {{else if (has-resource-filter-selections 'scopes.scope.auth-methods' 'type' diff --git a/ui/admin/app/templates/scopes/scope/users/user/accounts.hbs b/ui/admin/app/templates/scopes/scope/users/user/accounts.hbs index dc1ef3c37..b34f656b0 100644 --- a/ui/admin/app/templates/scopes/scope/users/user/accounts.hbs +++ b/ui/admin/app/templates/scopes/scope/users/user/accounts.hbs @@ -59,7 +59,7 @@ - {{#if (can 'removeAccounts user' @model.user)}} + {{#if (can 'removeAccount user' @model.user account=account)}} 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(); }); }); diff --git a/ui/admin/tests/acceptance/auth-methods/update-test.js b/ui/admin/tests/acceptance/auth-methods/update-test.js index fb8cd3985..6bb95c225 100644 --- a/ui/admin/tests/acceptance/auth-methods/update-test.js +++ b/ui/admin/tests/acceptance/auth-methods/update-test.js @@ -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'); diff --git a/ui/admin/tests/acceptance/authentication-test.js b/ui/admin/tests/acceptance/authentication-test.js index 694cd8229..4a2eb37a7 100644 --- a/ui/admin/tests/acceptance/authentication-test.js +++ b/ui/admin/tests/acceptance/authentication-test.js @@ -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; diff --git a/ui/admin/tests/acceptance/managed-groups/create-test.js b/ui/admin/tests/acceptance/managed-groups/create-test.js index 85fe28615..2d4802bfc 100644 --- a/ui/admin/tests/acceptance/managed-groups/create-test.js +++ b/ui/admin/tests/acceptance/managed-groups/create-test.js @@ -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, diff --git a/ui/admin/tests/acceptance/managed-groups/delete-test.js b/ui/admin/tests/acceptance/managed-groups/delete-test.js index 00a8d06d8..db30cefff 100644 --- a/ui/admin/tests/acceptance/managed-groups/delete-test.js +++ b/ui/admin/tests/acceptance/managed-groups/delete-test.js @@ -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, diff --git a/ui/admin/tests/acceptance/managed-groups/list-test.js b/ui/admin/tests/acceptance/managed-groups/list-test.js index 0c007bced..6b33317ad 100644 --- a/ui/admin/tests/acceptance/managed-groups/list-test.js +++ b/ui/admin/tests/acceptance/managed-groups/list-test.js @@ -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, diff --git a/ui/admin/tests/acceptance/managed-groups/members-test.js b/ui/admin/tests/acceptance/managed-groups/members-test.js index fc682cfec..7f36c5792 100644 --- a/ui/admin/tests/acceptance/managed-groups/members-test.js +++ b/ui/admin/tests/acceptance/managed-groups/members-test.js @@ -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' ); diff --git a/ui/admin/tests/acceptance/managed-groups/read-test.js b/ui/admin/tests/acceptance/managed-groups/read-test.js index 2cb3c1317..67f97f853 100644 --- a/ui/admin/tests/acceptance/managed-groups/read-test.js +++ b/ui/admin/tests/acceptance/managed-groups/read-test.js @@ -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, diff --git a/ui/admin/tests/acceptance/users/accounts-test.js b/ui/admin/tests/acceptance/users/accounts-test.js index f60038420..40dfd2dd7 100644 --- a/ui/admin/tests/acceptance/users/accounts-test.js +++ b/ui/admin/tests/acceptance/users/accounts-test.js @@ -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(); }); }); diff --git a/ui/admin/tests/e2e/README.md b/ui/admin/tests/e2e/README.md index 9560a2109..e6de7407c 100644 --- a/ui/admin/tests/e2e/README.md +++ b/ui/admin/tests/e2e/README.md @@ -1,19 +1,59 @@ + + + +# 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) + + # 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` diff --git a/ui/admin/tests/unit/abilities/auth-method-test.js b/ui/admin/tests/unit/abilities/auth-method-test.js new file mode 100644 index 000000000..66888a231 --- /dev/null +++ b/ui/admin/tests/unit/abilities/auth-method-test.js @@ -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)); + }); +}); diff --git a/ui/admin/tests/unit/abilities/user-test.js b/ui/admin/tests/unit/abilities/user-test.js new file mode 100644 index 000000000..04c09244c --- /dev/null +++ b/ui/admin/tests/unit/abilities/user-test.js @@ -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, + }) + ); + }); +}); diff --git a/ui/desktop/app/routes/scopes/scope/authenticate/method.js b/ui/desktop/app/routes/scopes/scope/authenticate/method.js index f01c49aa2..8358f8ef5 100644 --- a/ui/desktop/app/routes/scopes/scope/authenticate/method.js +++ b/ui/desktop/app/routes/scopes/scope/authenticate/method.js @@ -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, diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs index a39a6d1b3..51414095b 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/index.hbs @@ -121,10 +121,10 @@ {{else}} {{model.displayName}} {{/if}} - {{#if model.target.description}} -

{{model.target.description}}

+ {{#if model.description}} +

{{model.description}}

{{/if}} - {{#if model.target.isActive}} + {{#if model.isActive}}