diff --git a/ui/core/app/controllers/application.js b/ui/core/app/controllers/application.js new file mode 100644 index 000000000..3fd811d62 --- /dev/null +++ b/ui/core/app/controllers/application.js @@ -0,0 +1,10 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class ApplicationController extends Controller { + + // =services + + @service session; + +} diff --git a/ui/core/app/routes/application.js b/ui/core/app/routes/application.js index 6cc5ca574..be17ca092 100644 --- a/ui/core/app/routes/application.js +++ b/ui/core/app/routes/application.js @@ -1,6 +1,68 @@ import Route from '@ember/routing/route'; +import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin'; +import Ember from 'ember'; +import { getOwner } from '@ember/application'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { later } from '@ember/runloop'; /** * Entry route for the application. */ -export default class ApplicationRoute extends Route {} +export default class ApplicationRoute extends Route.extend( + ApplicationRouteMixin +) { + + // =services + + @service session; + + // =attributes + + routeIfUnauthenticated = 'index'; + + // =methods + + /** + * After becoming authenticated, does nothing. This overrides the default + * behavior of the ApplicationRouteMixin, which is to redirect after auth. + * We'll handle this redirect manually in sub routes. + * @override + */ + sessionAuthenticated() { + // no op + } + + /** + * When the session ends, redirect to authenticate and reload the page to + * purge any in-memory state. + * + * TODO This application route is redirecting to a subroute, assuming + * that the user is logged into a specific organization. This won't always + * necessarily be the case. When we support MSP, users may authenticate + * without an org scope. So this will need to be rethought. + */ + async sessionInvalidated() { + // Catch error in this transition, since it will be aborted by the + // org auth route when it redirects to the first auth method. + await this.transitionTo(this.routeIfUnauthenticated); + // The Ember way of accessing globals... + const document = getOwner(this).lookup('service:-document').documentElement; + // defaultView === window, but without using globals directly + const { location } = document.parentNode.defaultView; + // Wait a beat, then reload the page... + // This is mostly to give the deauth request a chance to fire. + /* istanbul ignore if */ + if (!Ember.testing) later(location, location.reload, 150); + } + + // =actions + + /** + * Delegates invalidation to the session service. + */ + @action + invalidateSession() { + this.session.invalidate(); + } +} diff --git a/ui/core/app/routes/scopes/scope/authenticate/method.js b/ui/core/app/routes/scopes/scope/authenticate/method.js index c4ce43a11..29014e819 100644 --- a/ui/core/app/routes/scopes/scope/authenticate/method.js +++ b/ui/core/app/routes/scopes/scope/authenticate/method.js @@ -44,7 +44,6 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route { const authenticatorName = 'authenticator:password'; const scope = this.modelFor('scopes.scope'); const authMethod = this.currentModel; - const authMethodId = authMethod.id; const requestCookies = false; try { await this.session.authenticate( diff --git a/ui/core/app/templates/application.hbs b/ui/core/app/templates/application.hbs index fbbecf1e7..0f0b66871 100644 --- a/ui/core/app/templates/application.hbs +++ b/ui/core/app/templates/application.hbs @@ -6,6 +6,8 @@ + + {{!-- {{t 'dev.menu-item'}} @@ -15,16 +17,22 @@ {{t 'dev.section'}} {{t 'dev.section'}} - - - {{t 'dev.menu-item'}} - {{t 'dev.button-menu-item'}} - - + --}} + + {{#if this.session.isAuthenticated}} + + + + {{t 'actions.deauthenticate'}} + + + + {{/if}} + diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index aa0b6e14a..2ce197f7b 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -1,5 +1,5 @@ import config from '../config/environment'; -import { authHandler } from './route-handlers/auth'; +import { authHandler, deauthHandler } from './route-handlers/auth'; import { Response } from 'miragejs'; export default function() { @@ -56,8 +56,9 @@ export default function() { this.patch('/scopes/:scope_id/auth-methods/:id'); this.del('/scopes/:scope_id/auth-methods/:id'); - // Authenticate/deauthenticate route + // Authenticate/deauthenticate routes this.post('/scopes/:scope_id/auth-methods/:id_method', authHandler); + this.post('/scopes/:id_method', deauthHandler); // IAM : Users this.get('/scopes/:scope_id/users'); diff --git a/ui/core/mirage/route-handlers/auth.js b/ui/core/mirage/route-handlers/auth.js index 8bedb771c..596500f46 100644 --- a/ui/core/mirage/route-handlers/auth.js +++ b/ui/core/mirage/route-handlers/auth.js @@ -23,38 +23,28 @@ import { Response } from 'miragejs'; // } // } -/** - * Mirage doesn't distinguish between `/path:authenticate` and - * `/path:deauthenticate`, so we use the presence of a request body to indicate - * an authentication request and its absence to indicate deauthentication. - */ export function authHandler({ scopes }, request) { - const [ , method ] = request.params.id_method.split(':'); const payload = JSON.parse(request.requestBody); - const isAuthenticationRequest = method === 'authenticate'; - if (isAuthenticationRequest) { - // this is an auth request - if (payload.credentials.name === 'error') { - return new Response(400); - } else { - console.log(request); - const scopeAttrs = - this.serialize(scopes.find(request.params.scope_id)); - const scope = scopeAttrs; - return new Response(200, {}, { - scope, - id: 'token123', - token: 'thetokenstring', - user_id: 'user123', - auth_method_id: 'authmethod123', - created_time: '', - updated_time: '', - last_used_time: '', - expiration_time: '' - }); - } + if (payload.credentials.name === 'error') { + return new Response(400); } else { - // this is a deauth request - return new Response(200); + const scopeAttrs = + this.serialize(scopes.find(request.params.scope_id)); + const scope = scopeAttrs; + return new Response(200, {}, { + scope, + id: 'token123', + token: 'thetokenstring', + user_id: 'user123', + auth_method_id: 'authmethod123', + created_time: '', + updated_time: '', + last_used_time: '', + expiration_time: '' + }); } } + +export function deauthHandler() { + return new Response(200); +} diff --git a/ui/core/tests/acceptance/authentication-test.js b/ui/core/tests/acceptance/authentication-test.js index 89c9b842b..bd5c2a1a5 100644 --- a/ui/core/tests/acceptance/authentication-test.js +++ b/ui/core/tests/acceptance/authentication-test.js @@ -3,6 +3,11 @@ import { visit, currentURL, fillIn, click, find } 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'; +import { + currentSession, + //authenticateSession, + //invalidateSession, +} from 'ember-simple-auth/test-support'; module('Acceptance | authentication', function(hooks) { setupApplicationTest(hooks); @@ -10,6 +15,7 @@ module('Acceptance | authentication', function(hooks) { let orgScope; let orgScopeID; + let scope; let authMethod; let authMethodID; let authMethodAuthenticateURL; @@ -17,8 +23,8 @@ module('Acceptance | authentication', function(hooks) { hooks.beforeEach(function () { orgScope = this.server.create('scope', {type: 'org'}, 'withChildren'); - const scope = { id: orgScope.id, type: orgScope.type }; - authMethod = server.create('auth-method', { scope }); + scope = { id: orgScope.id, type: orgScope.type }; + authMethod = this.server.create('auth-method', { scope }); orgScopeID = orgScope.id; authMethodID = authMethod.id; authMethodAuthenticateURL = `/scopes/${orgScopeID}/authenticate/${authMethodID}`; @@ -33,21 +39,37 @@ module('Acceptance | authentication', function(hooks) { }); test('failed authentication shows a notification message', async function(assert) { - assert.expect(1); + assert.expect(3); await visit(authMethodAuthenticateURL); + assert.notOk(currentSession().isAuthenticated); await fillIn('[name="identification"]', 'error'); await click('[type="submit"]'); assert.ok(find('.rose-notification.is-error')); + assert.notOk(currentSession().isAuthenticated); }); test('successful authentication redirects to projects', async function(assert) { - assert.expect(2); + assert.expect(4); await visit(authMethodAuthenticateURL); + assert.notOk(currentSession().isAuthenticated); assert.equal(currentURL(), authMethodAuthenticateURL); await fillIn('[name="identification"]', 'test'); await fillIn('[name="password"]', 'test'); await click('[type="submit"]'); assert.equal(currentURL(), projectsURL); + assert.ok(currentSession().isAuthenticated); + }); + + test('deauthentication redirects to index', async function(assert) { + assert.expect(3); + await visit(authMethodAuthenticateURL); + await fillIn('[name="identification"]', 'test'); + await fillIn('[name="password"]', 'test'); + await click('[type="submit"]'); + assert.ok(currentSession().isAuthenticated); + await click('.rose-button-header-dropdown'); + assert.notOk(currentSession().isAuthenticated); + assert.equal(currentURL(), '/'); }); }); diff --git a/ui/core/tests/unit/controllers/application-test.js b/ui/core/tests/unit/controllers/application-test.js new file mode 100644 index 000000000..93d19cebe --- /dev/null +++ b/ui/core/tests/unit/controllers/application-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | application', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:application'); + assert.ok(controller); + }); +});