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