diff --git a/.changelog/11672.txt b/.changelog/11672.txt new file mode 100644 index 00000000000..07c9c6e8308 --- /dev/null +++ b/.changelog/11672.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: Fix the ACL requirements for displaying the job details page +``` diff --git a/ui/app/abilities/client.js b/ui/app/abilities/client.js index 350200dc81a..f48eeb93e4d 100644 --- a/ui/app/abilities/client.js +++ b/ui/app/abilities/client.js @@ -7,18 +7,30 @@ import classic from 'ember-classic-decorator'; export default class Client extends AbstractAbility { // Map abilities to policy options (which are coarse for nodes) // instead of specific behaviors. + @or('bypassAuthorization', 'selfTokenIsManagement', 'policiesIncludeNodeRead') + canRead; + @or('bypassAuthorization', 'selfTokenIsManagement', 'policiesIncludeNodeWrite') canWrite; @computed('token.selfTokenPolicies.[]') - get policiesIncludeNodeWrite() { - // For each policy record, extract the Node policy - const policies = (this.get('token.selfTokenPolicies') || []) - .toArray() - .map(policy => get(policy, 'rulesJSON.Node.Policy')) - .compact(); + get policiesIncludeNodeRead() { + return policiesIncludePermissions(this.get('token.selfTokenPolicies'), ['read', 'write']); + } - // Node write is allowed if any policy allows it - return policies.some(policy => policy === 'write'); + @computed('token.selfTokenPolicies.[]') + get policiesIncludeNodeWrite() { + return policiesIncludePermissions(this.get('token.selfTokenPolicies'), ['write']); } } + +function policiesIncludePermissions(policies = [], permissions = []) { + // For each policy record, extract the Node policy + const nodePolicies = policies + .toArray() + .map(policy => get(policy, 'rulesJSON.Node.Policy')) + .compact(); + + // Check for requested permissions + return nodePolicies.some(policy => permissions.includes(policy)); +} diff --git a/ui/app/components/job-page/abstract.js b/ui/app/components/job-page/abstract.js index 29beaa6aa12..4239f844fce 100644 --- a/ui/app/components/job-page/abstract.js +++ b/ui/app/components/job-page/abstract.js @@ -5,6 +5,7 @@ import classic from 'ember-classic-decorator'; @classic export default class Abstract extends Component { + @service can; @service system; job = null; @@ -20,6 +21,10 @@ export default class Abstract extends Component { // Set to a { title, description } to surface an error errorMessage = null; + get shouldDisplayClientInformation() { + return this.can.can('read client') && this.job.hasClientStatus; + } + @action clearErrorMessage() { this.set('errorMessage', null); diff --git a/ui/app/components/job-page/parameterized-child.js b/ui/app/components/job-page/parameterized-child.js index daf0e418340..3f941067c06 100644 --- a/ui/app/components/job-page/parameterized-child.js +++ b/ui/app/components/job-page/parameterized-child.js @@ -1,14 +1,11 @@ import { computed } from '@ember/object'; import { alias } from '@ember/object/computed'; -import { inject as service } from '@ember/service'; import PeriodicChildJobPage from './periodic-child'; import classic from 'ember-classic-decorator'; -import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic export default class ParameterizedChild extends PeriodicChildJobPage { @alias('job.decodedPayload') payload; - @service store; @computed('payload') get payloadJSON() { @@ -20,10 +17,4 @@ export default class ParameterizedChild extends PeriodicChildJobPage { } return json; } - - @jobClientStatus('nodes', 'job') jobClientStatus; - - get nodes() { - return this.store.peekAll('node'); - } } diff --git a/ui/app/components/job-page/parts/job-client-status-summary.js b/ui/app/components/job-page/parts/job-client-status-summary.js index 15ac0c45922..515f085688e 100644 --- a/ui/app/components/job-page/parts/job-client-status-summary.js +++ b/ui/app/components/job-page/parts/job-client-status-summary.js @@ -2,20 +2,26 @@ import Component from '@ember/component'; import { action, computed } from '@ember/object'; import { classNames } from '@ember-decorators/component'; import classic from 'ember-classic-decorator'; +import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic @classNames('boxed-section') export default class JobClientStatusSummary extends Component { job = null; - jobClientStatus = null; + nodes = null; + forceCollapsed = false; gotoClients() {} - @computed + @computed('forceCollapsed') get isExpanded() { + if (this.forceCollapsed) return false; + const storageValue = window.localStorage.nomadExpandJobClientStatusSummary; return storageValue != null ? JSON.parse(storageValue) : true; } + @jobClientStatus('nodes', 'job') jobClientStatus; + @action onSliceClick(ev, slice) { this.gotoClients([slice.className.camelize()]); diff --git a/ui/app/components/job-page/periodic-child.js b/ui/app/components/job-page/periodic-child.js index d581d88dc29..dfe42225dc3 100644 --- a/ui/app/components/job-page/periodic-child.js +++ b/ui/app/components/job-page/periodic-child.js @@ -1,13 +1,9 @@ import AbstractJobPage from './abstract'; import { computed } from '@ember/object'; -import { inject as service } from '@ember/service'; import classic from 'ember-classic-decorator'; -import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic export default class PeriodicChild extends AbstractJobPage { - @service store; - @computed('job.{name,id}', 'job.parent.{name,id}') get breadcrumbs() { const job = this.job; @@ -25,10 +21,4 @@ export default class PeriodicChild extends AbstractJobPage { }, ]; } - - @jobClientStatus('nodes', 'job') jobClientStatus; - - get nodes() { - return this.store.peekAll('node'); - } } diff --git a/ui/app/components/job-page/sysbatch.js b/ui/app/components/job-page/sysbatch.js index 0819ed49426..abdbdfdf3df 100644 --- a/ui/app/components/job-page/sysbatch.js +++ b/ui/app/components/job-page/sysbatch.js @@ -1,15 +1,5 @@ import AbstractJobPage from './abstract'; import classic from 'ember-classic-decorator'; -import { inject as service } from '@ember/service'; -import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic -export default class Sysbatch extends AbstractJobPage { - @service store; - - @jobClientStatus('nodes', 'job') jobClientStatus; - - get nodes() { - return this.store.peekAll('node'); - } -} +export default class Sysbatch extends AbstractJobPage {} diff --git a/ui/app/components/job-page/system.js b/ui/app/components/job-page/system.js index 5909c8f163d..bf2c0444246 100644 --- a/ui/app/components/job-page/system.js +++ b/ui/app/components/job-page/system.js @@ -1,15 +1,5 @@ import AbstractJobPage from './abstract'; import classic from 'ember-classic-decorator'; -import { inject as service } from '@ember/service'; -import jobClientStatus from 'nomad-ui/utils/properties/job-client-status'; @classic -export default class System extends AbstractJobPage { - @service store; - - @jobClientStatus('nodes', 'job') jobClientStatus; - - get nodes() { - return this.store.peekAll('node'); - } -} +export default class System extends AbstractJobPage {} diff --git a/ui/app/components/list-accordion/accordion-head.js b/ui/app/components/list-accordion/accordion-head.js index 320092634b1..6ef4f9f2a67 100644 --- a/ui/app/components/list-accordion/accordion-head.js +++ b/ui/app/components/list-accordion/accordion-head.js @@ -9,8 +9,10 @@ export default class AccordionHead extends Component { 'data-test-accordion-head' = true; buttonLabel = 'toggle'; + tooltip = ''; isOpen = false; isExpandable = true; + isDisabled = false; item = null; onClose() {} diff --git a/ui/app/controllers/jobs/job/index.js b/ui/app/controllers/jobs/job/index.js index 2b663959f97..b57a44343a2 100644 --- a/ui/app/controllers/jobs/job/index.js +++ b/ui/app/controllers/jobs/job/index.js @@ -1,5 +1,5 @@ import { inject as service } from '@ember/service'; -import { alias } from '@ember/object/computed'; +import { computed } from '@ember/object'; import Controller from '@ember/controller'; import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting'; import { action } from '@ember/object'; @@ -23,7 +23,15 @@ export default class IndexController extends Controller.extend(WithNamespaceRese currentPage = 1; - @alias('model') job; + @computed('model.job') + get job() { + return this.model.job; + } + + @computed('model.nodes.[]') + get nodes() { + return this.model.nodes; + } sortProperty = 'name'; sortDescending = false; diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index 028ee23bf40..79db30dd155 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -30,6 +30,16 @@ export default class Allocation extends Model { @fragment('resources') allocatedResources; @attr('number') jobVersion; + // Store basic node information returned by the API to avoid the need for + // node:read ACL permission. + @attr('string') nodeName; + @computed + get shortNodeId() { + return this.belongsTo('node') + .id() + .split('-')[0]; + } + @attr('number') modifyIndex; @attr('date') modifyTime; diff --git a/ui/app/routes/jobs/job/index.js b/ui/app/routes/jobs/job/index.js index 463edee0101..b085ca744f5 100644 --- a/ui/app/routes/jobs/job/index.js +++ b/ui/app/routes/jobs/job/index.js @@ -1,4 +1,5 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; import { collect } from '@ember/object/computed'; import { watchRecord, @@ -9,34 +10,49 @@ import { import WithWatchers from 'nomad-ui/mixins/with-watchers'; export default class IndexRoute extends Route.extend(WithWatchers) { + @service can; + @service store; + async model() { - // Optimizing future node look ups by preemptively loading everything - await this.store.findAll('node'); - return this.modelFor('jobs.job'); + const job = this.modelFor('jobs.job'); + if (!job) { + return { job, nodes: [] }; + } + + // Optimizing future node look ups by preemptively loading all nodes if + // necessary and allowed. + if (this.can.can('read client') && job.get('hasClientStatus')) { + await this.store.findAll('node'); + } + const nodes = this.store.peekAll('node'); + return { job, nodes }; } startWatchers(controller, model) { - if (!model) { + if (!model.job) { return; } controller.set('watchers', { - model: this.watch.perform(model), - summary: this.watchSummary.perform(model.get('summary')), - allocations: this.watchAllocations.perform(model), - evaluations: this.watchEvaluations.perform(model), + model: this.watch.perform(model.job), + summary: this.watchSummary.perform(model.job.get('summary')), + allocations: this.watchAllocations.perform(model.job), + evaluations: this.watchEvaluations.perform(model.job), latestDeployment: - model.get('supportsDeployments') && this.watchLatestDeployment.perform(model), + model.job.get('supportsDeployments') && this.watchLatestDeployment.perform(model.job), list: - model.get('hasChildren') && - this.watchAllJobs.perform({ namespace: model.namespace.get('name') }), - nodes: model.get('hasClientStatus') && this.watchNodes.perform(), + model.job.get('hasChildren') && + this.watchAllJobs.perform({ namespace: model.job.namespace.get('name') }), + nodes: + this.can.can('read client') && + model.job.get('hasClientStatus') && + this.watchNodes.perform(), }); } setupController(controller, model) { // Parameterized and periodic detail pages, which list children jobs, // should sort by submit time. - if (model && ['periodic', 'parameterized'].includes(model.templateType)) { + if (model.job && ['periodic', 'parameterized'].includes(model.job.templateType)) { controller.setProperties({ sortProperty: 'submitTime', sortDescending: true, diff --git a/ui/app/templates/components/allocation-row.hbs b/ui/app/templates/components/allocation-row.hbs index efa1915524c..934d69336d6 100644 --- a/ui/app/templates/components/allocation-row.hbs +++ b/ui/app/templates/components/allocation-row.hbs @@ -1,8 +1,10 @@ - {{#if this.allocation.unhealthyDrivers.length}} - - {{x-icon "alert-triangle" class="is-warning"}} - + {{#if (can "read client")}} + {{#if this.allocation.unhealthyDrivers.length}} + + {{x-icon "alert-triangle" class="is-warning"}} + + {{/if}} {{/if}} {{#if this.allocation.nextAllocation}} @@ -38,20 +40,28 @@ {{#if (eq this.context "volume")}} - - - {{this.allocation.node.shortId}} - + + {{#if (can "read client")}} + + {{this.allocation.shortNodeId}} + + {{else}} + {{this.allocation.shortNodeId}} + {{/if}} {{/if}} {{#if (or (eq this.context "taskGroup") (eq this.context "job"))}} {{this.allocation.jobVersion}} - - - {{this.allocation.node.shortId}} - + + {{#if (can "read client")}} + + {{this.allocation.shortNodeId}} + + {{else}} + {{this.allocation.shortNodeId}} + {{/if}} {{else if (or (eq this.context "node") (eq this.context "volume"))}} diff --git a/ui/app/templates/components/job-page/parameterized-child.hbs b/ui/app/templates/components/job-page/parameterized-child.hbs index 83c6f1b0f97..e42afa68ab3 100644 --- a/ui/app/templates/components/job-page/parameterized-child.hbs +++ b/ui/app/templates/components/job-page/parameterized-child.hbs @@ -16,13 +16,13 @@ {{#if this.job.hasClientStatus}} + @nodes={{this.nodes}} + @forceCollapsed={{not this.shouldDisplayClientInformation}} + @gotoClients={{this.gotoClients}} /> {{/if}} - + diff --git a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs index 837a6c34a0f..09eee819a2e 100644 --- a/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs +++ b/ui/app/templates/components/job-page/parts/job-client-status-summary.hbs @@ -1,22 +1,28 @@ - +
Job Status in Client - - {{this.jobClientStatus.totalNodes}} - + {{#if this.jobClientStatus}} + + {{this.jobClientStatus.totalNodes}} + + {{/if}} {{x-icon "info-circle-outline" class="is-faded"}}
- {{#unless a.isOpen}} + {{#if (and this.jobClientStatus (not a.isOpen))}}
- {{/unless}} + {{/if}}
- -
    - {{#each chart.data as |datum index|}} -
  1. - {{#if (gt datum.value 0)}} - + {{#if this.jobClientStatus}} + +
      + {{#each chart.data as |datum index|}} +
    1. + {{#if (gt datum.value 0)}} + + + + {{else}} - - {{else}} - - {{/if}} -
    2. - {{/each}} -
    -
    + {{/if}} +
  2. + {{/each}} +
+
+ {{/if}}
diff --git a/ui/app/templates/components/job-page/periodic-child.hbs b/ui/app/templates/components/job-page/periodic-child.hbs index 65bcad3cc93..61ec3e17974 100644 --- a/ui/app/templates/components/job-page/periodic-child.hbs +++ b/ui/app/templates/components/job-page/periodic-child.hbs @@ -16,13 +16,13 @@ {{#if this.job.hasClientStatus}} + @nodes={{this.nodes}} + @forceCollapsed={{not this.shouldDisplayClientInformation}} + @gotoClients={{this.gotoClients}} /> {{/if}} - + diff --git a/ui/app/templates/components/job-page/sysbatch.hbs b/ui/app/templates/components/job-page/sysbatch.hbs index c5fd40faaa3..429002bf0fb 100644 --- a/ui/app/templates/components/job-page/sysbatch.hbs +++ b/ui/app/templates/components/job-page/sysbatch.hbs @@ -7,10 +7,11 @@ - + diff --git a/ui/app/templates/components/job-page/system.hbs b/ui/app/templates/components/job-page/system.hbs index 76b1ad7c225..16a85e4405b 100644 --- a/ui/app/templates/components/job-page/system.hbs +++ b/ui/app/templates/components/job-page/system.hbs @@ -13,10 +13,11 @@ - + diff --git a/ui/app/templates/components/job-subnav.hbs b/ui/app/templates/components/job-subnav.hbs index 67d9974dbc5..eb33c641961 100644 --- a/ui/app/templates/components/job-subnav.hbs +++ b/ui/app/templates/components/job-subnav.hbs @@ -8,7 +8,7 @@ {{/if}}
  • Allocations
  • Evaluations
  • - {{#if (and this.job.hasClientStatus (not this.job.hasChildren))}} + {{#if (and (can "read client") (and this.job.hasClientStatus (not this.job.hasChildren)))}}
  • Clients
  • {{/if}} diff --git a/ui/app/templates/components/list-accordion/accordion-head.hbs b/ui/app/templates/components/list-accordion/accordion-head.hbs index 16359b72402..52e9931ddf3 100644 --- a/ui/app/templates/components/list-accordion/accordion-head.hbs +++ b/ui/app/templates/components/list-accordion/accordion-head.hbs @@ -3,7 +3,9 @@