refactor: 💡 only show scopes with auth methods at authenticate (#2129)

* refactor: 💡 only show scopes with auth methods at authenticate

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-2355

* test: 💍 verify scopes with no auth methods are hidden

when authenticating

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-2355

* refactor: 💡 update logic for fetching scopes and auth methods

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-2355

* refactor: 💡 PR feedback changes

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-2355

* refactor: 💡 added unless block back to template

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-2355

* refactor: 💡 update admin authenticate route and template

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-2355

* test: 💍 add test for orgs with no auth methods admin ui

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-2355
This commit is contained in:
Cameron Perera authored and GitHub committed 2024-02-08 10:01:20 -06:00
1 parent 022ac0fffe
commit d2d332b7d1
6 files changed
+93 -40

No files matched your search

@@ -5,7 +5,6 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
export default class ScopesScopeAuthenticateRoute extends Route {
// =services
@@ -13,6 +12,7 @@ export default class ScopesScopeAuthenticateRoute extends Route {
@service session;
@service router;
@service resourceFilterStore;
// =methods
beforeModel() {
@@ -27,16 +27,7 @@ export default class ScopesScopeAuthenticateRoute extends Route {
*/
async model() {
const { id: scope_id } = this.modelFor('scopes.scope');
// Fetch auth methods for the current scope
const authMethods = await this.resourceFilterStore.queryBy(
'auth-method',
{
authorized_actions: [{ contains: 'authenticate' }],
},
{
scope_id,
},
);
// Preload all authenticatable auth methods into the store
const authMethodsForAllScopes = await this.resourceFilterStore.queryBy(
'auth-method',
@@ -48,22 +39,25 @@ export default class ScopesScopeAuthenticateRoute extends Route {
recursive: true,
},
);
// Fetch org scopes
// and filter out any that have no auth methods
const scopes = this.modelFor('scopes').filter(
({ id: scope_id, isOrg }) =>
isOrg &&
authMethodsForAllScopes.filter((i) => i.scopeID === scope_id).length,
const scopeIDs = new Set(
authMethodsForAllScopes.map((authMethod) => authMethod.scopeID),
);
return hash({
// Fetch org scopes and filter out any that have no auth methods
const scopes = this.modelFor('scopes').filter(({ id: scope_id }) =>
scopeIDs.has(scope_id),
);
// Filter out auth methods that are not for the current scope
const authMethods = authMethodsForAllScopes.filter(
(authMethod) => authMethod.scopeID === scope_id,
);
return {
scope: this.modelFor('scopes.scope'),
scopes,
authMethods,
// for integration testing:
// authMethods: A([{
// id: 'am_1234567890',
// type: 'password'
// }])
});
};
}
}
@@ -61,6 +61,19 @@
{{#if @model.authMethods}}
{{outlet}}
{{/if}}
{{#unless @model.authMethods}}
<Rose::Layout::Centered>
<Rose::Message
@title={{t 'resources.auth-method.messages.none.title'}}
@icon='flight-icons/svg/alert-circle-16'
as |message|
>
<message.description>
{{t 'resources.auth-method.messages.none.description'}}
</message.description>
</Rose::Message>
</Rose::Layout::Centered>
{{/unless}}
</:body>
</Rose::Frame>
</page.body>
@@ -76,6 +76,15 @@ module('Acceptance | authentication', function (hooks) {
},
'withChildren',
);
// create an emtpy org with no auth methods
this.server.create(
'scope',
{
type: 'org',
scope: { id: globalScope.id, type: globalScope.type },
},
'withChildren',
);
scope = { id: orgScope.id, type: orgScope.type };
globalAuthMethod = this.server.create('auth-method', {
scope: globalScope,
@@ -366,4 +375,12 @@ module('Acceptance | authentication', function (hooks) {
});
// TODO: test OIDC retry and cancel
test('org scopes with no auth methods are not visible in dropdown', async function (assert) {
await visit(authMethodGlobalAuthenticateURL);
await click('.rose-dropdown-trigger');
assert.dom('.rose-dropdown-content a').exists({ count: 2 });
});
});
@@ -5,7 +5,6 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
export default class ScopesScopeAuthenticateRoute extends Route {
// =services
@@ -14,6 +13,7 @@ export default class ScopesScopeAuthenticateRoute extends Route {
@service session;
@service clusterUrl;
@service router;
@service resourceFilterStore;
// =methods
@@ -28,14 +28,40 @@ export default class ScopesScopeAuthenticateRoute extends Route {
* scope and all scopes (for org navigation).
* @return {Promise} `{scope, scopes, authMethods}`
*/
model() {
async model() {
const { id: scope_id } = this.modelFor('scopes.scope');
return hash({
// Preload all authenticatable auth methods into the store
const authMethodsForAllScopes = await this.resourceFilterStore.queryBy(
'auth-method',
{
authorized_actions: [{ contains: 'authenticate' }],
},
{
scope_id: 'global',
recursive: true,
},
);
const scopeIDs = new Set(
authMethodsForAllScopes.map((authMethod) => authMethod.scopeID),
);
// Fetch org scopes and filter out any that have no auth methods
const scopes = this.modelFor('scopes').filter(({ id: scope_id }) =>
scopeIDs.has(scope_id),
);
// Filter out auth methods that are not for the current scope
const authMethods = authMethodsForAllScopes.filter(
(authMethod) => authMethod.scopeID === scope_id,
);
return {
scope: this.modelFor('scopes.scope'),
scopes: this.modelFor('scopes').filter((scope) => scope.isOrg),
authMethods: this.store.query('auth-method', { scope_id }),
});
scopes,
authMethods,
};
}
redirect() {
@@ -70,6 +70,5 @@
</Rose::Message>
</Rose::Layout::Centered>
{{/unless}}
</BrandedCard>
</main>
@@ -31,6 +31,7 @@ module('Acceptance | authentication', function (hooks) {
scopes: {
global: null,
org: null,
emptyOrg: null,
project: null,
},
authMethods: {
@@ -92,6 +93,10 @@ module('Acceptance | authentication', function (hooks) {
stubs.project = { id: instances.scopes.project.id, type: 'project' };
// create other resources
instances.scopes.emptyOrg = this.server.create('scope', {
type: 'org',
scope: stubs.global,
});
instances.authMethods.global = this.server.create('auth-method', {
scope: instances.scopes.global,
type: 'password',
@@ -138,15 +143,6 @@ module('Acceptance | authentication', function (hooks) {
assert.strictEqual(currentURL(), urls.authenticate.methods.global);
});
test('visiting authenticate route when there no methods shows a message', async function (assert) {
assert.expect(2);
instances.authMethods.global.destroy();
await visit(urls.authenticate.global);
await a11yAudit();
assert.strictEqual(currentURL(), urls.authenticate.global);
assert.ok(find('.rose-message'));
});
test('visiting authenticate route without clusterUrl redirects to clusterUrl index', async function (assert) {
assert.expect(1);
this.owner.lookup('service:clusterUrl').rendererClusterUrl = null;
@@ -255,4 +251,12 @@ module('Acceptance | authentication', function (hooks) {
assert.notOk(getRootElement().classList.contains('rose-theme-light'));
assert.notOk(getRootElement().classList.contains('rose-theme-dark'));
});
test('org scopes with no auth methods are not visible in dropdown', async function (assert) {
await visit(urls.authenticate.methods.global);
await click('.rose-dropdown-trigger');
assert.dom('.rose-dropdown-content a').exists({ count: 2 });
});
});