From 4ef58a0d6d57ed3529ed3365482e353473d07568 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 24 Jul 2020 11:15:40 -0400 Subject: [PATCH 01/17] chore: port auth addon from demo branch --- addons/auth/addon/.gitkeep | 0 addons/auth/addon/authenticators/password.js | 87 ++++++++++++ addons/auth/addon/session-stores/cookie.js | 134 ++++++++++++++++++ .../addon/session-stores/local-storage.js | 20 +++ addons/auth/package.json | 4 + .../dummy/app/authenticators/password.js | 6 + .../tests/dummy/app/session-stores/cookie.js | 6 + .../dummy/app/session-stores/local-storage.js | 3 + addons/auth/tests/dummy/mirage/config.js | 22 +++ .../tests/dummy/mirage/scenarios/default.js | 7 + .../dummy/mirage/serializers/application.js | 3 + .../unit/authenticators/password-test.js | 92 ++++++++++++ .../tests/unit/session-stores/cookie-test.js | 92 ++++++++++++ yarn.lock | 87 +++++++++++- 14 files changed, 558 insertions(+), 5 deletions(-) delete mode 100644 addons/auth/addon/.gitkeep create mode 100644 addons/auth/addon/authenticators/password.js create mode 100644 addons/auth/addon/session-stores/cookie.js create mode 100644 addons/auth/addon/session-stores/local-storage.js create mode 100644 addons/auth/tests/dummy/app/authenticators/password.js create mode 100644 addons/auth/tests/dummy/app/session-stores/cookie.js create mode 100644 addons/auth/tests/dummy/app/session-stores/local-storage.js create mode 100644 addons/auth/tests/dummy/mirage/config.js create mode 100644 addons/auth/tests/dummy/mirage/scenarios/default.js create mode 100644 addons/auth/tests/dummy/mirage/serializers/application.js create mode 100644 addons/auth/tests/unit/authenticators/password-test.js create mode 100644 addons/auth/tests/unit/session-stores/cookie-test.js diff --git a/addons/auth/addon/.gitkeep b/addons/auth/addon/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/addons/auth/addon/authenticators/password.js b/addons/auth/addon/authenticators/password.js new file mode 100644 index 000000000..f4dc5668c --- /dev/null +++ b/addons/auth/addon/authenticators/password.js @@ -0,0 +1,87 @@ +import BaseAuthenticator from 'ember-simple-auth/authenticators/base'; +import { resolve, reject } from 'rsvp'; +import fetch from 'fetch'; + +/** + * + * This authenticator is intended for use with Ember Simple Auth. + * + * Simple id/password base authenticator sends credentials to an endpoint + * specified in the `authEndpoint` URL. If the HTTP response code is in the + * success range, authentication resolves. Otherwise it rejects. + * + * Upon session invalidation, deauthentication is attempted at the + * `deauthEndpoint`, but is not guaranteed. + * + * This authenticator should not be used directly because it does not specify + * an `authEndpoint` or `deauthEndpoint` of its own. To use, generate an + * application authenticator in your app `authenticators/application.js` and + * extend this class. + * + * @example + * + * import PasswordAuthenticator from 'auth/authenticators/password'; + * export default class ApplicationAuthenticator extends PasswordAuthenticator { + * authEndpoint = '/api/authenticate'; + * deauthEndpoint = '/api/deauthenticate'; + * } + * + */ +export default class PasswordAuthenticator extends BaseAuthenticator { + // =methods + + /** + * Restores the session if data is present. Otherwise rejects. + * @override + * @param {object} data + * @return {Promise} + */ + restore(data) { + return data ? resolve(data) : reject(); + } + + /** + * Posts credentials to the URL specified in `authEndpoint` and resolves + * if a success HTTP status code is received, otherwise rejects. + * + * If requesting cookies, be sure to use the cookie session store. Otherwise + * use the local storage session store. + * + * @override + * @param {object} creds + * @param {string} creds.identification is sent to server as `name` + * @param {string} creds.password + * @param {string} authMethodId ID of the auth method to use + * @param {boolean} requestCookies request cookie tokens (default `true`) + * @return {Promise} + */ + async authenticate( + { identification: name, password }, + authMethodID, + requestCookies = true + ) { + const body = JSON.stringify({ + auth_method_id: authMethodID, + token_type: requestCookies ? 'cookie' : null, + credentials: { name, password }, + }); + const response = await fetch(this.authEndpoint, { method: 'post', body }); + const json = await response.json(); + return response.status < 400 ? resolve(json) : reject(); + } + + /** + * Posts to the `deauthEntpoint` on a best-effort basis and then returns. + * Deauthentication with the server is not guaranteed and request failures + * are ignored. + * + * @override + * @return {Promise} + */ + invalidate() { + fetch(this.deauthEndpoint, { method: 'post' }).catch(() => { + /* no op */ + }); + return super.invalidate(...arguments); + } +} diff --git a/addons/auth/addon/session-stores/cookie.js b/addons/auth/addon/session-stores/cookie.js new file mode 100644 index 000000000..ea0501ad5 --- /dev/null +++ b/addons/auth/addon/session-stores/cookie.js @@ -0,0 +1,134 @@ +import BaseSessionStore from 'ember-simple-auth/session-stores/base'; +import { resolve, reject } from 'rsvp'; +import { get } from '@ember/object'; +import { inject as service } from '@ember/service'; + +/** + * + * This session store is intended for use with Ember Simple Auth when requesting + * that the server send cookies (see authenticators). + * + * The cookie session store is used to maintain session state for auth servers + * that send cookies. In this strategy, the server sets any cookies needed for + * future authorization. This store determines if a session is authenticated + * by looking for the presence of a cookie with name specified in `cookieName`. + * + * This session store should not be used directly because it does not specify + * `cookiePath` or `cookieName`. To use, create an application session store + * and extend this class, being sure to specify these attributes. + * + * @example + * + * import CookieSessionStore from 'auth/session-stores/cookie'; + * export default class ApplicationSessionStore extends CookieSessionStore { + * cookiePath = '/'; + * cookieName = 'auth-token'; + * } + * + */ +export default class CookieSessionStore extends BaseSessionStore { + // =services + + @service cookies; + + // =attributes + + /** + * The name of the cookie that stores the name of the authenticator, derived + * from the name of the session cookie. The authenticator name is used + * internally by Ember Simple Auth, since it supports simultaneous + * authentication schemes within a single application. + * @type {string} + */ + get authenticatorCookieName() { + return `${this.cookieName}-authenticator`; + } + + /** + * Name of the authenticator used, if any. + * @type {?string} + */ + get authenticatorName() { + return this.cookies.read(this.authenticatorCookieName); + } + + /** + * True if both session and authenticator cookies are present. + * False otherwise. + * @type {boolean} + */ + get sessionExists() { + const tokenExists = this.cookies.exists(this.cookieName); + const authenticatorExists = this.cookies.exists( + this.authenticatorCookieName + ); + return tokenExists && authenticatorExists; + } + + // =methods + + /** + * This method is called automatically on initialization to perform cleanup + * if necessary. + * + * Sessions exist only if both the session and authenticator cookies are + * present. If either one is missing, we want to cleanup to be sure they + * are both absent. + */ + init() { + if (!this.sessionExists) this.clear(); + } + + /** + * While the primary session cookie is persisted by the server, not the + * application, we still need to "remember" the name of the authenticator + * used to authenticate to properly restore a session. Thus when this method + * is called with an authenticator name, we save it into a cookie with name + * `authenticatorCookieName`. If this method is called without an + * authenticator, all related cookies are cleared. + * @param {object} data + * @return {Promise} + */ + persist(data) { + const authenticator = get(data, 'authenticated.authenticator'); + const path = this.cookiePath; + if (authenticator) { + this.cookies.write(this.authenticatorCookieName, authenticator, { path }); + } else { + this.clear(); + } + return resolve(); + } + + /** + * If a session exists (as reported by `sessionExists`), then this method + * resolves with the authentication object expected by Ember Simple Auth, + * which must specify the name of the authenticator used to authenticate. + * + * If the session does not exist, all related cookies are cleared and this + * method rejects. + * + * @return {Promise} + */ + restore() { + if (this.sessionExists) { + return resolve({ + authenticated: { + authenticator: this.authenticatorName, + }, + }); + } + this.clear(); + return reject(); + } + + /** + * Clears both cookies related to the session: the server specified session + * cookie as well as the authenticator name cookie. + */ + clear() { + const path = this.cookiePath; + this.cookies.clear(this.cookieName, { path }); + this.cookies.clear(this.authenticatorCookieName, { path }); + } +} diff --git a/addons/auth/addon/session-stores/local-storage.js b/addons/auth/addon/session-stores/local-storage.js new file mode 100644 index 000000000..7c216fa55 --- /dev/null +++ b/addons/auth/addon/session-stores/local-storage.js @@ -0,0 +1,20 @@ +import LocalStorageSessionStore from 'ember-simple-auth/session-stores/local-storage'; + +/** + * + * This session store is intended for use with Ember Simple Auth when requesting + * that the server send a full token (e.g. cookies are not requested, + * see authenticators). + * + * NOTE: when using a local storage scheme, authorization is not provided + * automatically. In order to achieve authorization... TODO + * + * @example + * + * import LocalStorageSessionStore from 'auth/session-stores/local-storage'; + * export default class ApplicationSessionStore extends LocalStorageSessionStore { + * + * } + * + */ +export default class AuthLocalStorageSessionStore extends LocalStorageSessionStore {} diff --git a/addons/auth/package.json b/addons/auth/package.json index cdd1f5623..33199987a 100644 --- a/addons/auth/package.json +++ b/addons/auth/package.json @@ -33,16 +33,20 @@ "ember-cli-dependency-checker": "^3.2.0", "ember-cli-eslint": "^5.1.0", "ember-cli-inject-live-reload": "^2.0.2", + "ember-cli-mirage": "^1.1.7", "ember-cli-sri": "^2.1.1", "ember-cli-template-lint": "^1.0.0-beta.3", "ember-cli-uglify": "^3.0.0", + "ember-cookies": "^0.5.2", "ember-data": "^3.16.0", "ember-disable-prototype-extensions": "^1.1.3", "ember-export-application-global": "^2.0.1", + "ember-fetch": "^8.0.1", "ember-load-initializers": "^2.1.1", "ember-maybe-import-regenerator": "^0.1.6", "ember-qunit": "^4.6.0", "ember-resolver": "^7.0.0", + "ember-simple-auth": "^3.0.0", "ember-source": "~3.16.0", "ember-source-channel-url": "^2.0.1", "ember-try": "^1.4.0", diff --git a/addons/auth/tests/dummy/app/authenticators/password.js b/addons/auth/tests/dummy/app/authenticators/password.js new file mode 100644 index 000000000..2f312a7e4 --- /dev/null +++ b/addons/auth/tests/dummy/app/authenticators/password.js @@ -0,0 +1,6 @@ +import PasswordAuthenticator from 'auth/authenticators/password'; + +export default class ApplicationPasswordAuthenticator extends PasswordAuthenticator { + authEndpoint = '/authenticate'; + deauthEndpoint = '/deauthenticate'; +} diff --git a/addons/auth/tests/dummy/app/session-stores/cookie.js b/addons/auth/tests/dummy/app/session-stores/cookie.js new file mode 100644 index 000000000..50c1a4441 --- /dev/null +++ b/addons/auth/tests/dummy/app/session-stores/cookie.js @@ -0,0 +1,6 @@ +import CookieSessionStore from 'auth/session-stores/cookie'; + +export default class ApplicationCookieSessionStore extends CookieSessionStore { + cookiePath = '/'; + cookieName = 'my-cookie'; +} diff --git a/addons/auth/tests/dummy/app/session-stores/local-storage.js b/addons/auth/tests/dummy/app/session-stores/local-storage.js new file mode 100644 index 000000000..b7546d920 --- /dev/null +++ b/addons/auth/tests/dummy/app/session-stores/local-storage.js @@ -0,0 +1,3 @@ +import LocalStorageSessionStore from 'auth/session-stores/local-storage'; + +export default class ApplicationLocalStorageSessionStore extends LocalStorageSessionStore {} diff --git a/addons/auth/tests/dummy/mirage/config.js b/addons/auth/tests/dummy/mirage/config.js new file mode 100644 index 000000000..3e74fe7c0 --- /dev/null +++ b/addons/auth/tests/dummy/mirage/config.js @@ -0,0 +1,22 @@ +export default function () { + // These comments are here to help you get started. Feel free to delete them. + /* + Config (with defaults). + + Note: these only affect routes defined *after* them! + */ + // this.urlPrefix = ''; // make this `http://localhost:8080`, for example, if your API is on a different server + // this.namespace = ''; // make this `/api`, for example, if your API is namespaced + // this.timing = 400; // delay for each request, automatically set to 0 during testing + /* + Shorthand cheatsheet: + + this.get('/posts'); + this.post('/posts'); + this.get('/posts/:id'); + this.put('/posts/:id'); // or this.patch + this.del('/posts/:id'); + + https://www.ember-cli-mirage.com/docs/route-handlers/shorthands + */ +} diff --git a/addons/auth/tests/dummy/mirage/scenarios/default.js b/addons/auth/tests/dummy/mirage/scenarios/default.js new file mode 100644 index 000000000..d388296b5 --- /dev/null +++ b/addons/auth/tests/dummy/mirage/scenarios/default.js @@ -0,0 +1,7 @@ +export default function (/* server */) { + /* + Seed your development database using your factories. + This data will not be loaded in your tests. + */ + // server.createList('post', 10); +} diff --git a/addons/auth/tests/dummy/mirage/serializers/application.js b/addons/auth/tests/dummy/mirage/serializers/application.js new file mode 100644 index 000000000..6224ab8c9 --- /dev/null +++ b/addons/auth/tests/dummy/mirage/serializers/application.js @@ -0,0 +1,3 @@ +import { JSONAPISerializer } from 'ember-cli-mirage'; + +export default JSONAPISerializer.extend({}); diff --git a/addons/auth/tests/unit/authenticators/password-test.js b/addons/auth/tests/unit/authenticators/password-test.js new file mode 100644 index 000000000..bf516dd43 --- /dev/null +++ b/addons/auth/tests/unit/authenticators/password-test.js @@ -0,0 +1,92 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import { Response } from 'miragejs'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; + +module('Unit | Authenticator | password', function (hooks) { + setupTest(hooks); + setupMirage(hooks); + + test('it authenticates to the specified authEndpoint', async function (assert) { + assert.expect(2); + const authenticator = this.owner.lookup('authenticator:password'); + this.server.post(authenticator.authEndpoint, (schema, request) => { + const json = JSON.parse(request.requestBody); + assert.ok(json.token_type, 'Requested token cookies by default'); + return new Response(200); + }); + await authenticator.authenticate({}, '').then(() => { + assert.ok(true, 'authentication succeeded'); + }); + }); + + test('it can authenticate without requesting cookies', async function (assert) { + assert.expect(2); + const authenticator = this.owner.lookup('authenticator:password'); + this.server.post(authenticator.authEndpoint, (schema, request) => { + const json = JSON.parse(request.requestBody); + assert.notOk(json.token_type, 'Did not request tokens cookies'); + return new Response(200); + }); + await authenticator.authenticate({}, '', false).then(() => { + assert.ok(true, 'authentication succeeded'); + }); + }); + + test('it authenticates with the expected payload', async function (assert) { + assert.expect(1); + const authenticator = this.owner.lookup('authenticator:password'); + this.server.post(authenticator.authEndpoint, (schema, request) => { + const json = JSON.parse(request.requestBody); + assert.deepEqual(json, { + auth_method_id: '123', + token_type: 'cookie', + credentials: { + name: 'foo', + password: 'bar', + }, + }); + return new Response(200); + }); + const creds = { + identification: 'foo', + password: 'bar', + }; + const authMethodID = '123'; + await authenticator.authenticate(creds, authMethodID); + }); + + test('it rejects if the endpoint sends an error status code', async function (assert) { + assert.expect(1); + const authenticator = this.owner.lookup('authenticator:password'); + this.server.post(authenticator.authEndpoint, () => { + return new Response(400); + }); + await authenticator.authenticate({}).catch(() => { + assert.ok(true, 'authentication failed'); + }); + }); + + test('it deauthenticates on invalidation', async function (assert) { + assert.expect(1); + const authenticator = this.owner.lookup('authenticator:password'); + this.server.post(authenticator.deauthEndpoint, () => { + assert.ok(true, 'deauthentication occurred'); + return new Response(200); + }); + await authenticator.invalidate(); + }); + + test('it invalidation succeeds even if the deauthentication request fails', async function (assert) { + assert.expect(2); + const authenticator = this.owner.lookup('authenticator:password'); + this.server.post(authenticator.deauthEndpoint, () => { + assert.ok(true, 'deauthentication was requested'); + return new Response(500); + }); + await authenticator + .invalidate() + .then(() => assert.ok(true)) + .catch(() => assert.notOk(true, 'uh oh, this should not happen')); + }); +}); diff --git a/addons/auth/tests/unit/session-stores/cookie-test.js b/addons/auth/tests/unit/session-stores/cookie-test.js new file mode 100644 index 000000000..464ebab52 --- /dev/null +++ b/addons/auth/tests/unit/session-stores/cookie-test.js @@ -0,0 +1,92 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Session Store | cookie', function (hooks) { + setupTest(hooks); + + let sessionStore; + + hooks.beforeEach(function () { + sessionStore = this.owner.lookup('session-store:cookie'); + sessionStore.clear(); + }); + + hooks.afterEach(function () { + sessionStore.clear(); + }); + + test('it restores a session if both necessary cookies are present (session exists)', async function (assert) { + assert.expect(2); + sessionStore.cookies.write(sessionStore.cookieName, 'token-value', { + path: '/', + }); + sessionStore.cookies.write( + sessionStore.authenticatorCookieName, + 'authenticator-name', + { path: '/' } + ); + assert.ok(sessionStore.sessionExists); + const session = await sessionStore.restore(); + assert.deepEqual(session, { + authenticated: { + authenticator: 'authenticator-name', + }, + }); + }); + + test('it rejects restore if the session does not exist', async function (assert) { + assert.expect(2); + assert.notOk(sessionStore.sessionExists); + await sessionStore.restore().catch(() => {}); + assert.ok(true, 'Rejected'); + }); + + test('it cleans up cookies on restore rejection', async function (assert) { + assert.expect(4); + // For session cookie + sessionStore.cookies.write(sessionStore.cookieName, 'token-value', { + path: '/', + }); + assert.ok( + sessionStore.cookies.exists(sessionStore.cookieName), + 'cookie exists' + ); + await sessionStore.restore().catch(() => {}); + assert.notOk( + sessionStore.cookies.exists(sessionStore.cookieName), + 'cookie was cleared' + ); + // For authenticator name cookie + sessionStore.cookies.write( + sessionStore.authenticatorCookieName, + 'authenticator-name', + { path: '/' } + ); + assert.ok( + sessionStore.cookies.exists(sessionStore.authenticatorCookieName), + 'cookie exists' + ); + await sessionStore.restore().catch(() => {}); + assert.notOk( + sessionStore.cookies.exists(sessionStore.authenticatorCookieName), + 'cookie was cleared' + ); + }); + + test('it persists only an authenticator name cookie, since the server persists the session cookie', async function (assert) { + assert.expect(4); + assert.notOk(sessionStore.cookies.exists(sessionStore.cookieName)); + assert.notOk( + sessionStore.cookies.exists(sessionStore.authenticatorCookieName) + ); + sessionStore.persist({ authenticated: { authenticator: 'foobar' } }); + assert.notOk( + sessionStore.cookies.exists(sessionStore.cookieName), + 'session cookie was not set' + ); + assert.equal( + sessionStore.cookies.read(sessionStore.authenticatorCookieName), + 'foobar' + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index c7cf88da0..5304e5ace 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5335,6 +5335,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base-64@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" + integrity sha1-eAqZyE59YAJgNhURxId2E78k9rs= + base64-arraybuffer@0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" @@ -5707,6 +5712,23 @@ broccoli-concat@^3.2.2, broccoli-concat@^3.7.1, broccoli-concat@^3.7.4, broccoli lodash.uniq "^4.2.0" walk-sync "^0.3.2" +broccoli-concat@^4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/broccoli-concat/-/broccoli-concat-4.2.4.tgz#78e359ddc540b999d815355163bf3cfb6bd67322" + integrity sha512-NgdBIE57r+U/AslBohQr0mCS7PopIWL8dihMI1CzqffQkisAgqWMuddjYmizqRBQlml7crBFaBeUnPDHhf4/RQ== + dependencies: + broccoli-debug "^0.6.5" + broccoli-kitchen-sink-helpers "^0.3.1" + broccoli-plugin "^4.0.2" + ensure-posix-path "^1.0.2" + fast-sourcemap-concat "^2.1.0" + find-index "^1.1.0" + fs-extra "^8.1.0" + fs-tree-diff "^2.0.1" + lodash.merge "^4.6.2" + lodash.omit "^4.1.0" + lodash.uniq "^4.2.0" + broccoli-config-loader@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/broccoli-config-loader/-/broccoli-config-loader-1.0.1.tgz#d10aaf8ebc0cb45c1da5baa82720e1d88d28c80a" @@ -5744,7 +5766,7 @@ broccoli-file-creator@^1.1.1: broccoli-plugin "^1.1.0" mkdirp "^0.5.1" -broccoli-file-creator@^2.1.1: +broccoli-file-creator@^2.0.0, broccoli-file-creator@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/broccoli-file-creator/-/broccoli-file-creator-2.1.1.tgz#7351dd2496c762cfce7736ce9b49e3fce0c7b7db" integrity sha512-YpjOExWr92C5vhnK0kmD81kM7U09kdIRZk9w4ZDCDHuHXW+VE/x6AGEOQQW3loBQQ6Jk+k+TSm8dESy4uZsnjw== @@ -5816,7 +5838,7 @@ broccoli-funnel@^1.0.1: symlink-or-copy "^1.0.0" walk-sync "^0.3.1" -broccoli-funnel@^2.0.0, broccoli-funnel@^2.0.1, broccoli-funnel@^2.0.2, broccoli-funnel@~2.0.2: +"broccoli-funnel@^1.2.0 || ^2.0.0", broccoli-funnel@^2.0.0, broccoli-funnel@^2.0.1, broccoli-funnel@^2.0.2, broccoli-funnel@~2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/broccoli-funnel/-/broccoli-funnel-2.0.2.tgz#0edf629569bc10bd02cc525f74b9a38e71366a75" integrity sha512-/vDTqtv7ipjEZQOVqO4vGDVAOZyuYzQ/EgGoyewfOgh1M7IQAToBKZI0oAQPgMBeFPPlIbfMuAngk+ohPBuaHQ== @@ -5896,7 +5918,7 @@ broccoli-merge-trees@^2.0.0: broccoli-plugin "^1.3.0" merge-trees "^1.0.1" -broccoli-merge-trees@^3.0.0, broccoli-merge-trees@^3.0.1, broccoli-merge-trees@^3.0.2, broccoli-merge-trees@~3.0.2: +"broccoli-merge-trees@^2.0.0 || ^3.0.0", broccoli-merge-trees@^3.0.0, broccoli-merge-trees@^3.0.1, broccoli-merge-trees@^3.0.2, broccoli-merge-trees@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-3.0.2.tgz#f33b451994225522b5c9bcf27d59decfd8ba537d" integrity sha512-ZyPAwrOdlCddduFbsMyyFzJUrvW6b04pMvDiAQZrCwghlvgowJDY+EfoXn+eR1RRA5nmGHJ+B68T63VnpRiT1A== @@ -8329,7 +8351,7 @@ ember-cli-babel-plugin-helpers@^1.0.0, ember-cli-babel-plugin-helpers@^1.1.0: resolved "https://registry.yarnpkg.com/ember-cli-babel-plugin-helpers/-/ember-cli-babel-plugin-helpers-1.1.0.tgz#de3baedd093163b6c2461f95964888c1676325ac" integrity sha512-Zr4my8Xn+CzO0gIuFNXji0eTRml5AxZUTDQz/wsNJ5AJAtyFWCY4QtKdoELNNbiCVGt1lq5yLiwTm4scGKu6xA== -ember-cli-babel@7: +ember-cli-babel@7, ember-cli-babel@^7.1.0: version "7.21.0" resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.21.0.tgz#c79e888876aee87dfc3260aee7cb580b74264bbc" integrity sha512-jHVi9melAibo0DrAG3GAxid+29xEyjBoU53652B4qcu3Xp58feZGTH/JGXovH7TjvbeNn65zgNyoV3bk1onULw== @@ -8901,6 +8923,14 @@ ember-compatibility-helpers@^1.1.1, ember-compatibility-helpers@^1.1.2, ember-co ember-cli-version-checker "^2.1.1" semver "^5.4.1" +ember-cookies@^0.5.0, ember-cookies@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/ember-cookies/-/ember-cookies-0.5.2.tgz#06e33463f2f83254fefaf224cc944dec3fb9d3ba" + integrity sha512-nZ7oG97kBcO9UHjO95ryABpnVx62Bhxo7lIsBJNmWvFXleILm9DGueiAynzXxuYWWPuKIeeSbYakrS1869tNTw== + dependencies: + ember-cli-babel "^7.1.0" + ember-getowner-polyfill "^1.1.0 || ^2.0.0" + ember-copy@1.0.0, ember-copy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/ember-copy/-/ember-copy-1.0.0.tgz#426554ba6cf65920f31d24d0a3ca2cb1be16e4aa" @@ -9031,6 +9061,25 @@ ember-fetch@^7.0.0: node-fetch "^2.6.0" whatwg-fetch "^3.0.0" +ember-fetch@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ember-fetch/-/ember-fetch-8.0.1.tgz#dc57f3c3ba464e4a8722785f6b44265c46b38020" + integrity sha512-J+knuBFQJlMQUuij2tziMiVcJ+Ss0lCV2XiCQO2cHmG2PUsfvLlTGZ04uUyOXPqrJISxY2W3Et43fQBg4jy7SA== + dependencies: + abortcontroller-polyfill "^1.4.0" + broccoli-concat "^4.2.4" + broccoli-debug "^0.6.5" + broccoli-merge-trees "^4.2.0" + broccoli-rollup "^2.1.1" + broccoli-stew "^3.0.0" + broccoli-templater "^2.0.1" + calculate-cache-key-for-tree "^2.0.0" + caniuse-api "^3.0.0" + ember-cli-babel "^7.13.2" + ember-cli-typescript "^3.1.3" + node-fetch "^2.6.0" + whatwg-fetch "^3.0.0" + ember-get-config@0.2.4, ember-get-config@^0.2.2, ember-get-config@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/ember-get-config/-/ember-get-config-0.2.4.tgz#118492a2a03d73e46004ed777928942021fe1ecd" @@ -9039,7 +9088,7 @@ ember-get-config@0.2.4, ember-get-config@^0.2.2, ember-get-config@^0.2.4: broccoli-file-creator "^1.1.1" ember-cli-babel "^6.3.0" -ember-getowner-polyfill@^2.0.0: +"ember-getowner-polyfill@^1.1.0 || ^2.0.0", ember-getowner-polyfill@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/ember-getowner-polyfill/-/ember-getowner-polyfill-2.2.0.tgz#38e7dccbcac69d5ec694000329ec0b2be651d2b2" integrity sha512-rwGMJgbGzxIAiWYjdpAh04Abvt0s3HuS/VjHzUFhVyVg2pzAuz45B9AzOxYXzkp88vFC7FPaiA4kE8NxNk4A4Q== @@ -9247,6 +9296,20 @@ ember-router-generator@^2.0.0: "@babel/traverse" "^7.4.5" recast "^0.18.1" +ember-simple-auth@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ember-simple-auth/-/ember-simple-auth-3.0.0.tgz#d994f512cda1a8a5a6ef5a853fac60c0c0d6acd6" + integrity sha512-JaEw5W/Z9+HIH9I2BmE/JD4KofH4rp79Y51zJpt3UQI1OwWFOzFa+ulN0rK/DVLpGOz7uL7hBIE/rtEcEN1eqA== + dependencies: + base-64 "^0.1.0" + broccoli-file-creator "^2.0.0" + broccoli-funnel "^1.2.0 || ^2.0.0" + broccoli-merge-trees "^2.0.0 || ^3.0.0" + ember-cli-babel "^7.12.0" + ember-cli-is-package-missing "^1.0.0" + ember-cookies "^0.5.0" + silent-error "^1.0.0" + ember-source-channel-url@^1.0.1: version "1.2.0" resolved "https://registry.yarnpkg.com/ember-source-channel-url/-/ember-source-channel-url-1.2.0.tgz#77eb9d0889e5f5370e6c70fcb2696c63ff4a34a1" @@ -10209,6 +10272,20 @@ fast-sourcemap-concat@^1.4.0: source-map-url "^0.3.0" sourcemap-validator "^1.1.0" +fast-sourcemap-concat@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-sourcemap-concat/-/fast-sourcemap-concat-2.1.0.tgz#12dd36bfc38c804093e4bd1de61dd6216f574211" + integrity sha512-L9uADEnnHOeF4U5Kc3gzEs3oFpNCFkiTJXvT+nKmR0zcFqHZJJbszWT7dv4t9558FJRGpCj8UxUpTgz2zwiIZA== + dependencies: + chalk "^2.0.0" + fs-extra "^5.0.0" + heimdalljs-logger "^0.1.9" + memory-streams "^0.1.3" + mkdirp "^0.5.0" + source-map "^0.4.2" + source-map-url "^0.3.0" + sourcemap-validator "^1.1.0" + fastq@^1.6.0: version "1.8.0" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" From c5280ca27cda7505c8b103314ed281a9b5a7cd67 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 24 Jul 2020 11:29:19 -0400 Subject: [PATCH 02/17] refactor: auth method is no longer passed via the payload, but is encoded in the path --- addons/auth/addon/authenticators/password.js | 3 --- addons/auth/tests/unit/authenticators/password-test.js | 8 +++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/addons/auth/addon/authenticators/password.js b/addons/auth/addon/authenticators/password.js index f4dc5668c..25ba4da1c 100644 --- a/addons/auth/addon/authenticators/password.js +++ b/addons/auth/addon/authenticators/password.js @@ -51,17 +51,14 @@ export default class PasswordAuthenticator extends BaseAuthenticator { * @param {object} creds * @param {string} creds.identification is sent to server as `name` * @param {string} creds.password - * @param {string} authMethodId ID of the auth method to use * @param {boolean} requestCookies request cookie tokens (default `true`) * @return {Promise} */ async authenticate( { identification: name, password }, - authMethodID, requestCookies = true ) { const body = JSON.stringify({ - auth_method_id: authMethodID, token_type: requestCookies ? 'cookie' : null, credentials: { name, password }, }); diff --git a/addons/auth/tests/unit/authenticators/password-test.js b/addons/auth/tests/unit/authenticators/password-test.js index bf516dd43..5ab5d247a 100644 --- a/addons/auth/tests/unit/authenticators/password-test.js +++ b/addons/auth/tests/unit/authenticators/password-test.js @@ -15,7 +15,7 @@ module('Unit | Authenticator | password', function (hooks) { assert.ok(json.token_type, 'Requested token cookies by default'); return new Response(200); }); - await authenticator.authenticate({}, '').then(() => { + await authenticator.authenticate({}).then(() => { assert.ok(true, 'authentication succeeded'); }); }); @@ -28,7 +28,7 @@ module('Unit | Authenticator | password', function (hooks) { assert.notOk(json.token_type, 'Did not request tokens cookies'); return new Response(200); }); - await authenticator.authenticate({}, '', false).then(() => { + await authenticator.authenticate({}, false).then(() => { assert.ok(true, 'authentication succeeded'); }); }); @@ -39,7 +39,6 @@ module('Unit | Authenticator | password', function (hooks) { this.server.post(authenticator.authEndpoint, (schema, request) => { const json = JSON.parse(request.requestBody); assert.deepEqual(json, { - auth_method_id: '123', token_type: 'cookie', credentials: { name: 'foo', @@ -52,8 +51,7 @@ module('Unit | Authenticator | password', function (hooks) { identification: 'foo', password: 'bar', }; - const authMethodID = '123'; - await authenticator.authenticate(creds, authMethodID); + await authenticator.authenticate(creds); }); test('it rejects if the endpoint sends an error status code', async function (assert) { From 69b08ffcb061cdccb3eb083591894e961d8ab632 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 24 Jul 2020 14:37:50 -0400 Subject: [PATCH 03/17] refactor: auth/deauth URLs should be built via a method call rather than statically --- addons/auth/addon/authenticators/password.js | 23 +++++++++++-------- .../dummy/app/authenticators/password.js | 11 +++++++-- .../unit/authenticators/password-test.js | 12 +++++----- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/addons/auth/addon/authenticators/password.js b/addons/auth/addon/authenticators/password.js index 25ba4da1c..7c6d79a4e 100644 --- a/addons/auth/addon/authenticators/password.js +++ b/addons/auth/addon/authenticators/password.js @@ -7,16 +7,16 @@ import fetch from 'fetch'; * This authenticator is intended for use with Ember Simple Auth. * * Simple id/password base authenticator sends credentials to an endpoint - * specified in the `authEndpoint` URL. If the HTTP response code is in the + * specified by the `buildAuthEndpointURL`. If the HTTP response code is in the * success range, authentication resolves. Otherwise it rejects. * - * Upon session invalidation, deauthentication is attempted at the - * `deauthEndpoint`, but is not guaranteed. + * Upon session invalidation, deauthentication is attempted at the URL generated + * by `buildDeauthEndpointURL`, but is not guaranteed. * * This authenticator should not be used directly because it does not specify - * an `authEndpoint` or `deauthEndpoint` of its own. To use, generate an - * application authenticator in your app `authenticators/application.js` and - * extend this class. + * an `buildAuthEndpointURL` or `buildDeauthEndpointURL` of its own. + * To use, generate an application authenticator in your app + * `authenticators/application.js` and extend this class. * * @example * @@ -56,13 +56,15 @@ export default class PasswordAuthenticator extends BaseAuthenticator { */ async authenticate( { identification: name, password }, - requestCookies = true + requestCookies = true, + options ) { const body = JSON.stringify({ token_type: requestCookies ? 'cookie' : null, credentials: { name, password }, }); - const response = await fetch(this.authEndpoint, { method: 'post', body }); + const authEndpointURL = this.buildAuthEndpointURL(options); + const response = await fetch(authEndpointURL, { method: 'post', body }); const json = await response.json(); return response.status < 400 ? resolve(json) : reject(); } @@ -75,8 +77,9 @@ export default class PasswordAuthenticator extends BaseAuthenticator { * @override * @return {Promise} */ - invalidate() { - fetch(this.deauthEndpoint, { method: 'post' }).catch(() => { + invalidate(options) { + const deauthEndpointURL = this.buildDeauthEndpointURL(options); + fetch(deauthEndpointURL, { method: 'post' }).catch(() => { /* no op */ }); return super.invalidate(...arguments); diff --git a/addons/auth/tests/dummy/app/authenticators/password.js b/addons/auth/tests/dummy/app/authenticators/password.js index 2f312a7e4..e259039a3 100644 --- a/addons/auth/tests/dummy/app/authenticators/password.js +++ b/addons/auth/tests/dummy/app/authenticators/password.js @@ -1,6 +1,13 @@ import PasswordAuthenticator from 'auth/authenticators/password'; export default class ApplicationPasswordAuthenticator extends PasswordAuthenticator { - authEndpoint = '/authenticate'; - deauthEndpoint = '/deauthenticate'; + + buildAuthEndpointURL() { + return '/authenticate'; + } + + buildDeauthEndpointURL() { + return '/deauthenticate'; + } + } diff --git a/addons/auth/tests/unit/authenticators/password-test.js b/addons/auth/tests/unit/authenticators/password-test.js index 5ab5d247a..aebdfee56 100644 --- a/addons/auth/tests/unit/authenticators/password-test.js +++ b/addons/auth/tests/unit/authenticators/password-test.js @@ -10,7 +10,7 @@ module('Unit | Authenticator | password', function (hooks) { test('it authenticates to the specified authEndpoint', async function (assert) { assert.expect(2); const authenticator = this.owner.lookup('authenticator:password'); - this.server.post(authenticator.authEndpoint, (schema, request) => { + this.server.post(authenticator.buildAuthEndpointURL(), (schema, request) => { const json = JSON.parse(request.requestBody); assert.ok(json.token_type, 'Requested token cookies by default'); return new Response(200); @@ -23,7 +23,7 @@ module('Unit | Authenticator | password', function (hooks) { test('it can authenticate without requesting cookies', async function (assert) { assert.expect(2); const authenticator = this.owner.lookup('authenticator:password'); - this.server.post(authenticator.authEndpoint, (schema, request) => { + this.server.post(authenticator.buildAuthEndpointURL(), (schema, request) => { const json = JSON.parse(request.requestBody); assert.notOk(json.token_type, 'Did not request tokens cookies'); return new Response(200); @@ -36,7 +36,7 @@ module('Unit | Authenticator | password', function (hooks) { test('it authenticates with the expected payload', async function (assert) { assert.expect(1); const authenticator = this.owner.lookup('authenticator:password'); - this.server.post(authenticator.authEndpoint, (schema, request) => { + this.server.post(authenticator.buildAuthEndpointURL(), (schema, request) => { const json = JSON.parse(request.requestBody); assert.deepEqual(json, { token_type: 'cookie', @@ -57,7 +57,7 @@ module('Unit | Authenticator | password', function (hooks) { test('it rejects if the endpoint sends an error status code', async function (assert) { assert.expect(1); const authenticator = this.owner.lookup('authenticator:password'); - this.server.post(authenticator.authEndpoint, () => { + this.server.post(authenticator.buildAuthEndpointURL(), () => { return new Response(400); }); await authenticator.authenticate({}).catch(() => { @@ -68,7 +68,7 @@ module('Unit | Authenticator | password', function (hooks) { test('it deauthenticates on invalidation', async function (assert) { assert.expect(1); const authenticator = this.owner.lookup('authenticator:password'); - this.server.post(authenticator.deauthEndpoint, () => { + this.server.post(authenticator.buildDeauthEndpointURL(), () => { assert.ok(true, 'deauthentication occurred'); return new Response(200); }); @@ -78,7 +78,7 @@ module('Unit | Authenticator | password', function (hooks) { test('it invalidation succeeds even if the deauthentication request fails', async function (assert) { assert.expect(2); const authenticator = this.owner.lookup('authenticator:password'); - this.server.post(authenticator.deauthEndpoint, () => { + this.server.post(authenticator.buildDeauthEndpointURL(), () => { assert.ok(true, 'deauthentication was requested'); return new Response(500); }); From dba62d81616b8f50950e9755217ad4960ad13c3c Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 24 Jul 2020 16:11:26 -0400 Subject: [PATCH 04/17] chore: add initial simple auth integration with updated mocks --- ui/core/app/authenticators/password.js | 47 ++++++++++++++ ui/core/app/session-stores/application.js | 64 +++++++++++++++++++ ui/core/mirage/scenarios/default.js | 10 ++- ui/core/package.json | 2 + .../tests/unit/session-stores/application.js | 53 +++++++++++++++ 5 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 ui/core/app/authenticators/password.js create mode 100644 ui/core/app/session-stores/application.js create mode 100644 ui/core/tests/unit/session-stores/application.js diff --git a/ui/core/app/authenticators/password.js b/ui/core/app/authenticators/password.js new file mode 100644 index 000000000..041d5b9dc --- /dev/null +++ b/ui/core/app/authenticators/password.js @@ -0,0 +1,47 @@ +import BasePasswordAuthenticator from 'auth/authenticators/password'; +import { inject as service } from '@ember/service'; + +/** + * A username/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 PasswordAuthenticator extends BasePasswordAuthenticator { + + // =services + + @service store; + + // =attributes + + /** + * Generates an auth method URL with which to authenticate. + * @override + * @param {object} options + * @param {string} options.scope.scope_id + * @param {string} options.authMethod.id + * @return {string} + */ + buildAuthEndpointURL({ + scope: { scope_id: scopeID }, + authMethod: { id: authMethodID } + }) { + const adapter = this.store.adapterFor('application'); + const options = { adapterOptions: { scopeID, method: 'authenticate' } }; + return adapter.buildURL('auth-method', authMethodID, options, 'findRecord'); + } + + /** + * Generates a scope URL with which to deauthenticate. + * @override + * @param {object} options + * @param {string} scopeID + * @return {string} + */ + buildDeauthEndpointURL({ scope: { scope_id: scopeID } }) { + const adapter = this.store.adapterFor('application'); + const options = { adapterOptions: { method: 'deauthenticate' } }; + return adapter.buildURL('scope', scopeID, options, 'findRecord'); + } + +} diff --git a/ui/core/app/session-stores/application.js b/ui/core/app/session-stores/application.js new file mode 100644 index 000000000..555804f12 --- /dev/null +++ b/ui/core/app/session-stores/application.js @@ -0,0 +1,64 @@ +import LocalStorageSessionStore from 'auth/session-stores/local-storage'; +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; +//import config from '../config/environment'; + +export default class ApplicationSessionStore extends LocalStorageSessionStore { + + @service store; + + /** + * If the auth sessionData includes a token, apply it as a bearer token in the + * application adapter's Authorization header. Otherwise clear the header. + * @param {object} sessionData + * @return {object} + */ + persist(sessionData) { + this.addTokenToAuthorization(sessionData); + this.addScopeToSession(sessionData); + return super.persist(...arguments); + } + + /** + * If the session has a full token, it is added as a bearer token to the + * `Authorization` header for all API requests. + * @param {object} sessionData + */ + addTokenToAuthorization(sessionData) { + const token = get(sessionData, 'authenticated.token'); + const adapter = this.store.adapterFor('application'); + const headers = get(adapter, 'headers'); + if (!headers) adapter.headers = {}; + adapter.headers.Authorization = null; + if (token) adapter.headers.Authorization = `Bearer ${token}`; + } + + /** + * @param {object} sessionData + */ + addScopeToSession(sessionData) { + const scopeID = get(this.scope, 'scope.id'); + const authenticated = get(sessionData, 'authenticated'); + if (scopeID && authenticated) authenticated.scopeID = scopeID; + } + +} + +// import CookieSessionStore from 'auth/session-stores/cookie'; +// import config from '../config/environment'; +// +// export default class ApplicationSessionStore extends CookieSessionStore { +// +// // =attributes +// +// /** +// * @type {string} +// */ +// cookiePath = config.rootURL; +// +// /** +// * @type {string} +// */ +// cookieName = config.auth.passwordCookieName; +// +// } diff --git a/ui/core/mirage/scenarios/default.js b/ui/core/mirage/scenarios/default.js index f143f6ec6..9b498425a 100644 --- a/ui/core/mirage/scenarios/default.js +++ b/ui/core/mirage/scenarios/default.js @@ -2,14 +2,20 @@ export default function(server) { // Scope resources - server.create('scope', null, 'withChildren'); + const globalScope = server.create('scope', { id: 'global' }); // creates a global scope + const orgScope = server.create('scope', { + type: 'org', + scope: { id: globalScope.id, type: globalScope.type } + }, 'withChildren'); + + const scope = { id: orgScope.id, type: orgScope.type }; // Auth // Auth methods exist both at the global scope and the org scope. // For simplicity we don't scope our mock auth methods and just return the // same items for request at all scopes. - server.createList('auth-method', 3); + server.createList('auth-method', 3, { scope }); // User server.createList('user', 5); diff --git a/ui/core/package.json b/ui/core/package.json index f6ae93043..37cb246e5 100644 --- a/ui/core/package.json +++ b/ui/core/package.json @@ -32,6 +32,7 @@ "@glimmer/component": "^1.0.0", "@glimmer/tracking": "^1.0.0", "api": "*", + "auth": "*", "babel-eslint": "^10.0.3", "babel-loader": "^8.1.0", "broccoli-asset-rev": "^3.0.0", @@ -68,6 +69,7 @@ "ember-qunit": "^4.6.0", "ember-resolver": "^7.0.0", "ember-route-action-helper": "^2.0.8", + "ember-simple-auth": "^3.0.0", "ember-source": "~3.16.0", "ember-uuid": "^2.1.0", "ember-welcome-page": "^4.0.0", diff --git a/ui/core/tests/unit/session-stores/application.js b/ui/core/tests/unit/session-stores/application.js new file mode 100644 index 000000000..dd7132709 --- /dev/null +++ b/ui/core/tests/unit/session-stores/application.js @@ -0,0 +1,53 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Session Store | application', function (hooks) { + setupTest(hooks); + + let store; + let applicationAdapter; + let sessionStore; + + hooks.beforeEach(function () { + store = this.owner.lookup('service:store'); + applicationAdapter = store.adapterFor('application'); + sessionStore = this.owner.lookup('session-store:application'); + sessionStore.clear(); + }); + + hooks.afterEach(function () { + sessionStore.clear(); + }); + + test('it does not add an authorization header to application adapter on persist if no token is present', async function (assert) { + assert.expect(1); + sessionStore.persist({ authenticated: { token: null } }); + assert.notOk(applicationAdapter.headers.Authorization); + }); + + test('it adds an authorization header to application adapter on persist', async function (assert) { + assert.expect(1); + sessionStore.persist({ authenticated: { token: 'token1234' } }); + assert.equal(applicationAdapter.headers.Authorization, 'Bearer token1234'); + }); + + test('it adds an authorization header to application adapter which already has headers on persist', async function (assert) { + assert.expect(2); + applicationAdapter.headers = { foo: 'bar' }; + sessionStore.persist({ authenticated: { token: 'token1234' } }); + assert.equal(applicationAdapter.headers.foo, 'bar'); + assert.equal(applicationAdapter.headers.Authorization, 'Bearer token1234'); + }); + + test('it adds an org_id to the session data if authenticating within a scope', async function (assert) { + assert.expect(2); + let sessionData = {}; + sessionStore.set('scope', {}); + sessionStore.persist(sessionData); + assert.notOk(sessionData.authenticated, 'There is no authenticated key'); + sessionData = { authenticated: {} }; + sessionStore.set('scope', { org: { id: 1 } }); + sessionStore.persist(sessionData); + assert.equal(sessionData.authenticated.org_id, 1); + }); +}); From 99b529057060bb470ade2aee007adf373a3e5d73 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 24 Jul 2020 17:05:55 -0400 Subject: [PATCH 05/17] chore: add core authentication flow with acceptance tests --- ui/core/app/authenticators/password.js | 4 +- ui/core/app/router.js | 3 + ui/core/app/routes/scopes.js | 20 ++++++ ui/core/app/routes/scopes/scope.js | 17 +++-- .../app/routes/scopes/scope/authenticate.js | 26 ++++++++ .../scopes/scope/authenticate/method.js | 63 +++++++++++++++++++ ui/core/app/session-stores/application.js | 4 +- .../templates/scopes/scope/authenticate.hbs | 1 + .../scopes/scope/authenticate/method.hbs | 26 ++++++++ ui/core/mirage/config.js | 4 ++ ui/core/mirage/route-handlers/auth.js | 60 ++++++++++++++++++ .../tests/acceptance/authentication-test.js | 53 ++++++++++++++++ .../routes/scopes/scope/authenticate-test.js | 11 ++++ .../scopes/scope/authenticate/method-test.js | 11 ++++ 14 files changed, 290 insertions(+), 13 deletions(-) create mode 100644 ui/core/app/routes/scopes/scope/authenticate.js create mode 100644 ui/core/app/routes/scopes/scope/authenticate/method.js create mode 100644 ui/core/app/templates/scopes/scope/authenticate.hbs create mode 100644 ui/core/app/templates/scopes/scope/authenticate/method.hbs create mode 100644 ui/core/mirage/route-handlers/auth.js create mode 100644 ui/core/tests/acceptance/authentication-test.js create mode 100644 ui/core/tests/unit/routes/scopes/scope/authenticate-test.js create mode 100644 ui/core/tests/unit/routes/scopes/scope/authenticate/method-test.js diff --git a/ui/core/app/authenticators/password.js b/ui/core/app/authenticators/password.js index 041d5b9dc..3224130a3 100644 --- a/ui/core/app/authenticators/password.js +++ b/ui/core/app/authenticators/password.js @@ -23,7 +23,7 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator { * @return {string} */ buildAuthEndpointURL({ - scope: { scope_id: scopeID }, + scope: { id: scopeID }, authMethod: { id: authMethodID } }) { const adapter = this.store.adapterFor('application'); @@ -38,7 +38,7 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator { * @param {string} scopeID * @return {string} */ - buildDeauthEndpointURL({ scope: { scope_id: scopeID } }) { + buildDeauthEndpointURL({ scope: { id: scopeID } }) { const adapter = this.store.adapterFor('application'); const options = { adapterOptions: { method: 'deauthenticate' } }; return adapter.buildURL('scope', scopeID, options, 'findRecord'); diff --git a/ui/core/app/router.js b/ui/core/app/router.js index e07a9ee94..ef3536c60 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -10,6 +10,9 @@ Router.map(function () { this.route('scopes', function() { this.route('scope', { path: ':scope_id' }, function() { + this.route('authenticate', function() { + this.route('method', { path: ':auth_method_id'}); + }); this.route('projects', function() { this.route('project', { path: ':project_id' }, function() {}); this.route('new'); diff --git a/ui/core/app/routes/scopes.js b/ui/core/app/routes/scopes.js index 5fff74b2e..a849878ba 100644 --- a/ui/core/app/routes/scopes.js +++ b/ui/core/app/routes/scopes.js @@ -1,4 +1,24 @@ import Route from '@ember/routing/route'; +import { A } from '@ember/array'; export default class ScopesRoute extends Route { + + // =methods + + /** + * Attempt to load the specified scope from the API. This is allowed + * to fail, since in some cases the user may not have permission to read a + * scope directly, but may have permission to read resources under it. + * If the scope fails to load, we still proceed using a temporary scope object + * consisting of only the specified ID. + * @param {object} params + * @param {string} params.scope_id + * @return {Promise{ScopeModel}} + */ + model() { + // NOTE: In the absence of a `scope_id` query parameter, this endpoint is + // expected to default to the global scope, thus returning org scopes. + return this.store.findAll('scope').catch(() => A([])); + } + } diff --git a/ui/core/app/routes/scopes/scope.js b/ui/core/app/routes/scopes/scope.js index 8cde487bd..c9ecb358f 100644 --- a/ui/core/app/routes/scopes/scope.js +++ b/ui/core/app/routes/scopes/scope.js @@ -14,15 +14,14 @@ export default class ScopesScopeRoute extends Route { * @param {string} params.scope_id * @return {Promise{ScopeModel}} */ - async model({ scope_id: id }) { - try { - return await this.store.findRecord('scope', id); - } catch { - const maybeExistingScope = this.store.peekRecord('scope', id); - // TODO it's unclear if creating a temporary unsaved scope is reliable... - // we may need to revert to a POJO. - return maybeExistingScope || this.store.createRecord('scope', { id }); - } + model({ scope_id: id }) { + return this.store.findRecord('scope', id) + .catch(() => { + const maybeExistingScope = this.store.peekRecord('scope', id); + // TODO it's unclear if creating a temporary unsaved scope is reliable + // we may need to revert to a POJO. + return maybeExistingScope || this.store.createRecord('scope', { id }); + }); } } diff --git a/ui/core/app/routes/scopes/scope/authenticate.js b/ui/core/app/routes/scopes/scope/authenticate.js new file mode 100644 index 000000000..3ae8edbcf --- /dev/null +++ b/ui/core/app/routes/scopes/scope/authenticate.js @@ -0,0 +1,26 @@ +import Route from '@ember/routing/route'; +import { hash } from 'rsvp'; + +export default class ScopesScopeAuthenticateRoute extends Route { + + /** + * Returns all auth methods for the current scope, along with the current + * scope and all scopes (for org navigation). + * @return {Promise} `{scope, scopes, authMethods}` + */ + model() { + const adapterOptions = { scopeID: this.modelFor('scopes.scope').id }; + return hash({ + scope: this.modelFor('scopes.scope'), + scopes: this.modelFor('scopes'), + authMethods: this.store.findAll('auth-method', { adapterOptions }), + + // for integration testing: + // authMethods: A([{ + // id: 'am_1234567890', + // type: 'password' + // }]) + }); + } + +} diff --git a/ui/core/app/routes/scopes/scope/authenticate/method.js b/ui/core/app/routes/scopes/scope/authenticate/method.js new file mode 100644 index 000000000..c4ce43a11 --- /dev/null +++ b/ui/core/app/routes/scopes/scope/authenticate/method.js @@ -0,0 +1,63 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +export default class ScopesScopeAuthenticateMethodRoute extends Route { + + // =services + + @service session; + @service notify; + @service intl; + + // =methods + + /** + * Returns an auth method by id. + * @param {object} params + * @return {Promise{AuthMethodModel}} + */ + model({ auth_method_id: id }) { + const adapterOptions = { scopeID: this.modelFor('scopes.scope').id }; + return this.store.findRecord('auth-method', id, adapterOptions); + } + + /** + * Resets the password field on the controller so it is not + * inadvertantly retained in memory. + */ + resetCredentials() { + this.controller.setProperties({ + password: null, + }); + } + + // =actions + + /** + * Delegates authentication to the `password` authenticator. + * If authentication fails, notifies with an alert. + */ + @action + async authenticate(creds) { + this.resetCredentials(); + 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( + authenticatorName, + creds, + requestCookies, + { scope, authMethod } + ); + this.transitionTo('scopes.scope.projects'); + } catch (e) { + const errorMessage = this.intl.t('errors.titles.authentication-failed'); + this.notify.error(errorMessage, { closeAfter: null }); + } + } + +} diff --git a/ui/core/app/session-stores/application.js b/ui/core/app/session-stores/application.js index 555804f12..80d0603db 100644 --- a/ui/core/app/session-stores/application.js +++ b/ui/core/app/session-stores/application.js @@ -37,9 +37,9 @@ export default class ApplicationSessionStore extends LocalStorageSessionStore { * @param {object} sessionData */ addScopeToSession(sessionData) { - const scopeID = get(this.scope, 'scope.id'); + const scope = get(this, 'scope'); const authenticated = get(sessionData, 'authenticated'); - if (scopeID && authenticated) authenticated.scopeID = scopeID; + if (scope && authenticated) authenticated.scope = scope; } } diff --git a/ui/core/app/templates/scopes/scope/authenticate.hbs b/ui/core/app/templates/scopes/scope/authenticate.hbs new file mode 100644 index 000000000..e2147cab0 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/authenticate.hbs @@ -0,0 +1 @@ +{{outlet}} \ No newline at end of file diff --git a/ui/core/app/templates/scopes/scope/authenticate/method.hbs b/ui/core/app/templates/scopes/scope/authenticate/method.hbs new file mode 100644 index 000000000..60057d773 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/authenticate/method.hbs @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index ed6859c83..aa0b6e14a 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -1,4 +1,5 @@ import config from '../config/environment'; +import { authHandler } from './route-handlers/auth'; import { Response } from 'miragejs'; export default function() { @@ -55,6 +56,9 @@ export default function() { this.patch('/scopes/:scope_id/auth-methods/:id'); this.del('/scopes/:scope_id/auth-methods/:id'); + // Authenticate/deauthenticate route + this.post('/scopes/:scope_id/auth-methods/:id_method', authHandler); + // IAM : Users this.get('/scopes/:scope_id/users'); this.post('/scopes/:scope_id/users'); diff --git a/ui/core/mirage/route-handlers/auth.js b/ui/core/mirage/route-handlers/auth.js new file mode 100644 index 000000000..8bedb771c --- /dev/null +++ b/ui/core/mirage/route-handlers/auth.js @@ -0,0 +1,60 @@ +// import config from '../../config/environment'; +// import { v1 } from 'ember-uuid'; +import { Response } from 'miragejs'; + +// /** +// * Simulates a cookie-based response to authentication requests. Any user/pass +// * values are allowed *except* when `username` equals `error`, in which case a +// * simulated error response is returned. +// * @param {object} schema +// * @param {object} request +// * @return {Response} +// */ +// export default function authenticateHandler(schema, request) { +// const payload = JSON.parse(request.requestBody); +// if (payload.credentials.name === 'error') { +// return new Response(400); +// } else { +// const cookieName = config.auth.passwordCookieName; +// const cookieValue = v1(); +// const cookiePath = config.rootURL; +// document.cookie = `${cookieName}=${cookieValue};path=${cookiePath}`; +// return new Response(200); +// } +// } + +/** + * 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: '' + }); + } + } else { + // this is a deauth request + return new Response(200); + } +} diff --git a/ui/core/tests/acceptance/authentication-test.js b/ui/core/tests/acceptance/authentication-test.js new file mode 100644 index 000000000..89c9b842b --- /dev/null +++ b/ui/core/tests/acceptance/authentication-test.js @@ -0,0 +1,53 @@ +import { module, test } from 'qunit'; +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'; + +module('Acceptance | authentication', function(hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + let orgScope; + let orgScopeID; + let authMethod; + let authMethodID; + let authMethodAuthenticateURL; + let projectsURL; + + 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 }); + orgScopeID = orgScope.id; + authMethodID = authMethod.id; + authMethodAuthenticateURL = `/scopes/${orgScopeID}/authenticate/${authMethodID}`; + projectsURL = `/scopes/${orgScopeID}/projects`; + }); + + test('visiting auth method authenticate route', async function(assert) { + assert.expect(1); + await visit(authMethodAuthenticateURL); + await a11yAudit(); + assert.equal(currentURL(), authMethodAuthenticateURL); + }); + + test('failed authentication shows a notification message', async function(assert) { + assert.expect(1); + await visit(authMethodAuthenticateURL); + await fillIn('[name="identification"]', 'error'); + await click('[type="submit"]'); + assert.ok(find('.rose-notification.is-error')); + }); + + test('successful authentication redirects to projects', async function(assert) { + assert.expect(2); + await visit(authMethodAuthenticateURL); + assert.equal(currentURL(), authMethodAuthenticateURL); + await fillIn('[name="identification"]', 'test'); + await fillIn('[name="password"]', 'test'); + await click('[type="submit"]'); + assert.equal(currentURL(), projectsURL); + }); + +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/authenticate-test.js b/ui/core/tests/unit/routes/scopes/scope/authenticate-test.js new file mode 100644 index 000000000..2ee818453 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/authenticate-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/authenticate', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/authenticate'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/authenticate/method-test.js b/ui/core/tests/unit/routes/scopes/scope/authenticate/method-test.js new file mode 100644 index 000000000..5aed2aefe --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/authenticate/method-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/authenticate/method', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/authenticate/method'); + assert.ok(route); + }); +}); From 659a49409206768ffa6411e5fcba7ec4bc289ad0 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 24 Jul 2020 17:49:42 -0400 Subject: [PATCH 06/17] chore: add core deauthenticate flow --- ui/core/app/controllers/application.js | 10 +++ ui/core/app/routes/application.js | 64 ++++++++++++++++++- .../scopes/scope/authenticate/method.js | 1 - ui/core/app/templates/application.hbs | 28 +++++--- ui/core/mirage/config.js | 5 +- ui/core/mirage/route-handlers/auth.js | 50 ++++++--------- .../tests/acceptance/authentication-test.js | 30 +++++++-- .../unit/controllers/application-test.js | 12 ++++ 8 files changed, 152 insertions(+), 48 deletions(-) create mode 100644 ui/core/app/controllers/application.js create mode 100644 ui/core/tests/unit/controllers/application-test.js 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); + }); +}); From 6c1375fe16bf5908cef4d8c8a67181d699045d6c Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 24 Jul 2020 18:08:37 -0400 Subject: [PATCH 07/17] chore: setup auth redirection logic --- .../app/routes/scopes/scope/authenticate.js | 17 ++++++++++++++++ .../routes/scopes/scope/authenticate/index.js | 20 +++++++++++++++++++ .../scopes/scope/authenticate/method.js | 6 +++++- .../scopes/scope/authenticate/index.hbs | 1 + .../tests/acceptance/authentication-test.js | 20 ++++++++++++++++++- .../scopes/scope/authenticate/index-test.js | 11 ++++++++++ 6 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 ui/core/app/routes/scopes/scope/authenticate/index.js create mode 100644 ui/core/app/templates/scopes/scope/authenticate/index.hbs create mode 100644 ui/core/tests/unit/routes/scopes/scope/authenticate/index-test.js diff --git a/ui/core/app/routes/scopes/scope/authenticate.js b/ui/core/app/routes/scopes/scope/authenticate.js index 3ae8edbcf..42d238ffd 100644 --- a/ui/core/app/routes/scopes/scope/authenticate.js +++ b/ui/core/app/routes/scopes/scope/authenticate.js @@ -1,8 +1,19 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; import { hash } from 'rsvp'; export default class ScopesScopeAuthenticateRoute extends Route { + // =services + + @service session; + + // =attributes + + routeIfAlreadyAuthenticated = 'scopes.scope.projects'; + + // =methods + /** * Returns all auth methods for the current scope, along with the current * scope and all scopes (for org navigation). @@ -23,4 +34,10 @@ export default class ScopesScopeAuthenticateRoute extends Route { }); } + redirect() { + if (this.session.isAuthenticated) { + this.transitionTo(this.routeIfAlreadyAuthenticated); + } + } + } diff --git a/ui/core/app/routes/scopes/scope/authenticate/index.js b/ui/core/app/routes/scopes/scope/authenticate/index.js new file mode 100644 index 000000000..5ee523b46 --- /dev/null +++ b/ui/core/app/routes/scopes/scope/authenticate/index.js @@ -0,0 +1,20 @@ +import Route from '@ember/routing/route'; + +export default class ScopesScopeAuthenticateIndexRoute extends Route { + + // =methods + + /** + * This index route exists solely to redirect the user on to the + * first auth method. + * @param {object} model + * @param {[AuthMethodModel]} model.authMethods + */ + redirect({ authMethods }) { + const firstAuthMethod = authMethods.firstObject; + if (firstAuthMethod) { + this.transitionTo('scopes.scope.authenticate.method', firstAuthMethod); + } + } + +} diff --git a/ui/core/app/routes/scopes/scope/authenticate/method.js b/ui/core/app/routes/scopes/scope/authenticate/method.js index 29014e819..b52406722 100644 --- a/ui/core/app/routes/scopes/scope/authenticate/method.js +++ b/ui/core/app/routes/scopes/scope/authenticate/method.js @@ -10,6 +10,10 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route { @service notify; @service intl; + // =attributes + + routeAfterAuthentication = 'scopes.scope.projects'; + // =methods /** @@ -52,7 +56,7 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route { requestCookies, { scope, authMethod } ); - this.transitionTo('scopes.scope.projects'); + this.transitionTo(this.routeAfterAuthentication); } catch (e) { const errorMessage = this.intl.t('errors.titles.authentication-failed'); this.notify.error(errorMessage, { closeAfter: null }); diff --git a/ui/core/app/templates/scopes/scope/authenticate/index.hbs b/ui/core/app/templates/scopes/scope/authenticate/index.hbs new file mode 100644 index 000000000..e2147cab0 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/authenticate/index.hbs @@ -0,0 +1 @@ +{{outlet}} \ No newline at end of file diff --git a/ui/core/tests/acceptance/authentication-test.js b/ui/core/tests/acceptance/authentication-test.js index bd5c2a1a5..e71c1a3b0 100644 --- a/ui/core/tests/acceptance/authentication-test.js +++ b/ui/core/tests/acceptance/authentication-test.js @@ -5,7 +5,7 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { currentSession, - //authenticateSession, + authenticateSession, //invalidateSession, } from 'ember-simple-auth/test-support'; @@ -18,6 +18,7 @@ module('Acceptance | authentication', function(hooks) { let scope; let authMethod; let authMethodID; + let authenticateURL; let authMethodAuthenticateURL; let projectsURL; @@ -27,10 +28,18 @@ module('Acceptance | authentication', function(hooks) { authMethod = this.server.create('auth-method', { scope }); orgScopeID = orgScope.id; authMethodID = authMethod.id; + authenticateURL = `/scopes/${orgScopeID}/authenticate`; authMethodAuthenticateURL = `/scopes/${orgScopeID}/authenticate/${authMethodID}`; projectsURL = `/scopes/${orgScopeID}/projects`; }); + test('visiting auth methods authenticate route redirects to first auth method', async function(assert) { + assert.expect(1); + await visit(authenticateURL); + await a11yAudit(); + assert.equal(currentURL(), authMethodAuthenticateURL); + }); + test('visiting auth method authenticate route', async function(assert) { assert.expect(1); await visit(authMethodAuthenticateURL); @@ -38,6 +47,15 @@ module('Acceptance | authentication', function(hooks) { assert.equal(currentURL(), authMethodAuthenticateURL); }); + test('visiting any authenticate route while already authenticated redirects to projects', async function(assert) { + assert.expect(2); + authenticateSession(); + await visit(authenticateURL); + assert.equal(currentURL(), projectsURL); + await visit(authMethodAuthenticateURL); + assert.equal(currentURL(), projectsURL); + }); + test('failed authentication shows a notification message', async function(assert) { assert.expect(3); await visit(authMethodAuthenticateURL); diff --git a/ui/core/tests/unit/routes/scopes/scope/authenticate/index-test.js b/ui/core/tests/unit/routes/scopes/scope/authenticate/index-test.js new file mode 100644 index 000000000..88d5a1121 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/authenticate/index-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/authenticate/index', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/authenticate/index'); + assert.ok(route); + }); +}); From 4b1bb3a50881c755389376a4f66e244d8e4f95cf Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 24 Jul 2020 18:32:39 -0400 Subject: [PATCH 08/17] docs: add doc comment --- ui/core/app/routes/scopes/scope/authenticate.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/core/app/routes/scopes/scope/authenticate.js b/ui/core/app/routes/scopes/scope/authenticate.js index 42d238ffd..93f565baa 100644 --- a/ui/core/app/routes/scopes/scope/authenticate.js +++ b/ui/core/app/routes/scopes/scope/authenticate.js @@ -10,6 +10,8 @@ export default class ScopesScopeAuthenticateRoute extends Route { // =attributes + // TODO: this assumes we're in an org scope, but the "landing" + // for global scope is probably different. routeIfAlreadyAuthenticated = 'scopes.scope.projects'; // =methods From bbbe3e2819256d6534447c23f3d42cbfcf902c57 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 24 Jul 2020 19:12:20 -0400 Subject: [PATCH 09/17] chore: setup remaining auth redirect logic --- ui/core/app/router.js | 1 + ui/core/app/routes/application.js | 9 +--- ui/core/app/routes/index.js | 14 ++++++ ui/core/app/routes/scopes/index.js | 29 +++++++++++ ui/core/app/routes/scopes/scope/index.js | 14 ++++++ ui/core/app/routes/scopes/scope/projects.js | 8 +++ ui/core/app/templates/scopes/index.hbs | 1 + ui/core/app/templates/scopes/scope/index.hbs | 1 + ui/core/mirage/config.js | 17 ++----- .../tests/acceptance/authentication-test.js | 49 ++++++++++++++++--- ui/core/tests/unit/routes/index-test.js | 11 +++++ .../tests/unit/routes/scopes/index-test.js | 11 +++++ .../unit/routes/scopes/scope/index-test.js | 11 +++++ 13 files changed, 149 insertions(+), 27 deletions(-) create mode 100644 ui/core/app/routes/index.js create mode 100644 ui/core/app/routes/scopes/index.js create mode 100644 ui/core/app/routes/scopes/scope/index.js create mode 100644 ui/core/app/templates/scopes/index.hbs create mode 100644 ui/core/app/templates/scopes/scope/index.hbs create mode 100644 ui/core/tests/unit/routes/index-test.js create mode 100644 ui/core/tests/unit/routes/scopes/index-test.js create mode 100644 ui/core/tests/unit/routes/scopes/scope/index-test.js diff --git a/ui/core/app/router.js b/ui/core/app/router.js index ef3536c60..3c95ac355 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -18,6 +18,7 @@ Router.map(function () { this.route('new'); }); }); + this.route('cope'); }); this.route('orgs', function () { diff --git a/ui/core/app/routes/application.js b/ui/core/app/routes/application.js index be17ca092..024a0b887 100644 --- a/ui/core/app/routes/application.js +++ b/ui/core/app/routes/application.js @@ -36,16 +36,11 @@ export default class ApplicationRoute extends Route.extend( /** * 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); + // scope auth route when it redirects to the first auth method. + await this.transitionTo(this.routeIfUnauthenticated).catch(() => {}); // The Ember way of accessing globals... const document = getOwner(this).lookup('service:-document').documentElement; // defaultView === window, but without using globals directly diff --git a/ui/core/app/routes/index.js b/ui/core/app/routes/index.js new file mode 100644 index 000000000..4ec1a62b0 --- /dev/null +++ b/ui/core/app/routes/index.js @@ -0,0 +1,14 @@ +import Route from '@ember/routing/route'; + +export default class IndexRoute extends Route { + + // =methods + + /** + * Redirects to scopes route for further processing. + */ + redirect() { + this.transitionTo('scopes'); + } + +} diff --git a/ui/core/app/routes/scopes/index.js b/ui/core/app/routes/scopes/index.js new file mode 100644 index 000000000..06a263171 --- /dev/null +++ b/ui/core/app/routes/scopes/index.js @@ -0,0 +1,29 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { get } from '@ember/object'; + +export default class ScopesIndexRoute extends Route { + + // =services + + @service session; + + // =methods + + /** + * If authenticated, redirects to the scope route of the authenticted scope. + * If unauthenticated, redirects to the first scope that was loaded (if any). + * @param {[ScopeModel]} model + * @param {?ScopeModel} model.firstObject + */ + redirect({ firstObject }) { + const authenticatedScopeID = + get(this.session, 'data.authenticated.scope.id'); + if (authenticatedScopeID) { + this.transitionTo('scopes.scope', authenticatedScopeID); + } else if (firstObject) { + this.transitionTo('scopes.scope', firstObject.id); + } + } + +} diff --git a/ui/core/app/routes/scopes/scope/index.js b/ui/core/app/routes/scopes/scope/index.js new file mode 100644 index 000000000..0215ea9da --- /dev/null +++ b/ui/core/app/routes/scopes/scope/index.js @@ -0,0 +1,14 @@ +import Route from '@ember/routing/route'; + +export default class ScopesScopeIndexRoute extends Route { + + // =methods + + /** + * Redirects to scopes/scope/authenticate for further processing. + */ + redirect() { + this.transitionTo('scopes.scope.authenticate'); + } + +} diff --git a/ui/core/app/routes/scopes/scope/projects.js b/ui/core/app/routes/scopes/scope/projects.js index ebc5e97e5..a7ab28af6 100644 --- a/ui/core/app/routes/scopes/scope/projects.js +++ b/ui/core/app/routes/scopes/scope/projects.js @@ -7,9 +7,17 @@ export default class ScopesScopeProjectsRoute extends Route { // =services @service notify; + @service session; // =methods + /** + * If arriving here unauthenticated, redirect to index for further processing. + */ + beforeModel() { + if (!this.session.isAuthenticated) this.transitionTo('index'); + } + /** * Loads all scopes under the current scope. * @return {Promise{[ScopeModel]}} diff --git a/ui/core/app/templates/scopes/index.hbs b/ui/core/app/templates/scopes/index.hbs new file mode 100644 index 000000000..e2147cab0 --- /dev/null +++ b/ui/core/app/templates/scopes/index.hbs @@ -0,0 +1 @@ +{{outlet}} \ No newline at end of file diff --git a/ui/core/app/templates/scopes/scope/index.hbs b/ui/core/app/templates/scopes/scope/index.hbs new file mode 100644 index 000000000..e2147cab0 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/index.hbs @@ -0,0 +1 @@ +{{outlet}} \ No newline at end of file diff --git a/ui/core/mirage/config.js b/ui/core/mirage/config.js index 2ce197f7b..fec96c052 100644 --- a/ui/core/mirage/config.js +++ b/ui/core/mirage/config.js @@ -1,6 +1,6 @@ import config from '../config/environment'; import { authHandler, deauthHandler } from './route-handlers/auth'; -import { Response } from 'miragejs'; +//import { Response } from 'miragejs'; export default function() { @@ -21,9 +21,10 @@ export default function() { this.get('/scopes', ({ scopes }, { queryParams: { scope_id } }) => { // Default parent scope is global if (!scope_id) scope_id = 'global'; - return scopes.where(scope => { + const results = scopes.where(scope => { return scope.scope ? scope.scope.id === scope_id : false }); + return results; }); this.post('/scopes', function ({ scopes }, request) { // Parent scope comes through the payload via `scope_id`, but this needs @@ -34,17 +35,7 @@ export default function() { attrs.scope = parentScopeAttrs; return scopes.create(attrs); }); - // To simulate a possible real-world case, org scopes are not returned, - // but project scopes are. - // TODO this should be expanded to support other scenarios. - this.get('/scopes/:id', ({ scopes }, { params: { id: scopeID } }) => { - const scope = scopes.find(scopeID); - if (scope.type !== 'project') { - return new Response(404, {}, {}); - } else { - return scope; - } - }); + this.get('/scopes/:id'); this.patch('/scopes/:id'); this.del('/scopes/:id'); diff --git a/ui/core/tests/acceptance/authentication-test.js b/ui/core/tests/acceptance/authentication-test.js index e71c1a3b0..f6fa1c8ab 100644 --- a/ui/core/tests/acceptance/authentication-test.js +++ b/ui/core/tests/acceptance/authentication-test.js @@ -6,15 +6,19 @@ import a11yAudit from 'ember-a11y-testing/test-support/audit'; import { currentSession, authenticateSession, - //invalidateSession, + invalidateSession, } from 'ember-simple-auth/test-support'; module('Acceptance | authentication', function(hooks) { setupApplicationTest(hooks); setupMirage(hooks); + let indexURL; + let globalScope; let orgScope; let orgScopeID; + let scopesURL; + let orgScopeURL; let scope; let authMethod; let authMethodID; @@ -23,11 +27,19 @@ module('Acceptance | authentication', function(hooks) { let projectsURL; hooks.beforeEach(function () { - orgScope = this.server.create('scope', {type: 'org'}, 'withChildren'); + invalidateSession(); + indexURL = '/'; + globalScope = this.server.create('scope', { id: 'global' }); + orgScope = this.server.create('scope', { + type: 'org', + scope: { id: globalScope.id, type: globalScope.type } + }, 'withChildren'); scope = { id: orgScope.id, type: orgScope.type }; authMethod = this.server.create('auth-method', { scope }); orgScopeID = orgScope.id; authMethodID = authMethod.id; + scopesURL = `/scopes`; + orgScopeURL = `/scopes/${orgScopeID}`; authenticateURL = `/scopes/${orgScopeID}/authenticate`; authMethodAuthenticateURL = `/scopes/${orgScopeID}/authenticate/${authMethodID}`; projectsURL = `/scopes/${orgScopeID}/projects`; @@ -40,6 +52,22 @@ module('Acceptance | authentication', function(hooks) { assert.equal(currentURL(), authMethodAuthenticateURL); }); + test('visiting any authentication parent route while unauthenticated redirects to first authenticate method', async function(assert) { + assert.expect(3); + await visit(indexURL); + assert.equal(currentURL(), authMethodAuthenticateURL); + await visit(scopesURL); + assert.equal(currentURL(), authMethodAuthenticateURL); + assert.notOk(currentSession().isAuthenticated); + }); + + test('visiting projects while unauthenticated redirects to first authenticate method', async function(assert) { + assert.expect(2); + await visit(projectsURL); + assert.equal(currentURL(), authMethodAuthenticateURL); + assert.notOk(currentSession().isAuthenticated); + }); + test('visiting auth method authenticate route', async function(assert) { assert.expect(1); await visit(authMethodAuthenticateURL); @@ -47,13 +75,20 @@ module('Acceptance | authentication', function(hooks) { assert.equal(currentURL(), authMethodAuthenticateURL); }); - test('visiting any authenticate route while already authenticated redirects to projects', async function(assert) { - assert.expect(2); - authenticateSession(); + test('visiting any authentication parent route while already authenticated redirects to projects', async function(assert) { + assert.expect(6); + authenticateSession({ scope }); + await visit(indexURL); + assert.equal(currentURL(), projectsURL); + await visit(scopesURL); + assert.equal(currentURL(), projectsURL); + await visit(orgScopeURL); + assert.equal(currentURL(), projectsURL); await visit(authenticateURL); assert.equal(currentURL(), projectsURL); await visit(authMethodAuthenticateURL); assert.equal(currentURL(), projectsURL); + assert.ok(currentSession().isAuthenticated); }); test('failed authentication shows a notification message', async function(assert) { @@ -78,7 +113,7 @@ module('Acceptance | authentication', function(hooks) { assert.ok(currentSession().isAuthenticated); }); - test('deauthentication redirects to index', async function(assert) { + test('deauthentication redirects to first authenticate method', async function(assert) { assert.expect(3); await visit(authMethodAuthenticateURL); await fillIn('[name="identification"]', 'test'); @@ -87,7 +122,7 @@ module('Acceptance | authentication', function(hooks) { assert.ok(currentSession().isAuthenticated); await click('.rose-button-header-dropdown'); assert.notOk(currentSession().isAuthenticated); - assert.equal(currentURL(), '/'); + assert.equal(currentURL(), authMethodAuthenticateURL); }); }); diff --git a/ui/core/tests/unit/routes/index-test.js b/ui/core/tests/unit/routes/index-test.js new file mode 100644 index 000000000..eaaac17bc --- /dev/null +++ b/ui/core/tests/unit/routes/index-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | index', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:index'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/index-test.js b/ui/core/tests/unit/routes/scopes/index-test.js new file mode 100644 index 000000000..c2fb98357 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/index-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/index', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/index'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/index-test.js b/ui/core/tests/unit/routes/scopes/scope/index-test.js new file mode 100644 index 000000000..ba0a5bdba --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/index-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/index', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/index'); + assert.ok(route); + }); +}); From 1e57d1a896a1304525ee3a33bf0e4695976d45d0 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Mon, 27 Jul 2020 10:16:34 -0400 Subject: [PATCH 10/17] ci: remove connect-client from test step for now --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e455ef34b..f07bec9d6 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "lint:addons:rose": "echo \"\n\nlint: addons/rose\n\" && cd addons/rose && yarn lint", "lint:ui:core": "echo \"\n\nlint: ui/core\n\" && cd ui/core && yarn lint", "lint:ui:connect-client": "echo \"\n\nlint: ui/connect-client\n\" && cd ui/connect-client && yarn lint", - "test": "yarn test:addons:api && yarn test:addons:auth && yarn test:addons:rose && yarn test:ui:core && yarn test:ui:connect-client", + "test": "yarn test:addons:api && yarn test:addons:auth && yarn test:addons:rose && yarn test:ui:core", "test:addons:api": "echo \"\n\ntest: addons/api\n\" && cd addons/api && yarn test", "test:addons:auth": "echo \"\n\ntest: addons/auth\n\" && cd addons/auth && yarn test", "test:addons:rose": "echo \"\n\ntest: addons/rose\n\" && cd addons/rose && yarn test", From 330a7ceb3b1cc65759e00319a63c19b28b747be6 Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Mon, 27 Jul 2020 14:34:39 -0400 Subject: [PATCH 11/17] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Update=20deauthen?= =?UTF-8?q?ticate=20params=20to=20still=20use=20org=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of session invalidation being provided: { scope: { id: scopeID } }, it gets: {authenticator: "authenticator:password", org_id: "1"}. --- ui/core/app/authenticators/password.js | 2 +- ui/core/translations/en-us.yaml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/core/app/authenticators/password.js b/ui/core/app/authenticators/password.js index 3224130a3..bd2011911 100644 --- a/ui/core/app/authenticators/password.js +++ b/ui/core/app/authenticators/password.js @@ -38,7 +38,7 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator { * @param {string} scopeID * @return {string} */ - buildDeauthEndpointURL({ scope: { id: scopeID } }) { + buildDeauthEndpointURL({ authenticator, org_id: scopeID }) { const adapter = this.store.adapterFor('application'); const options = { adapterOptions: { method: 'deauthenticate' } }; return adapter.buildURL('scope', scopeID, options, 'findRecord'); diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index e770de25c..f01e4cf72 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -4,6 +4,7 @@ actions: dismiss: Dismiss save: Save help: Need help? + deauthenticate: Deauthenticate resources: user: User users: Users From 356c02b3658ada49984c203896f181fea55fd99b Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Mon, 27 Jul 2020 14:48:33 -0400 Subject: [PATCH 12/17] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Show=20sidebar=20?= =?UTF-8?q?only=20when=20authenticated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/core/app/templates/scopes/scope.hbs | 46 ++++++++++++++------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/ui/core/app/templates/scopes/scope.hbs b/ui/core/app/templates/scopes/scope.hbs index 2d1a73607..276ca4835 100644 --- a/ui/core/app/templates/scopes/scope.hbs +++ b/ui/core/app/templates/scopes/scope.hbs @@ -1,32 +1,34 @@ + {{#if this.session.isAuthenticated}} + - + + + {{t "resources.projects"}} + + - - - {{t "resources.projects"}} - - + {{outlet "sidebar"}} - {{outlet "sidebar"}} - - {{!-- - - - {{t "resources.users"}} - - - {{t "resources.groups"}} - + {{!-- + + + {{t "resources.users"}} + - - {{t "resources.roles"}} - - - --}} + + {{t "resources.groups"}} + - + + {{t "resources.roles"}} + + + --}} + + + {{/if}}
From 3f1a7f155c2f57dc7034b57e3c52f5eda0bfdc68 Mon Sep 17 00:00:00 2001 From: Susmitha Girumala Date: Mon, 27 Jul 2020 14:49:42 -0400 Subject: [PATCH 13/17] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Port=20authentica?= =?UTF-8?q?te=20template=20from=20pre-scope=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templates/scopes/scope/authenticate.hbs | 6 ++++- .../scopes/scope/authenticate/index.hbs | 1 - .../scopes/scope/authenticate/method.hbs | 26 +++++++++++++++++++ ui/core/translations/en-us.yaml | 16 ++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/ui/core/app/templates/scopes/scope/authenticate.hbs b/ui/core/app/templates/scopes/scope/authenticate.hbs index e2147cab0..5471432d9 100644 --- a/ui/core/app/templates/scopes/scope/authenticate.hbs +++ b/ui/core/app/templates/scopes/scope/authenticate.hbs @@ -1 +1,5 @@ -{{outlet}} \ No newline at end of file + +
+ {{outlet}} +
+
diff --git a/ui/core/app/templates/scopes/scope/authenticate/index.hbs b/ui/core/app/templates/scopes/scope/authenticate/index.hbs index e2147cab0..e69de29bb 100644 --- a/ui/core/app/templates/scopes/scope/authenticate/index.hbs +++ b/ui/core/app/templates/scopes/scope/authenticate/index.hbs @@ -1 +0,0 @@ -{{outlet}} \ No newline at end of file diff --git a/ui/core/app/templates/scopes/scope/authenticate/method.hbs b/ui/core/app/templates/scopes/scope/authenticate/method.hbs index 60057d773..c6e4157e2 100644 --- a/ui/core/app/templates/scopes/scope/authenticate/method.hbs +++ b/ui/core/app/templates/scopes/scope/authenticate/method.hbs @@ -1,3 +1,29 @@ +{{#if @model}} +

{{t 'actions.authenticate'}}

+

{{t "auth.descriptions.choose-org"}}

+ + {{#each @model as |org|}} + + {{org.displayName}} + + {{/each}} + +{{/if}} + +{{#unless @model}} + + + {{t "auth.descriptions.no-orgs"}} + + +{{/unless}} + Date: Mon, 27 Jul 2020 16:19:08 -0400 Subject: [PATCH 14/17] =?UTF-8?q?chore:=20=F0=9F=A4=96=20Update=20auth=20t?= =?UTF-8?q?emplates=20under=20scopes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ui/core/app/authenticators/password.js | 2 +- ui/core/app/router.js | 1 - ui/core/app/routes/scopes/scope.js | 1 - ui/core/app/templates/scopes/scope.hbs | 45 ++++++++-------- .../templates/scopes/scope/authenticate.hbs | 53 +++++++++++++++++-- .../scopes/scope/authenticate/index.hbs | 12 +++++ .../scopes/scope/authenticate/method.hbs | 26 --------- ui/core/app/templates/scopes/scope/index.hbs | 2 +- ui/core/mirage/scenarios/default.js | 11 ++++ 9 files changed, 96 insertions(+), 57 deletions(-) diff --git a/ui/core/app/authenticators/password.js b/ui/core/app/authenticators/password.js index bd2011911..41e75d078 100644 --- a/ui/core/app/authenticators/password.js +++ b/ui/core/app/authenticators/password.js @@ -38,7 +38,7 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator { * @param {string} scopeID * @return {string} */ - buildDeauthEndpointURL({ authenticator, org_id: scopeID }) { + buildDeauthEndpointURL({ org_id: scopeID }) { const adapter = this.store.adapterFor('application'); const options = { adapterOptions: { method: 'deauthenticate' } }; return adapter.buildURL('scope', scopeID, options, 'findRecord'); diff --git a/ui/core/app/router.js b/ui/core/app/router.js index 3c95ac355..ef3536c60 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -18,7 +18,6 @@ Router.map(function () { this.route('new'); }); }); - this.route('cope'); }); this.route('orgs', function () { diff --git a/ui/core/app/routes/scopes/scope.js b/ui/core/app/routes/scopes/scope.js index c9ecb358f..939ef5076 100644 --- a/ui/core/app/routes/scopes/scope.js +++ b/ui/core/app/routes/scopes/scope.js @@ -23,5 +23,4 @@ export default class ScopesScopeRoute extends Route { return maybeExistingScope || this.store.createRecord('scope', { id }); }); } - } diff --git a/ui/core/app/templates/scopes/scope.hbs b/ui/core/app/templates/scopes/scope.hbs index 276ca4835..35287c253 100644 --- a/ui/core/app/templates/scopes/scope.hbs +++ b/ui/core/app/templates/scopes/scope.hbs @@ -1,34 +1,31 @@ - {{#if this.session.isAuthenticated}} - - - - {{t "resources.projects"}} - - + + + + {{t "resources.projects"}} + + - {{outlet "sidebar"}} + {{outlet "sidebar"}} - {{!-- - - - {{t "resources.users"}} - + {{!-- + + + {{t "resources.users"}} + - - {{t "resources.groups"}} - + + {{t "resources.groups"}} + - - {{t "resources.roles"}} - - - --}} - - - {{/if}} + + {{t "resources.roles"}} + + + --}} +
diff --git a/ui/core/app/templates/scopes/scope/authenticate.hbs b/ui/core/app/templates/scopes/scope/authenticate.hbs index 5471432d9..c0e8c5485 100644 --- a/ui/core/app/templates/scopes/scope/authenticate.hbs +++ b/ui/core/app/templates/scopes/scope/authenticate.hbs @@ -1,5 +1,52 @@ -
- {{outlet}} -
+ + + +

{{t 'actions.authenticate'}}

+

{{t 'auth.descriptions.authenticating-with-org' name=@model.scope.displayName}}

+
+ + + + + {{#each @model.scopes as |scope|}} + + {{scope.displayName}} + + {{/each}} + + + + + + {{#each @model.authMethods as |authMethod|}} + + {{t (concat 'auth.method-types.' authMethod.type)}} + + {{/each}} + + + + + {{#if @model.authMethods}} + {{outlet}} + {{/if}} + {{#unless @model.authMethods}} + + + + {{t "auth.descriptions.no-methods"}} + + + + {{/unless}} + + +
diff --git a/ui/core/app/templates/scopes/scope/authenticate/index.hbs b/ui/core/app/templates/scopes/scope/authenticate/index.hbs index e69de29bb..22cc46f4e 100644 --- a/ui/core/app/templates/scopes/scope/authenticate/index.hbs +++ b/ui/core/app/templates/scopes/scope/authenticate/index.hbs @@ -0,0 +1,12 @@ +{{#unless @model}} + + + + {{t "auth.descriptions.no-methods"}} + + + +{{/unless}} diff --git a/ui/core/app/templates/scopes/scope/authenticate/method.hbs b/ui/core/app/templates/scopes/scope/authenticate/method.hbs index c6e4157e2..60057d773 100644 --- a/ui/core/app/templates/scopes/scope/authenticate/method.hbs +++ b/ui/core/app/templates/scopes/scope/authenticate/method.hbs @@ -1,29 +1,3 @@ -{{#if @model}} -

{{t 'actions.authenticate'}}

-

{{t "auth.descriptions.choose-org"}}

- - {{#each @model as |org|}} - - {{org.displayName}} - - {{/each}} - -{{/if}} - -{{#unless @model}} - - - {{t "auth.descriptions.no-orgs"}} - - -{{/unless}} - Date: Tue, 28 Jul 2020 10:31:40 -0400 Subject: [PATCH 15/17] chore: tweak input sizing and media queries --- addons/rose/addon/styles/rose/components/form/_input.scss | 1 + addons/rose/addon/styles/rose/variables/_media.scss | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/rose/addon/styles/rose/components/form/_input.scss b/addons/rose/addon/styles/rose/components/form/_input.scss index 6c0186b50..7b11a0a8e 100644 --- a/addons/rose/addon/styles/rose/components/form/_input.scss +++ b/addons/rose/addon/styles/rose/components/form/_input.scss @@ -19,6 +19,7 @@ .rose-form-input-field { @include type.type(m); + box-sizing: border-box; margin-bottom: sizing.rems(xs); padding: sizing.rems(xs); color: var(--stark); diff --git a/addons/rose/addon/styles/rose/variables/_media.scss b/addons/rose/addon/styles/rose/variables/_media.scss index 289c96ad9..95d1ab86d 100644 --- a/addons/rose/addon/styles/rose/variables/_media.scss +++ b/addons/rose/addon/styles/rose/variables/_media.scss @@ -9,7 +9,7 @@ $width-breakpoints: ( xlarge: 1200px, large: 1024px, medium: 768px, - small: 480px, + small: 540px, xsmall: 320px, ); From 0b974f122c46baec937a920578b07adba41508c5 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Tue, 28 Jul 2020 10:36:41 -0400 Subject: [PATCH 16/17] chore: hide sidebar when not authenticated --- ui/core/app/controllers/scopes/scope.js | 10 +++++ ui/core/app/templates/scopes/scope.hbs | 44 ++++++++++--------- .../unit/controllers/scopes/scope-test.js | 12 +++++ 3 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 ui/core/app/controllers/scopes/scope.js create mode 100644 ui/core/tests/unit/controllers/scopes/scope-test.js diff --git a/ui/core/app/controllers/scopes/scope.js b/ui/core/app/controllers/scopes/scope.js new file mode 100644 index 000000000..5bc636752 --- /dev/null +++ b/ui/core/app/controllers/scopes/scope.js @@ -0,0 +1,10 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class ScopesScopeController extends Controller { + + // =services + + @service session; + +} diff --git a/ui/core/app/templates/scopes/scope.hbs b/ui/core/app/templates/scopes/scope.hbs index 35287c253..172eefc48 100644 --- a/ui/core/app/templates/scopes/scope.hbs +++ b/ui/core/app/templates/scopes/scope.hbs @@ -1,31 +1,33 @@ - - - - {{t "resources.projects"}} - - + {{#if this.session.isAuthenticated}} + + + + {{t "resources.projects"}} + + - {{outlet "sidebar"}} + {{outlet "sidebar"}} - {{!-- - - - {{t "resources.users"}} - + {{!-- + + + {{t "resources.users"}} + - - {{t "resources.groups"}} - + + {{t "resources.groups"}} + - - {{t "resources.roles"}} - - - --}} - + + {{t "resources.roles"}} + + + --}} + + {{/if}}
diff --git a/ui/core/tests/unit/controllers/scopes/scope-test.js b/ui/core/tests/unit/controllers/scopes/scope-test.js new file mode 100644 index 000000000..ec2c389bb --- /dev/null +++ b/ui/core/tests/unit/controllers/scopes/scope-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | scopes/scope', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:scopes/scope'); + assert.ok(controller); + }); +}); From acf45fada90d904e584f7b9d32cd261b7d6bbfc6 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Tue, 28 Jul 2020 10:39:35 -0400 Subject: [PATCH 17/17] chore: add page title --- ui/core/app/templates/scopes/scope/authenticate.hbs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/ui/core/app/templates/scopes/scope/authenticate.hbs b/ui/core/app/templates/scopes/scope/authenticate.hbs index c0e8c5485..43b329188 100644 --- a/ui/core/app/templates/scopes/scope/authenticate.hbs +++ b/ui/core/app/templates/scopes/scope/authenticate.hbs @@ -1,15 +1,17 @@ +{{title (t "actions.authenticate")}} + -

{{t 'actions.authenticate'}}

-

{{t 'auth.descriptions.authenticating-with-org' name=@model.scope.displayName}}

+

{{t "actions.authenticate"}}

+

{{t "auth.descriptions.authenticating-with-org" name=@model.scope.displayName}}

{{#each @model.scopes as |scope|}} @@ -24,7 +26,7 @@ {{#each @model.authMethods as |authMethod|}} - {{t (concat 'auth.method-types.' authMethod.type)}} + {{t (concat "auth.method-types." authMethod.type)}} {{/each}}