ui: Change Run Job availability based on ACLs (#5944)

This builds on API changes in #6017 and #6021 to conditionally turn off the
“Run Job” button based on the current token’s capabilities, or the capabilities
of the anonymous policy if no token is present.

If you try to visit the job-run route directly, it redirects to the job list.
This commit is contained in:
Buck Doyle
2020-01-20 14:57:01 -06:00
committed by GitHub
parent 6c3a29a877
commit 3adb3cd1fe
25 changed files with 511 additions and 54 deletions

79
ui/app/abilities/job.js Normal file
View File

@@ -0,0 +1,79 @@
import { Ability } from 'ember-can';
import { inject as service } from '@ember/service';
import { computed, get } from '@ember/object';
import { equal, or } from '@ember/object/computed';
export default Ability.extend({
system: service(),
token: service(),
canRun: or('selfTokenIsManagement', 'policiesSupportRunning'),
selfTokenIsManagement: equal('token.selfToken.type', 'management'),
activeNamespace: computed('system.activeNamespace.name', function() {
return this.get('system.activeNamespace.name') || 'default';
}),
rulesForActiveNamespace: computed('activeNamespace', 'token.selfTokenPolicies.[]', function() {
let activeNamespace = this.activeNamespace;
return (this.get('token.selfTokenPolicies') || []).toArray().reduce((rules, policy) => {
let policyNamespaces = get(policy, 'rulesJSON.Namespaces') || [];
let matchingNamespace = this._findMatchingNamespace(policyNamespaces, activeNamespace);
if (matchingNamespace) {
rules.push(policyNamespaces.find(namespace => namespace.Name === matchingNamespace));
}
return rules;
}, []);
}),
policiesSupportRunning: computed('rulesForActiveNamespace.@each.capabilities', function() {
return this.rulesForActiveNamespace.some(rules => {
let capabilities = get(rules, 'Capabilities') || [];
return capabilities.includes('submit-job');
});
}),
// Chooses the closest namespace as described at the bottom here:
// https://www.nomadproject.io/guides/security/acl.html#namespace-rules
_findMatchingNamespace(policyNamespaces, activeNamespace) {
let namespaceNames = policyNamespaces.mapBy('Name');
if (namespaceNames.includes(activeNamespace)) {
return activeNamespace;
}
let globNamespaceNames = namespaceNames.filter(namespaceName => namespaceName.includes('*'));
let matchingNamespaceName = globNamespaceNames.reduce(
(mostMatching, namespaceName) => {
// Convert * wildcards to .* for regex matching
let namespaceNameRegExp = new RegExp(namespaceName.replace(/\*/g, '.*'));
let characterDifference = activeNamespace.length - namespaceName.length;
if (
characterDifference < mostMatching.mostMatchingCharacterDifference &&
activeNamespace.match(namespaceNameRegExp)
) {
return {
mostMatchingNamespaceName: namespaceName,
mostMatchingCharacterDifference: characterDifference,
};
} else {
return mostMatching;
}
},
{ mostMatchingNamespaceName: null, mostMatchingCharacterDifference: Number.MAX_SAFE_INTEGER }
).mostMatchingNamespaceName;
if (matchingNamespaceName) {
return matchingNamespaceName;
} else if (namespaceNames.includes('default')) {
return 'default';
}
},
});

View File

@@ -5,4 +5,5 @@ export default Model.extend({
name: attr('string'),
description: attr('string'),
rules: attr('string'),
rulesJSON: attr(),
});

View File

@@ -8,6 +8,7 @@ export default Route.extend({
config: service(),
system: service(),
store: service(),
token: service(),
queryParams: {
region: {
@@ -22,28 +23,34 @@ export default Route.extend({
},
beforeModel(transition) {
return RSVP.all([this.get('system.regions'), this.get('system.defaultRegion')]).then(
promises => {
if (!this.get('system.shouldShowRegions')) return promises;
const fetchSelfTokenAndPolicies = this.get('token.fetchSelfTokenAndPolicies')
.perform()
.catch();
const queryParam = transition.to.queryParams.region;
const defaultRegion = this.get('system.defaultRegion.region');
const currentRegion = this.get('system.activeRegion') || defaultRegion;
return RSVP.all([
this.get('system.regions'),
this.get('system.defaultRegion'),
fetchSelfTokenAndPolicies,
]).then(promises => {
if (!this.get('system.shouldShowRegions')) return promises;
// Only reset the store if the region actually changed
if (
(queryParam && queryParam !== currentRegion) ||
(!queryParam && currentRegion !== defaultRegion)
) {
this.system.reset();
this.store.unloadAll();
}
const queryParam = transition.to.queryParams.region;
const defaultRegion = this.get('system.defaultRegion.region');
const currentRegion = this.get('system.activeRegion') || defaultRegion;
this.set('system.activeRegion', queryParam || defaultRegion);
return promises;
// Only reset the store if the region actually changed
if (
(queryParam && queryParam !== currentRegion) ||
(!queryParam && currentRegion !== defaultRegion)
) {
this.system.reset();
this.store.unloadAll();
}
);
this.set('system.activeRegion', queryParam || defaultRegion);
return promises;
});
},
// Model is being used as a way to transfer the provided region

View File

@@ -2,6 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default Route.extend({
can: service(),
store: service(),
system: service(),
@@ -12,6 +13,12 @@ export default Route.extend({
},
],
beforeModel() {
if (this.can.cannot('run job')) {
this.transitionTo('jobs');
}
},
model() {
return this.store.createRecord('job', {
namespace: this.get('system.activeNamespace'),

View File

@@ -1,10 +1,14 @@
import Service, { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { getOwner } from '@ember/application';
import { assign } from '@ember/polyfills';
import { task } from 'ember-concurrency';
import queryString from 'query-string';
import fetch from 'nomad-ui/utils/fetch';
export default Service.extend({
store: service(),
system: service(),
secret: computed({
@@ -22,6 +26,37 @@ export default Service.extend({
},
}),
fetchSelfToken: task(function*() {
const TokenAdapter = getOwner(this).lookup('adapter:token');
try {
return yield TokenAdapter.findSelf();
} catch (e) {
return null;
}
}),
selfToken: alias('fetchSelfToken.lastSuccessful.value'),
fetchSelfTokenPolicies: task(function*() {
try {
if (this.selfToken) {
return yield this.selfToken.get('policies');
} else {
let policy = yield this.store.findRecord('policy', 'anonymous');
return [policy];
}
} catch (e) {
return [];
}
}),
selfTokenPolicies: alias('fetchSelfTokenPolicies.lastSuccessful.value'),
fetchSelfTokenAndPolicies: task(function*() {
yield this.fetchSelfToken.perform();
yield this.fetchSelfTokenPolicies.perform();
}),
// All non Ember Data requests should go through authorizedRequest.
// However, the request that gets regions falls into that category.
// This authorizedRawRequest is necessary in order to fetch data

View File

@@ -46,6 +46,10 @@
transition: top 0.1s ease-in-out;
}
.tooltip.is-right-aligned::after {
transform: translateX(-75%);
}
.tooltip:hover::after,
.tooltip.always-active::after {
opacity: 1;

View File

@@ -15,7 +15,11 @@
</div>
{{#if (media "isMobile")}}
<div class="toolbar-item is-right-aligned">
{{#link-to "jobs.run" data-test-run-job class="button is-primary"}}Run Job{{/link-to}}
{{#if (can "run job")}}
{{#link-to "jobs.run" data-test-run-job class="button is-primary"}}Run Job{{/link-to}}
{{else}}
<button data-test-run-job class="button tooltip is-right-aligned" aria-label="You dont have permission to run jobs" disabled>Run Job</button>
{{/if}}
</div>
{{/if}}
<div class="toolbar-item is-right-aligned is-mobile-full-width">
@@ -48,7 +52,11 @@
</div>
{{#if (not (media "isMobile"))}}
<div class="toolbar-item is-right-aligned">
{{#link-to "jobs.run" data-test-run-job class="button is-primary"}}Run Job{{/link-to}}
{{#if (can "run job")}}
{{#link-to "jobs.run" data-test-run-job class="button is-primary"}}Run Job{{/link-to}}
{{else}}
<button data-test-run-job class="button tooltip is-right-aligned" aria-label="You dont have permission to run jobs" disabled>Run Job</button>
{{/if}}
</div>
{{/if}}
</div>

View File

@@ -278,6 +278,14 @@ export default function() {
const secret = req.requestHeaders['X-Nomad-Token'];
const tokenForSecret = tokens.findBy({ secretId: secret });
if (req.params.id === 'anonymous') {
if (policy) {
return this.serialize(policy);
} else {
return new Response(404, {}, null);
}
}
// Return the policy only if the token that matches the request header
// includes the policy or if the token that matches the request header
// is of type management

View File

@@ -49,6 +49,7 @@
"d3-transition": "^1.1.0",
"ember-ajax": "^5.0.0",
"ember-auto-import": "^1.2.21",
"ember-can": "^2.0.0",
"ember-cli": "~3.12.0",
"ember-cli-babel": "^7.7.3",
"ember-cli-clipboard": "^0.13.0",

View File

@@ -219,7 +219,9 @@ module('Acceptance | allocation detail', function(hooks) {
await Allocation.visit({ id: 'not-a-real-allocation' });
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
server.pretender.handledRequests
.filter(request => !request.url.includes('policy'))
.findBy('status', 404).url,
'/v1/allocation/not-a-real-allocation',
'A request to the nonexistent allocation is made'
);

View File

@@ -323,7 +323,9 @@ module('Acceptance | client detail', function(hooks) {
await ClientDetail.visit({ id: 'not-a-real-node' });
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
server.pretender.handledRequests
.filter(request => !request.url.includes('policy'))
.findBy('status', 404).url,
'/v1/node/not-a-real-node',
'A request to the nonexistent node is made'
);

View File

@@ -105,7 +105,9 @@ module('Acceptance | job allocations', function(hooks) {
await Allocations.visit({ id: 'not-a-real-job' });
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
server.pretender.handledRequests
.filter(request => !request.url.includes('policy'))
.findBy('status', 404).url,
'/v1/job/not-a-real-job',
'A request to the nonexistent job is made'
);

View File

@@ -79,7 +79,9 @@ module('Acceptance | job definition', function(hooks) {
await Definition.visit({ id: 'not-a-real-job' });
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
server.pretender.handledRequests
.filter(request => !request.url.includes('policy'))
.findBy('status', 404).url,
'/v1/job/not-a-real-job',
'A request to the nonexistent job is made'
);

View File

@@ -220,7 +220,9 @@ module('Acceptance | job deployments', function(hooks) {
await Deployments.visit({ id: 'not-a-real-job' });
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
server.pretender.handledRequests
.filter(request => !request.url.includes('policy'))
.findBy('status', 404).url,
'/v1/job/not-a-real-job',
'A request to the nonexistent job is made'
);

View File

@@ -47,7 +47,9 @@ moduleForJob(
await JobDetail.visit({ id: 'not-a-real-job' });
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
server.pretender.handledRequests
.filter(request => !request.url.includes('policy'))
.findBy('status', 404).url,
'/v1/job/not-a-real-job',
'A request to the nonexistent job is made'
);

View File

@@ -54,7 +54,9 @@ module('Acceptance | job evaluations', function(hooks) {
await Evaluations.visit({ id: 'not-a-real-job' });
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
server.pretender.handledRequests
.filter(request => !request.url.includes('policy'))
.findBy('status', 404).url,
'/v1/job/not-a-real-job',
'A request to the nonexistent job is made'
);

View File

@@ -9,6 +9,8 @@ import JobRun from 'nomad-ui/tests/pages/jobs/run';
const newJobName = 'new-job';
const newJobTaskGroupName = 'redis';
let managementToken, clientToken;
const jsonJob = overrides => {
return JSON.stringify(
assign(
@@ -45,6 +47,11 @@ module('Acceptance | job run', function(hooks) {
hooks.beforeEach(function() {
// Required for placing allocations (a result of creating jobs)
server.create('node');
managementToken = server.create('token');
clientToken = server.create('token');
window.localStorage.nomadTokenSecret = managementToken.secretId;
});
test('visiting /jobs/run', async function(assert) {
@@ -86,4 +93,11 @@ module('Acceptance | job run', function(hooks) {
`Redirected to the job overview page for ${newJobName} and switched the namespace to ${newNamespace}`
);
});
test('when the user doesnt have permission to run a job, redirects to the job overview page', async function(assert) {
window.localStorage.nomadTokenSecret = clientToken.secretId;
await JobRun.visit();
assert.equal(currentURL(), '/jobs');
});
});

View File

@@ -40,7 +40,9 @@ module('Acceptance | job versions', function(hooks) {
await Versions.visit({ id: 'not-a-real-job' });
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
server.pretender.handledRequests
.filter(request => !request.url.includes('policy'))
.findBy('status', 404).url,
'/v1/job/not-a-real-job',
'A request to the nonexistent job is made'
);

View File

@@ -4,6 +4,8 @@ import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import JobsList from 'nomad-ui/tests/pages/jobs/list';
let managementToken, clientToken;
module('Acceptance | jobs list', function(hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
@@ -11,6 +13,11 @@ module('Acceptance | jobs list', function(hooks) {
hooks.beforeEach(function() {
// Required for placing allocations (a result of creating jobs)
server.create('node');
managementToken = server.create('token');
clientToken = server.create('token');
window.localStorage.nomadTokenSecret = managementToken.secretId;
});
test('visiting /jobs', async function(assert) {
@@ -62,11 +69,76 @@ module('Acceptance | jobs list', function(hooks) {
test('the new job button transitions to the new job page', async function(assert) {
await JobsList.visit();
await JobsList.runJob();
await JobsList.runJobButton.click();
assert.equal(currentURL(), '/jobs/run');
});
test('the job run button is disabled when the token lacks permission', async function(assert) {
window.localStorage.nomadTokenSecret = clientToken.secretId;
await JobsList.visit();
assert.ok(JobsList.runJobButton.isDisabled);
await JobsList.runJobButton.click();
assert.equal(currentURL(), '/jobs');
});
test('the job run button state can change between namespaces', async function(assert) {
server.createList('namespace', 2);
const job1 = server.create('job', { namespaceId: server.db.namespaces[0].id });
const job2 = server.create('job', { namespaceId: server.db.namespaces[1].id });
window.localStorage.nomadTokenSecret = clientToken.secretId;
const policy = server.create('policy', {
id: 'something',
name: 'something',
rulesJSON: {
Namespaces: [
{
Name: job1.namespaceId,
Capabilities: ['list-jobs', 'submit-job'],
},
{
Name: job2.namespaceId,
Capabilities: ['list-jobs'],
},
],
},
});
clientToken.policyIds = [policy.id];
clientToken.save();
await JobsList.visit();
assert.notOk(JobsList.runJobButton.isDisabled);
const secondNamespace = server.db.namespaces[1];
await JobsList.visit({ namespace: secondNamespace.id });
assert.ok(JobsList.runJobButton.isDisabled);
});
test('the anonymous policy is fetched to check whether to show the job run button', async function(assert) {
window.localStorage.removeItem('nomadTokenSecret');
server.create('policy', {
id: 'anonymous',
name: 'anonymous',
rulesJSON: {
Namespaces: [
{
Name: 'default',
Capabilities: ['list-jobs', 'submit-job'],
},
],
},
});
await JobsList.visit();
assert.notOk(JobsList.runJobButton.isDisabled);
});
test('when there are no jobs, there is an empty message', async function(assert) {
await JobsList.visit();

View File

@@ -147,6 +147,7 @@ module('Acceptance | regions (many)', function(hooks) {
});
test('when the region is not the default region, all api requests include the region query param', async function(assert) {
window.localStorage.removeItem('nomadTokenSecret');
const region = server.db.regions[1].id;
await JobsList.visit({ region });
@@ -154,7 +155,12 @@ module('Acceptance | regions (many)', function(hooks) {
await JobsList.jobs.objectAt(0).clickRow();
await PageLayout.gutter.visitClients();
await PageLayout.gutter.visitServers();
const [regionsRequest, defaultRegionRequest, ...appRequests] = server.pretender.handledRequests;
const [
,
regionsRequest,
defaultRegionRequest,
...appRequests
] = server.pretender.handledRequests;
assert.notOk(
regionsRequest.url.includes('region='),

View File

@@ -145,7 +145,9 @@ module('Acceptance | task detail', function(hooks) {
await Task.visit({ id: 'not-a-real-allocation', name: task.name });
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
server.pretender.handledRequests
.filter(request => !request.url.includes('policy'))
.findBy('status', 404).url,
'/v1/allocation/not-a-real-allocation',
'A request to the nonexistent allocation is made'
);

View File

@@ -225,7 +225,9 @@ module('Acceptance | task group detail', function(hooks) {
await TaskGroup.visit({ id: 'not-a-real-job', name: 'not-a-real-task-group' });
assert.equal(
server.pretender.handledRequests.findBy('status', 404).url,
server.pretender.handledRequests
.filter(request => !request.url.includes('policy'))
.findBy('status', 404).url,
'/v1/job/not-a-real-job',
'A request to the nonexistent job is made'
);

View File

@@ -4,6 +4,7 @@ import {
collection,
clickable,
fillable,
is,
isPresent,
text,
visitable,
@@ -18,7 +19,10 @@ export default create({
search: fillable('[data-test-jobs-search] input'),
runJob: clickable('[data-test-run-job]'),
runJobButton: {
scope: '[data-test-run-job]',
isDisabled: is('[disabled]'),
},
jobs: collection('[data-test-job-row]', {
id: attribute('data-test-job-row'),

View File

@@ -0,0 +1,183 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import Service from '@ember/service';
module('Unit | Ability | job', function(hooks) {
setupTest(hooks);
test('it permits job run for management tokens', function(assert) {
const mockToken = Service.extend({
selfToken: { type: 'management' },
});
this.owner.register('service:token', mockToken);
const jobAbility = this.owner.lookup('ability:job');
assert.ok(jobAbility.canRun);
});
test('it permits job run for client tokens with a policy that has namespace submit-job', function(assert) {
const mockSystem = Service.extend({
activeNamespace: {
name: 'aNamespace',
},
});
const mockToken = Service.extend({
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'aNamespace',
Capabilities: ['submit-job'],
},
],
},
},
],
});
this.owner.register('service:system', mockSystem);
this.owner.register('service:token', mockToken);
const jobAbility = this.owner.lookup('ability:job');
assert.ok(jobAbility.canRun);
});
test('it permits job run for client tokens with a policy that has default namespace submit-job and no capabilities for active namespace', function(assert) {
const mockSystem = Service.extend({
activeNamespace: {
name: 'anotherNamespace',
},
});
const mockToken = Service.extend({
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'aNamespace',
Capabilities: [],
},
{
Name: 'default',
Capabilities: ['submit-job'],
},
],
},
},
],
});
this.owner.register('service:system', mockSystem);
this.owner.register('service:token', mockToken);
const jobAbility = this.owner.lookup('ability:job');
assert.ok(jobAbility.canRun);
});
test('it blocks job run for client tokens with a policy that has no submit-job capability', function(assert) {
const mockSystem = Service.extend({
activeNamespace: {
name: 'aNamespace',
},
});
const mockToken = Service.extend({
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'aNamespace',
Capabilities: ['list-jobs'],
},
],
},
},
],
});
this.owner.register('service:system', mockSystem);
this.owner.register('service:token', mockToken);
const jobAbility = this.owner.lookup('ability:job');
assert.notOk(jobAbility.canRun);
});
test('it handles globs in namespace names', function(assert) {
const mockSystem = Service.extend({
activeNamespace: {
name: 'aNamespace',
},
});
const mockToken = Service.extend({
selfToken: { type: 'client' },
selfTokenPolicies: [
{
rulesJSON: {
Namespaces: [
{
Name: 'production-*',
Capabilities: ['submit-job'],
},
{
Name: 'production-api',
Capabilities: ['submit-job'],
},
{
Name: 'production-web',
Capabilities: [],
},
{
Name: '*-suffixed',
Capabilities: ['submit-job'],
},
{
Name: '*-more-suffixed',
Capabilities: [],
},
{
Name: '*-abc-*',
Capabilities: ['submit-job'],
},
],
},
},
],
});
this.owner.register('service:system', mockSystem);
this.owner.register('service:token', mockToken);
const jobAbility = this.owner.lookup('ability:job');
const systemService = this.owner.lookup('service:system');
systemService.set('activeNamespace.name', 'production-web');
assert.notOk(jobAbility.canRun);
systemService.set('activeNamespace.name', 'production-api');
assert.ok(jobAbility.canRun);
systemService.set('activeNamespace.name', 'production-other');
assert.ok(jobAbility.canRun);
systemService.set('activeNamespace.name', 'something-suffixed');
assert.ok(jobAbility.canRun);
systemService.set('activeNamespace.name', 'something-more-suffixed');
assert.notOk(
jobAbility.canRun,
'expected the namespace with the greatest number of matched characters to be chosen'
);
systemService.set('activeNamespace.name', '000-abc-999');
assert.ok(jobAbility.canRun, 'expected to be able to match against more than one wildcard');
});
});

View File

@@ -4868,31 +4868,20 @@ ember-basic-dropdown@^2.0.5:
ember-maybe-in-element "^0.4.0"
ember-truth-helpers "2.1.0"
ember-can@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ember-can/-/ember-can-2.0.0.tgz#b01400380b42aedd9570b89521997a34677feab6"
integrity sha512-4c0HcXUC1HiNwGmW7Gp72Ojhlr/uULQTdJp85up4G3MjonCdV0ZdvPLsMIQITBgWqWY/H5HezMjrdaIDFuEDBA==
dependencies:
ember-cli-babel "7.8.0"
ember-inflector "3.0.1"
ember-cli-babel-plugin-helpers@^1.0.0, ember-cli-babel-plugin-helpers@^1.1.0:
version "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@^6.0.0-beta.4, ember-cli-babel@^6.0.0-beta.7, ember-cli-babel@^6.11.0, ember-cli-babel@^6.16.0, ember-cli-babel@^6.18.0, ember-cli-babel@^6.3.0, ember-cli-babel@^6.6.0, ember-cli-babel@^6.7.2, ember-cli-babel@^6.8.0, ember-cli-babel@^6.8.1, ember-cli-babel@^6.8.2, ember-cli-babel@^6.9.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-6.18.0.tgz#3f6435fd275172edeff2b634ee7b29ce74318957"
integrity sha512-7ceC8joNYxY2wES16iIBlbPSxwKDBhYwC8drU3ZEvuPDMwVv1KzxCNu1fvxyFEBWhwaRNTUxSCsEVoTd9nosGA==
dependencies:
amd-name-resolver "1.2.0"
babel-plugin-debug-macros "^0.2.0-beta.6"
babel-plugin-ember-modules-api-polyfill "^2.6.0"
babel-plugin-transform-es2015-modules-amd "^6.24.0"
babel-polyfill "^6.26.0"
babel-preset-env "^1.7.0"
broccoli-babel-transpiler "^6.5.0"
broccoli-debug "^0.6.4"
broccoli-funnel "^2.0.0"
broccoli-source "^1.1.0"
clone "^2.0.0"
ember-cli-version-checker "^2.1.2"
semver "^5.5.0"
ember-cli-babel@^7.1.0, ember-cli-babel@^7.7.3:
ember-cli-babel@7.8.0, ember-cli-babel@^7.1.0, ember-cli-babel@^7.7.3:
version "7.8.0"
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.8.0.tgz#e596500eca0f5a7c9aaee755f803d1542f578acf"
integrity sha512-xUBgJQ81fqd7k/KIiGU+pjpoXhrmmRf9pUrqLenNSU5N+yeNFT5a1+w0b+p1F7oBphfXVwuxApdZxrmAHOdA3Q==
@@ -4919,6 +4908,25 @@ ember-cli-babel@^7.1.0, ember-cli-babel@^7.7.3:
ensure-posix-path "^1.0.2"
semver "^5.5.0"
ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.0.0-beta.7, ember-cli-babel@^6.11.0, ember-cli-babel@^6.16.0, ember-cli-babel@^6.18.0, ember-cli-babel@^6.3.0, ember-cli-babel@^6.6.0, ember-cli-babel@^6.7.2, ember-cli-babel@^6.8.0, ember-cli-babel@^6.8.1, ember-cli-babel@^6.8.2, ember-cli-babel@^6.9.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-6.18.0.tgz#3f6435fd275172edeff2b634ee7b29ce74318957"
integrity sha512-7ceC8joNYxY2wES16iIBlbPSxwKDBhYwC8drU3ZEvuPDMwVv1KzxCNu1fvxyFEBWhwaRNTUxSCsEVoTd9nosGA==
dependencies:
amd-name-resolver "1.2.0"
babel-plugin-debug-macros "^0.2.0-beta.6"
babel-plugin-ember-modules-api-polyfill "^2.6.0"
babel-plugin-transform-es2015-modules-amd "^6.24.0"
babel-polyfill "^6.26.0"
babel-preset-env "^1.7.0"
broccoli-babel-transpiler "^6.5.0"
broccoli-debug "^0.6.4"
broccoli-funnel "^2.0.0"
broccoli-source "^1.1.0"
clone "^2.0.0"
ember-cli-version-checker "^2.1.2"
semver "^5.5.0"
ember-cli-babel@^7.1.2, ember-cli-babel@^7.5.0:
version "7.7.3"
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.7.3.tgz#f94709f6727583d18685ca6773a995877b87b8a0"
@@ -5556,7 +5564,7 @@ ember-getowner-polyfill@^2.0.1, ember-getowner-polyfill@^2.2.0:
ember-cli-version-checker "^2.1.0"
ember-factory-for-polyfill "^1.3.1"
"ember-inflector@^2.0.0 || ^3.0.0", ember-inflector@^3.0.1:
ember-inflector@3.0.1, "ember-inflector@^2.0.0 || ^3.0.0", ember-inflector@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/ember-inflector/-/ember-inflector-3.0.1.tgz#04be6df4d7e4000f6d6bd70787cdc995f77be4ab"
integrity sha512-fngrwMsnhkBt51KZgwNwQYxgURwV4lxtoHdjxf7RueGZ5zM7frJLevhHw7pbQNGqXZ3N+MRkhfNOLkdDK9kFdA==