Merge pull request #93 from hashicorp/refactor-scopes-auth
Refactor scopes auth
This commit is contained in:
54 files changed
+1485
-65
No files matched your search
Whitespace-only changes.
@@ -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 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 URL generated
|
||||
* by `buildDeauthEndpointURL`, but is not guaranteed.
|
||||
*
|
||||
* This authenticator should not be used directly because it does not specify
|
||||
* an `buildAuthEndpointURL` or `buildDeauthEndpointURL` 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 {boolean} requestCookies request cookie tokens (default `true`)
|
||||
* @return {Promise}
|
||||
*/
|
||||
async authenticate(
|
||||
{ identification: name, password },
|
||||
requestCookies = true,
|
||||
options
|
||||
) {
|
||||
const body = JSON.stringify({
|
||||
token_type: requestCookies ? 'cookie' : null,
|
||||
credentials: { name, password },
|
||||
});
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(options) {
|
||||
const deauthEndpointURL = this.buildDeauthEndpointURL(options);
|
||||
fetch(deauthEndpointURL, { method: 'post' }).catch(() => {
|
||||
/* no op */
|
||||
});
|
||||
return super.invalidate(...arguments);
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import PasswordAuthenticator from 'auth/authenticators/password';
|
||||
|
||||
export default class ApplicationPasswordAuthenticator extends PasswordAuthenticator {
|
||||
|
||||
buildAuthEndpointURL() {
|
||||
return '/authenticate';
|
||||
}
|
||||
|
||||
buildDeauthEndpointURL() {
|
||||
return '/deauthenticate';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import CookieSessionStore from 'auth/session-stores/cookie';
|
||||
|
||||
export default class ApplicationCookieSessionStore extends CookieSessionStore {
|
||||
cookiePath = '/';
|
||||
cookieName = 'my-cookie';
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import LocalStorageSessionStore from 'auth/session-stores/local-storage';
|
||||
|
||||
export default class ApplicationLocalStorageSessionStore extends LocalStorageSessionStore {}
|
||||
@@ -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
|
||||
*/
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { JSONAPISerializer } from 'ember-cli-mirage';
|
||||
|
||||
export default JSONAPISerializer.extend({});
|
||||
@@ -0,0 +1,90 @@
|
||||
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.buildAuthEndpointURL(), (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.buildAuthEndpointURL(), (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.buildAuthEndpointURL(), (schema, request) => {
|
||||
const json = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(json, {
|
||||
token_type: 'cookie',
|
||||
credentials: {
|
||||
name: 'foo',
|
||||
password: 'bar',
|
||||
},
|
||||
});
|
||||
return new Response(200);
|
||||
});
|
||||
const creds = {
|
||||
identification: 'foo',
|
||||
password: 'bar',
|
||||
};
|
||||
await authenticator.authenticate(creds);
|
||||
});
|
||||
|
||||
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.buildAuthEndpointURL(), () => {
|
||||
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.buildDeauthEndpointURL(), () => {
|
||||
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.buildDeauthEndpointURL(), () => {
|
||||
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'));
|
||||
});
|
||||
});
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -9,7 +9,7 @@ $width-breakpoints: (
|
||||
xlarge: 1200px,
|
||||
large: 1024px,
|
||||
medium: 768px,
|
||||
small: 480px,
|
||||
small: 540px,
|
||||
xsmall: 320px,
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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: { 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({ org_id: scopeID }) {
|
||||
const adapter = this.store.adapterFor('application');
|
||||
const options = { adapterOptions: { method: 'deauthenticate' } };
|
||||
return adapter.buildURL('scope', scopeID, options, 'findRecord');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -1,6 +1,63 @@
|
||||
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.
|
||||
*/
|
||||
async sessionInvalidated() {
|
||||
// Catch error in this transition, since it will be aborted by the
|
||||
// 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
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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([]));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,15 +14,13 @@ 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 });
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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
|
||||
|
||||
// TODO: this assumes we're in an org scope, but the "landing"
|
||||
// for global scope is probably different.
|
||||
routeIfAlreadyAuthenticated = 'scopes.scope.projects';
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* 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'
|
||||
// }])
|
||||
});
|
||||
}
|
||||
|
||||
redirect() {
|
||||
if (this.session.isAuthenticated) {
|
||||
this.transitionTo(this.routeIfAlreadyAuthenticated);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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;
|
||||
|
||||
// =attributes
|
||||
|
||||
routeAfterAuthentication = 'scopes.scope.projects';
|
||||
|
||||
// =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 requestCookies = false;
|
||||
try {
|
||||
await this.session.authenticate(
|
||||
authenticatorName,
|
||||
creds,
|
||||
requestCookies,
|
||||
{ scope, authMethod }
|
||||
);
|
||||
this.transitionTo(this.routeAfterAuthentication);
|
||||
} catch (e) {
|
||||
const errorMessage = this.intl.t('errors.titles.authentication-failed');
|
||||
this.notify.error(errorMessage, { closeAfter: null });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]}}
|
||||
|
||||
@@ -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 scope = get(this, 'scope');
|
||||
const authenticated = get(sessionData, 'authenticated');
|
||||
if (scope && authenticated) authenticated.scope = scope;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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;
|
||||
//
|
||||
// }
|
||||
@@ -6,6 +6,8 @@
|
||||
<layout.header>
|
||||
<Rose::Header as |header|>
|
||||
<header.brand @logo="logo-hashi" @text={{app-name}} />
|
||||
|
||||
{{!--
|
||||
<header.nav as |nav|>
|
||||
<nav.dropdown @text={{t 'dev.default-org'}} as |dropdown|>
|
||||
<dropdown.link @route="index">{{t 'dev.menu-item'}}</dropdown.link>
|
||||
@@ -15,16 +17,22 @@
|
||||
<nav.link @route="index">{{t 'dev.section'}}</nav.link>
|
||||
<nav.link @route="index">{{t 'dev.section'}}</nav.link>
|
||||
</header.nav>
|
||||
<header.utilities as |utilities|>
|
||||
<utilities.dropdown
|
||||
@text="user@example.net"
|
||||
@iconOnly={{true}}
|
||||
@icon="user-square-fill" as |dropdown|
|
||||
>
|
||||
<dropdown.link @route="index">{{t 'dev.menu-item'}}</dropdown.link>
|
||||
<dropdown.button>{{t 'dev.button-menu-item'}}</dropdown.button>
|
||||
</utilities.dropdown>
|
||||
</header.utilities>
|
||||
--}}
|
||||
|
||||
{{#if this.session.isAuthenticated}}
|
||||
<header.utilities as |utilities|>
|
||||
<utilities.dropdown
|
||||
@text="user@example.net"
|
||||
@iconOnly={{true}}
|
||||
@icon="user-square-fill" as |dropdown|
|
||||
>
|
||||
<dropdown.button {{on "click" (route-action "invalidateSession")}}>
|
||||
{{t 'actions.deauthenticate'}}
|
||||
</dropdown.button>
|
||||
</utilities.dropdown>
|
||||
</header.utilities>
|
||||
{{/if}}
|
||||
|
||||
</Rose::Header>
|
||||
</layout.header>
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{{outlet}}
|
||||
@@ -1,32 +1,33 @@
|
||||
<Rose::Layout::Sidebar as |sidebarLayout|>
|
||||
|
||||
<sidebarLayout.sidebar>
|
||||
{{#if this.session.isAuthenticated}}
|
||||
<sidebarLayout.sidebar>
|
||||
<Rose::Nav::Sidebar @title={{t "titles.general"}} as |nav|>
|
||||
<nav.link @route="scopes.scope.projects">
|
||||
{{t "resources.projects"}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Sidebar>
|
||||
|
||||
<Rose::Nav::Sidebar @title={{t "titles.general"}} as |nav|>
|
||||
<nav.link @route="scopes.scope.projects">
|
||||
{{t "resources.projects"}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Sidebar>
|
||||
{{outlet "sidebar"}}
|
||||
|
||||
{{outlet "sidebar"}}
|
||||
|
||||
{{!--
|
||||
<Rose::Nav::Sidebar @title={{t "titles.iam"}} as |nav|>
|
||||
<nav.link @route="orgs.org.users">
|
||||
{{t "resources.users"}}
|
||||
</nav.link>
|
||||
|
||||
<nav.link @route="orgs.org.groups">
|
||||
{{t "resources.groups"}}
|
||||
</nav.link>
|
||||
{{!--
|
||||
<Rose::Nav::Sidebar @title={{t "titles.iam"}} as |nav|>
|
||||
<nav.link @route="orgs.org.users">
|
||||
{{t "resources.users"}}
|
||||
</nav.link>
|
||||
|
||||
<nav.link @route="orgs.org.roles">
|
||||
{{t "resources.roles"}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Sidebar>
|
||||
--}}
|
||||
<nav.link @route="orgs.org.groups">
|
||||
{{t "resources.groups"}}
|
||||
</nav.link>
|
||||
|
||||
</sidebarLayout.sidebar>
|
||||
<nav.link @route="orgs.org.roles">
|
||||
{{t "resources.roles"}}
|
||||
</nav.link>
|
||||
</Rose::Nav::Sidebar>
|
||||
--}}
|
||||
</sidebarLayout.sidebar>
|
||||
{{/if}}
|
||||
|
||||
<sidebarLayout.body>
|
||||
<main>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
{{title (t "actions.authenticate")}}
|
||||
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Layout::Page as |page|>
|
||||
|
||||
<page.header>
|
||||
<h2>{{t "actions.authenticate"}}</h2>
|
||||
<p>{{t "auth.descriptions.authenticating-with-org" name=@model.scope.displayName}}</p>
|
||||
</page.header>
|
||||
|
||||
<page.navigation>
|
||||
|
||||
<Rose::Dropdown
|
||||
@text={{t "actions.choose-different-org"}}
|
||||
@icon="user-organization"
|
||||
as |dropdown|>
|
||||
{{#each @model.scopes as |scope|}}
|
||||
<dropdown.link @route="scopes.scope.authenticate" @model={{scope}}>
|
||||
{{scope.displayName}}
|
||||
</dropdown.link>
|
||||
{{/each}}
|
||||
</Rose::Dropdown>
|
||||
|
||||
<Rose::Separator />
|
||||
|
||||
<Rose::Nav::Tabs as |nav|>
|
||||
{{#each @model.authMethods as |authMethod|}}
|
||||
<nav.link @route="scopes.scope.authenticate.method" @model={{authMethod}}>
|
||||
{{t (concat "auth.method-types." authMethod.type)}}
|
||||
</nav.link>
|
||||
{{/each}}
|
||||
</Rose::Nav::Tabs>
|
||||
</page.navigation>
|
||||
|
||||
<page.body>
|
||||
{{#if @model.authMethods}}
|
||||
{{outlet}}
|
||||
{{/if}}
|
||||
{{#unless @model.authMethods}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message
|
||||
@title={{t "auth.titles.no-methods"}}
|
||||
@icon="alert-circle-outline"
|
||||
as |message|>
|
||||
<message.description>
|
||||
{{t "auth.descriptions.no-methods"}}
|
||||
</message.description>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
</page.body>
|
||||
|
||||
</Rose::Layout::Page>
|
||||
</Rose::Layout::Centered>
|
||||
@@ -0,0 +1,12 @@
|
||||
{{#unless @model}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message
|
||||
@title={{t "auth.titles.no-methods"}}
|
||||
@icon="alert-circle-outline"
|
||||
as |message|>
|
||||
<message.description>
|
||||
{{t "auth.descriptions.no-methods"}}
|
||||
</message.description>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Rose::Form
|
||||
@onSubmit={{route-action "authenticate" (hash
|
||||
identification=this.identification
|
||||
password=this.password
|
||||
)}}
|
||||
@cancel={{false}}
|
||||
as |form|
|
||||
>
|
||||
|
||||
<form.input
|
||||
name="identification"
|
||||
@type="text"
|
||||
@value={{this.identification}}
|
||||
@label={{t "form.label.identification"}} />
|
||||
|
||||
<form.input
|
||||
name="password"
|
||||
@type="password"
|
||||
@value={{this.password}}
|
||||
@label={{t "form.label.password"}} />
|
||||
|
||||
<form.actions
|
||||
@submitText={{t "actions.authenticate"}}
|
||||
@showCancel={{false}} />
|
||||
|
||||
</Rose::Form>
|
||||
@@ -0,0 +1 @@
|
||||
{{outlet}}
|
||||
@@ -1,5 +1,6 @@
|
||||
import config from '../config/environment';
|
||||
import { Response } from 'miragejs';
|
||||
import { authHandler, deauthHandler } from './route-handlers/auth';
|
||||
//import { Response } from 'miragejs';
|
||||
|
||||
export default function() {
|
||||
|
||||
@@ -20,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
|
||||
@@ -33,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');
|
||||
|
||||
@@ -55,6 +47,10 @@ export default function() {
|
||||
this.patch('/scopes/:scope_id/auth-methods/:id');
|
||||
this.del('/scopes/:scope_id/auth-methods/:id');
|
||||
|
||||
// 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');
|
||||
this.post('/scopes/:scope_id/users');
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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);
|
||||
// }
|
||||
// }
|
||||
|
||||
export function authHandler({ scopes }, request) {
|
||||
const payload = JSON.parse(request.requestBody);
|
||||
if (payload.credentials.name === 'error') {
|
||||
return new Response(400);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
@@ -2,14 +2,31 @@ 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 };
|
||||
|
||||
// Create additional scopes for authentication organization selection
|
||||
server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: globalScope.id, type: globalScope.type }
|
||||
}, 'withChildren');
|
||||
|
||||
server.create('scope', {
|
||||
type: 'org',
|
||||
scope: { id: globalScope.id, type: globalScope.type }
|
||||
}, 'withChildren');
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
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';
|
||||
import {
|
||||
currentSession,
|
||||
authenticateSession,
|
||||
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;
|
||||
let authenticateURL;
|
||||
let authMethodAuthenticateURL;
|
||||
let projectsURL;
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
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`;
|
||||
});
|
||||
|
||||
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 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);
|
||||
await a11yAudit();
|
||||
assert.equal(currentURL(), authMethodAuthenticateURL);
|
||||
});
|
||||
|
||||
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) {
|
||||
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(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 first authenticate method', 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(), authMethodAuthenticateURL);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,21 @@ actions:
|
||||
dismiss: Dismiss
|
||||
save: Save
|
||||
help: Need help?
|
||||
authenticate: Authenticate
|
||||
deauthenticate: Deauthenticate
|
||||
choose-org: Choose Org
|
||||
choose-different-org: Choose a Different Org
|
||||
auth:
|
||||
titles:
|
||||
no-orgs: No Organizations Available
|
||||
no-methods: No Authentication Method
|
||||
descriptions:
|
||||
no-orgs: No organizations were found. To authenticate, ask your administrator to create an organization and grant you access.
|
||||
no-methods: No authentication methods were found for this organization. Switch organizations and try again.
|
||||
choose-org: Choose an organization to authenticate.
|
||||
authenticating-with-org: "You're authenticating with {name}."
|
||||
method-types:
|
||||
password: Password
|
||||
resources:
|
||||
user: User
|
||||
users: Users
|
||||
@@ -46,6 +61,8 @@ form:
|
||||
name: Name
|
||||
description: Description
|
||||
type: Type
|
||||
identification: Username
|
||||
password: Password
|
||||
errors:
|
||||
titles:
|
||||
unauthenticated-error: You are not signed in
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user