diff --git a/addons/api/addon/abilities/auth-method.js b/addons/api/addon/abilities/auth-method.js new file mode 100644 index 000000000..744f93ac7 --- /dev/null +++ b/addons/api/addon/abilities/auth-method.js @@ -0,0 +1,20 @@ +/** + * 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'); + } +} diff --git a/addons/api/addon/models/auth-method.js b/addons/api/addon/models/auth-method.js index dc519cf47..8599160f1 100644 --- a/addons/api/addon/models/auth-method.js +++ b/addons/api/addon/models/auth-method.js @@ -6,6 +6,16 @@ import GeneratedAuthMethodModel from '../generated/models/auth-method'; import { equal } from '@ember/object/computed'; +export const TYPE_AUTH_METHOD_PASSWORD = 'password'; +export const TYPE_AUTH_METHOD_OIDC = 'oidc'; +export const TYPE_AUTH_METHOD_LDAP = 'ldap'; + +export const TYPES_AUTH_METHOD = Object.freeze([ + TYPE_AUTH_METHOD_PASSWORD, + TYPE_AUTH_METHOD_OIDC, + TYPE_AUTH_METHOD_LDAP, +]); + /** * Enum options per auth method type and field. */ @@ -44,14 +54,28 @@ export default class AuthMethodModel extends GeneratedAuthMethodModel { * @type {boolean} */ get isPassword() { - return this.type === 'password'; + return this.type === TYPE_AUTH_METHOD_PASSWORD; } /** * @type {boolean} */ get isOIDC() { - return this.type === 'oidc'; + return this.type === TYPE_AUTH_METHOD_OIDC; + } + + /** + * @type {boolean} + */ + get isLDAP() { + return this.type === TYPE_AUTH_METHOD_LDAP; + } + + /** + * @type {boolean} + */ + get isUnknown() { + return !TYPES_AUTH_METHOD.includes(this.type); } /** diff --git a/addons/api/addon/serializers/auth-method.js b/addons/api/addon/serializers/auth-method.js index d21f54cff..01488e4e6 100644 --- a/addons/api/addon/serializers/auth-method.js +++ b/addons/api/addon/serializers/auth-method.js @@ -4,6 +4,8 @@ */ import ApplicationSerializer from './application'; +import { TYPE_AUTH_METHOD_OIDC } from 'api/models/auth-method'; + export default class AuthMethodSerializer extends ApplicationSerializer { // =methods @@ -15,7 +17,7 @@ export default class AuthMethodSerializer extends ApplicationSerializer { */ serialize(snapshot) { switch (snapshot.record.type) { - case 'oidc': + case TYPE_AUTH_METHOD_OIDC: return this.serializeOIDC(...arguments); default: return this.serializeDefault(...arguments); diff --git a/addons/api/mirage/factories/auth-method.js b/addons/api/mirage/factories/auth-method.js index 96589133f..dee5481a3 100644 --- a/addons/api/mirage/factories/auth-method.js +++ b/addons/api/mirage/factories/auth-method.js @@ -8,7 +8,10 @@ import { trait } from 'miragejs'; import permissions from '../helpers/permissions'; import generateId from '../helpers/id'; -const types = ['password', 'oidc']; +import { + TYPE_AUTH_METHOD_OIDC, + TYPES_AUTH_METHOD, +} from 'api/models/auth-method'; export default factory.extend({ authorized_actions: () => @@ -29,7 +32,7 @@ export default factory.extend({ id: () => generateId('am_'), // Cycle through available types - type: (i) => types[i % types.length], + type: (i) => TYPES_AUTH_METHOD[i % TYPES_AUTH_METHOD.length], /** * Adds accounts (with associated users) to auth method and managed groups. @@ -43,7 +46,7 @@ export default factory.extend({ user.update({ accountIds: [id] }); }); - if (type === 'oidc') { + if (type === TYPE_AUTH_METHOD_OIDC) { server .createList('managed-group', 2, { scope, authMethod }) .map((managedGroup) => { diff --git a/addons/api/mirage/generated/factories/auth-method.js b/addons/api/mirage/generated/factories/auth-method.js index bc6b5c79b..d0cdd71c3 100644 --- a/addons/api/mirage/generated/factories/auth-method.js +++ b/addons/api/mirage/generated/factories/auth-method.js @@ -6,11 +6,16 @@ import { Factory } from 'miragejs'; import { faker } from '@faker-js/faker'; +import { + TYPE_AUTH_METHOD_PASSWORD, + TYPE_AUTH_METHOD_OIDC, +} from 'api/models/auth-method'; + /** * GeneratedAuthMethodModelFactory */ export default Factory.extend({ - type: 'password', + type: TYPE_AUTH_METHOD_PASSWORD, name: () => faker.random.words(), description: () => faker.random.words(), created_time: () => faker.date.recent(), @@ -20,7 +25,7 @@ export default Factory.extend({ attributes: function () { let attrs; switch (this.type) { - case 'oidc': + case TYPE_AUTH_METHOD_OIDC: attrs = { api_url_prefix: 'http://127.0.0.1:9200', callback_url: diff --git a/addons/api/tests/unit/models/auth-method-test.js b/addons/api/tests/unit/models/auth-method-test.js index 1e084b9ae..a908d2280 100644 --- a/addons/api/tests/unit/models/auth-method-test.js +++ b/addons/api/tests/unit/models/auth-method-test.js @@ -6,13 +6,23 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { + TYPE_AUTH_METHOD_PASSWORD, + TYPE_AUTH_METHOD_OIDC, + TYPE_AUTH_METHOD_LDAP, +} from 'api/models/auth-method'; module('Unit | Model | auth method', function (hooks) { setupTest(hooks); setupMirage(hooks); + let store; + + hooks.beforeEach(function () { + store = this.owner.lookup('service:store'); + }); + test('it exists', function (assert) { - let store = this.owner.lookup('service:store'); let model = store.createRecord('auth-method', {}); assert.ok(model); }); @@ -32,13 +42,12 @@ module('Unit | Model | auth method', function (hooks) { return { id: '123abc' }; } ); - const store = this.owner.lookup('service:store'); store.push({ data: { id: '123abc', type: 'auth-method', attributes: { - type: 'oidc', + type: TYPE_AUTH_METHOD_OIDC, state: 'foobar', version: 1, scope: { @@ -51,4 +60,72 @@ module('Unit | Model | auth method', function (hooks) { const model = store.peekRecord('auth-method', '123abc'); await model.changeState('foobar'); }); + + test('it has isPassword property and returns the expected values', async 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', async 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', async 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', async function (assert) { + assert.expect(4); + const modelA = store.createRecord('auth-method', { + type: TYPE_AUTH_METHOD_LDAP, + }); + const modelB = store.createRecord('auth-method', { + type: TYPE_AUTH_METHOD_PASSWORD, + }); + const modelC = store.createRecord('auth-method', { + type: TYPE_AUTH_METHOD_OIDC, + }); + const modelD = store.createRecord('auth-method', { + type: 'no-such-type', + }); + assert.false(modelA.isUnknown); + assert.false(modelB.isUnknown); + assert.false(modelC.isUnknown); + assert.true(modelD.isUnknown); + }); }); diff --git a/addons/api/tests/unit/serializers/auth-method-test.js b/addons/api/tests/unit/serializers/auth-method-test.js index 4ecf4d08c..f9989a5f3 100644 --- a/addons/api/tests/unit/serializers/auth-method-test.js +++ b/addons/api/tests/unit/serializers/auth-method-test.js @@ -7,6 +7,11 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { + TYPE_AUTH_METHOD_PASSWORD, + TYPE_AUTH_METHOD_OIDC, +} from 'api/models/auth-method'; + module('Unit | Serializer | auth method', function (hooks) { setupTest(hooks); setupMirage(hooks); @@ -21,7 +26,9 @@ module('Unit | Serializer | auth method', function (hooks) { test('it serializes password records', function (assert) { assert.expect(2); let store = this.owner.lookup('service:store'); - let record = store.createRecord('auth-method', { type: 'password' }); + let record = store.createRecord('auth-method', { + type: TYPE_AUTH_METHOD_PASSWORD, + }); let serializedRecord = record.serialize(); @@ -36,7 +43,7 @@ module('Unit | Serializer | auth method', function (hooks) { assert.expect(1); let store = this.owner.lookup('service:store'); let record = store.createRecord('auth-method', { - type: 'oidc', + type: TYPE_AUTH_METHOD_OIDC, name: 'OIDC Auth Method', state: 'foo', account_claim_maps: [{ from: 'foo', to: 'bar' }], @@ -62,7 +69,7 @@ module('Unit | Serializer | auth method', function (hooks) { let serializedRecord = record.serialize(); assert.deepEqual(serializedRecord, { - type: 'oidc', + type: TYPE_AUTH_METHOD_OIDC, name: 'OIDC Auth Method', description: null, attributes: { @@ -87,7 +94,7 @@ module('Unit | Serializer | auth method', function (hooks) { const store = this.owner.lookup('service:store'); const serializer = store.serializerFor('auth-method'); const record = store.createRecord('auth-method', { - type: 'oidc', + type: TYPE_AUTH_METHOD_OIDC, attributes: { state: 'foo', }, @@ -175,7 +182,7 @@ module('Unit | Serializer | auth method', function (hooks) { max_age: maxAge, }, version: 1, - type: 'oidc', + type: TYPE_AUTH_METHOD_OIDC, id: 'oidc123', })); diff --git a/ui/admin/app/abilities/auth-method.js b/ui/admin/app/abilities/auth-method.js new file mode 100644 index 000000000..ebbf9b0cd --- /dev/null +++ b/ui/admin/app/abilities/auth-method.js @@ -0,0 +1,21 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import AuthMethodAbility from 'api/abilities/auth-method'; + +export default class OverrideAuthMethodAbility extends AuthMethodAbility { + /** + * These override abilities are temporary and just part of Phase 1 + * of LDAP auth method implementation. + */ + + get canRead() { + return !this.model.isLDAP && super.canRead; + } + + get canMakePrimary() { + return !this.model.isLDAP; + } +} diff --git a/ui/admin/app/routes/scopes/scope/auth-methods.js b/ui/admin/app/routes/scopes/scope/auth-methods.js index e4c2b9312..b5522e3db 100644 --- a/ui/admin/app/routes/scopes/scope/auth-methods.js +++ b/ui/admin/app/routes/scopes/scope/auth-methods.js @@ -10,6 +10,7 @@ import { loading } from 'ember-loading'; import { confirm } from 'core/decorators/confirm'; import { notifySuccess, notifyError } from 'core/decorators/notify'; import { resourceFilter } from 'core/decorators/resource-filter'; +import { TYPES_AUTH_METHOD } from 'api/models/auth-method'; export default class ScopesScopeAuthMethodsRoute extends Route { // =services @@ -23,7 +24,7 @@ export default class ScopesScopeAuthMethodsRoute extends Route { // =attributes - @resourceFilter({ allowed: ['password', 'oidc'] }) type; + @resourceFilter({ allowed: TYPES_AUTH_METHOD }) type; // =methods diff --git a/ui/admin/app/routes/scopes/scope/authenticate/method.js b/ui/admin/app/routes/scopes/scope/authenticate/method.js index f6532a008..f7cc6b680 100644 --- a/ui/admin/app/routes/scopes/scope/authenticate/method.js +++ b/ui/admin/app/routes/scopes/scope/authenticate/method.js @@ -9,6 +9,11 @@ import { getOwner } from '@ember/application'; import { action } from '@ember/object'; import { loading } from 'ember-loading'; import { notifyError } from 'core/decorators/notify'; +import { + TYPE_AUTH_METHOD_PASSWORD, + TYPE_AUTH_METHOD_OIDC, + TYPE_AUTH_METHOD_LDAP, +} from 'api/models/auth-method'; export default class ScopesScopeAuthenticateMethodRoute extends Route { // =services @@ -62,8 +67,8 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route { const authenticatorName = `authenticator:${authMethod.type}`; const requestCookies = false; switch (authMethod.type) { - case 'password': - case 'ldap': + case TYPE_AUTH_METHOD_PASSWORD: + case TYPE_AUTH_METHOD_LDAP: await this.session.authenticate( authenticatorName, creds, @@ -72,7 +77,7 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route { ); this.router.transitionTo('index'); break; - case 'oidc': + case TYPE_AUTH_METHOD_OIDC: await this.startOIDCAuthentication(authenticatorName, { scope, authMethod, diff --git a/ui/admin/app/templates/scopes/scope/auth-methods/index.hbs b/ui/admin/app/templates/scopes/scope/auth-methods/index.hbs index 2ecdc0458..31dac7bb6 100644 --- a/ui/admin/app/templates/scopes/scope/auth-methods/index.hbs +++ b/ui/admin/app/templates/scopes/scope/auth-methods/index.hbs @@ -120,7 +120,7 @@ {{#each @model as |authMethod|}}