Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,24 @@ PastureStack is an independent community effort to preserve, audit, and moderniz

## Project status

The current source compatibility target is `1.6.152`. It retains the existing Node 24, Ember, Sass,
The current source compatibility target is `1.6.153`. It retains the existing Node 24, Ember, Sass,
dependency, browser-smoke, terminal, console, and test-harness modernization.
It adds a provider-neutral OpenID Connect administration and sign-in flow with
PKCE S256, staged configuration validation, a real test login before
activation, and local-authentication recovery. Product-owned names, logos,
icons, package metadata, and visible text use PastureStack branding. API
models and protocol fields remain compatible.

Release `1.6.153` makes Stack, Service, and Container write controls check
their current project/resource capability when the user acts; delayed project
upgrades and service scaling cannot carry a click into a different selected
project. It improves Registry edit recovery, localized errors and Japanese
form labels, and shows an unavailable state when host/container monitoring
cannot connect instead of leaving a spinner. These are browser-console
changes, not a substitute for Server-side per-resource authorization. See
the [release note](docs/releases/web-console-1.6.153.md) for test evidence
and the remaining 8080 acceptance boundary.

Release `1.6.152` uses each Stack, Service, and Container form's visible,
translated name label in its required-field error. This closes the
Chinese/Japanese Stack mismatch observed on isolated Server v1.6.485 QA;
Expand Down
49 changes: 40 additions & 9 deletions app/catalog-tab/launch/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Route from '@ember/routing/route';
import EmberObject, { get } from '@ember/object';
import C from 'ui/utils/constants';
import { catalogVersionOptions } from 'ui/utils/catalog-version-options';
import RequireCreatePermission from 'ui/mixins/require-create-permission';
import Errors from 'ui/utils/errors';

function resourceValue(resource, path) {
if ( !resource ) {
Expand All @@ -18,33 +18,63 @@ function resourceValue(resource, path) {
return get(resource, path);
}

export default Route.extend(RequireCreatePermission, {
export default Route.extend({
catalog: service(),

requiredCreateType: 'stack',
requiredUpdateType: 'stack',
updateWhenQueryParam: 'upgrade',
intl: service(),
projects: service(),

parentRoute: 'catalog-tab',

unavailable(key, status=403) {
return {status, code: status === 404 ? 'NotFound' : 'Forbidden', messageKey: key, message: this.get('intl').t(key)};
},

fetchTemplate(id, upgrade=false) {
return this.get('catalog').fetchTemplate(id, upgrade).catch((err) => {
let status = Errors.status(err);
if ( status === 403 || status === 404 ) {
throw this.unavailable(upgrade ? 'newCatalog.upgradeUnavailable' : 'newCatalog.templateUnavailable', 404);
}
throw err;
});
},

model: function(params/*, transition*/) {
var store = this.get('store');
let projectId = this.get('projects.current.id');

if ( !params.stackId && !this.get('projects').canCreateResource('stack') ) {
throw this.unavailable('newCatalog.permissionDenied');
}
if ( params.upgrade && !params.stackId ) {
throw this.unavailable('newCatalog.upgradeUnavailable');
}

var dependencies = {
tpl: this.get('catalog').fetchTemplate(params.template),
tpl: this.fetchTemplate(params.template),
};

if ( params.upgrade )
{
dependencies.upgrade = this.get('catalog').fetchTemplate(params.upgrade, true);
dependencies.upgrade = this.fetchTemplate(params.upgrade, true);
}

if ( params.stackId )
{
dependencies.stack = store.find('stack', params.stackId);
dependencies.stack = store.find('stack', params.stackId).catch((err) => {
let status = Errors.status(err);
if ( status === 403 || status === 404 ) {
throw this.unavailable('resourceLoadError.stackUnavailable', 404);
}
throw err;
});
}

return hash(dependencies, 'Load dependencies').then((results) => {
if ( results.stack && !resourceValue(results.stack, 'actionLinks.upgrade') ) {
throw this.unavailable('newCatalog.upgradeUnavailable');
}

if ( !results.stack )
{
results.stack = store.createRecord({
Expand Down Expand Up @@ -73,6 +103,7 @@ export default Route.extend(RequireCreatePermission, {
let verArr = catalogVersionOptions(links, currentOption);

return EmberObject.create({
projectId,
stack: results.stack,
tpl: results.tpl,
upgrade: results.upgrade,
Expand Down
1 change: 1 addition & 0 deletions app/catalog-tab/launch/template.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{{new-catalog
allTemplates=this.model.allTemplates
stackResource=this.model.stack
projectId=this.model.projectId
templateResource=this.model.tpl
versionLinks=this.model.versionLinks
versionsArray=this.model.versionsArray
Expand Down
47 changes: 44 additions & 3 deletions app/components/edit-registry/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,26 @@ export default ModalBase.extend(NewOrEdit, {
editing: true,
primaryResource: null,
intl: service(),
projects: service(),
missingCredential: false,
credentialSaveAttempted: false,
credentialOutcomeUnknown: false,

canSaveCredential: function() {
let projectId = this.get('originalModel.projectId');
let currentProjectId = this.get('projects.current.id');
if ( !currentProjectId || this.get('projects.schemaProjectId') !== currentProjectId ||
projectId !== currentProjectId ) {
return false;
}

let credential = this.get('originalModel.credential');
return credential ? Boolean(credential.get('actionLinks.update')) :
this.get('projects').canCreateResource('registryCredential');
}.property('originalModel.credential', 'originalModel.credential.actionLinks.update',
'originalModel.projectId', 'projects.current.id', 'projects.schemaProjectId',
'projects.schemaLoadGeneration'),

init: function() {
this._super(...arguments);
var orig = this.get('originalModel');
Expand Down Expand Up @@ -46,18 +62,37 @@ export default ModalBase.extend(NewOrEdit, {
},

doSave: function() {
if ( !this.get('canSaveCredential') ) {
throw {status: 403, code: 'Forbidden', messageKey: 'resourceSaveError.unavailable'};
}

if ( !this.get('missingCredential') ) {
const credential = this.get('primaryResource');
const data = {
publicValue: credential.get('publicValue'),
secretValue: credential.get('secretValue'),
};

return this._super({data});
const registryId = this.get('originalModel.registry.id');
const saveCredential = this._super.bind(this);
return this.get('originalModel.registry.store').find('registrycredential', credential.get('id'), {forceReload: true}).then((fresh) => {
if ( !fresh || fresh.get('registryId') !== registryId ) {
throw {status: 404, code: 'NotFound', messageKey: 'resourceSaveError.unavailable'};
}
if ( !this.get('canSaveCredential') || !fresh.get('actionLinks.update') ) {
throw {status: 403, code: 'Forbidden', messageKey: 'resourceSaveError.unavailable'};
}

return saveCredential({data});
});
}

const registry = this.get('originalModel.registry');
return registry.get('store').find('registrycredential', null, {forceReload: true}).then((credentials) => {
if ( !this.get('canSaveCredential') ) {
throw {status: 403, code: 'Forbidden', messageKey: 'resourceSaveError.unavailable'};
}

const existing = credentialsForRegistry(credentials, registry.get('id'));
if ( existing.get('length') ) {
throw new Error(this.get('intl').t('editRegistry.credentialAppeared'));
Expand All @@ -66,8 +101,14 @@ export default ModalBase.extend(NewOrEdit, {
throw new Error(this.get('intl').t('editRegistry.credentialOutcomeUnknown'));
}

this.set('credentialSaveAttempted', true);
return resolve().then(() => this.get('primaryResource').save()).then(
return resolve().then(() => {
if ( !this.get('canSaveCredential') ) {
throw {status: 403, code: 'Forbidden', messageKey: 'resourceSaveError.unavailable'};
}

this.set('credentialSaveAttempted', true);
return this.get('primaryResource').save();
}).then(
(saved) => this.mergeResult(saved),
(error) => {
this.set('credentialOutcomeUnknown', !definitelyRejected(error));
Expand Down
2 changes: 1 addition & 1 deletion app/components/edit-registry/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@
</div>
</section>

{{save-cancel editing=this.editing save="save" cancel="cancel"}}
{{save-cancel editing=this.editing save="save" cancel="cancel" saveDisabled=(not this.canSaveCredential)}}
15 changes: 13 additions & 2 deletions app/components/info-multi-stats/component.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cancel, next } from '@ember/runloop';
import { alias, and, not } from '@ember/object/computed';
import { alias, and, not, or } from '@ember/object/computed';
import { service } from '@ember/service';
import Component from '@ember/component';
import bb from 'billboard.js';
Expand Down Expand Up @@ -93,7 +93,18 @@ export default Component.extend({
active: alias('statsSocket.active'),
loading: alias('statsSocket.loading'),
notRenderOk: not('renderOk'),
waitingForData: and('available', 'notRenderOk'),
statsError: alias('statsSocket.connectError'),
statsErrorStatus: alias('statsSocket.connectErrorStatus'),
noStatsError: not('statsError'),
noStatsAvailable: not('available'),
showUnavailable: or('noStatsAvailable', 'statsError'),
waitingForData: and('available', 'notRenderOk', 'noStatsError'),
unavailableMessage: function() {
const status = this.get('statsErrorStatus');
const key = status === 401 ? 'authError' : status === 403 ? 'permissionError' :
status === 404 ? 'notFound' : 'utilizationStats';
return this.get('intl').t(`infoMultiStats.${key}`);
}.property('statsErrorStatus'),

cpuCanvas: '#cpuGraph',
cpuGraph: null,
Expand Down
16 changes: 8 additions & 8 deletions app/components/info-multi-stats/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
<div class="text-center text-muted {{unless this.waitingForData 'hide'}}" style="height: 100px; padding-top: 30px;">
<i class="icon icon-spinner icon-spin"></i> {{t 'infoMultiStats.connecting'}}
</div>
<div class="text-center text-muted {{if this.available 'hide'}}" style="height: 100px; padding-top: 30px;">
{{t 'infoMultiStats.utilizationStats'}}
<div class="text-center text-muted {{unless this.showUnavailable 'hide'}}" style="height: 100px; padding-top: 30px;">
{{this.unavailableMessage}}
</div>
</div>
</div>
Expand All @@ -36,8 +36,8 @@
<div class="text-center text-muted {{unless this.waitingForData 'hide'}}" style="height: 100px; padding-top: 30px;">
<i class="icon icon-spinner icon-spin"></i> {{t 'infoMultiStats.connecting'}}
</div>
<div class="text-center text-muted {{if this.available 'hide'}}" style="height: 100px; padding-top: 30px;">
{{t 'infoMultiStats.utilizationStats'}}
<div class="text-center text-muted {{unless this.showUnavailable 'hide'}}" style="height: 100px; padding-top: 30px;">
{{this.unavailableMessage}}
</div>
</div>
</div>
Expand All @@ -57,8 +57,8 @@
<div class="text-center text-muted {{unless this.waitingForData 'hide'}}" style="height: 100px; padding-top: 30px;">
<i class="icon icon-spinner icon-spin"></i> {{t 'infoMultiStats.connecting'}}
</div>
<div class="text-center text-muted {{if this.available 'hide'}}" style="height: 100px; padding-top: 30px;">
{{t 'infoMultiStats.utilizationStats'}}
<div class="text-center text-muted {{unless this.showUnavailable 'hide'}}" style="height: 100px; padding-top: 30px;">
{{this.unavailableMessage}}
</div>
</div>
</div>
Expand All @@ -77,8 +77,8 @@
<div class="text-center text-muted {{unless this.waitingForData 'hide'}}" style="height: 100px; padding-top: 30px;">
<i class="icon icon-spinner icon-spin"></i> {{t 'infoMultiStats.connecting'}}
</div>
<div class="text-center text-muted {{if this.available 'hide'}}" style="height: 100px; padding-top: 30px;">
{{t 'infoMultiStats.utilizationStats'}}
<div class="text-center text-muted {{unless this.showUnavailable 'hide'}}" style="height: 100px; padding-top: 30px;">
{{this.unavailableMessage}}
</div>
</div>
</div>
Expand Down
18 changes: 11 additions & 7 deletions app/components/lb-addtl-info/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,18 @@
</div>
<div class="r-mt10">
<label class="text-muted">{{t 'lbAddtlInfo.scale'}}</label>
{{#if this.service.canScale}}
<span class="r-ml20 r-mr20">{{this.service.scale}}</span>
<div class="btn-group btn-group-xs">
<button class="btn btn-default btn-xs" {{action "scaleDown" target=this.service}} disabled={{eq this.service.scale 1}}><i class="icon icon-minus"></i></button>
<button class="btn btn-default btn-xs" {{action "scaleUp" target=this.service}}><i class="icon icon-plus"></i></button>
</div>
{{#if this.service.isGlobalScale}}
{{t 'lbAddtlInfo.global'}}
{{else}}
{{if this.service.isGlobalScale (t 'lbAddtlInfo.global')}}
{{#if this.service.isReal}}
<span class="r-ml20 r-mr20">{{this.service.scale}}</span>
{{#if this.service.canScale}}
<div class="btn-group btn-group-xs">
<button class="btn btn-default btn-xs" {{action "scaleDown" target=this.service}} disabled={{eq this.service.scale 1}}><i class="icon icon-minus"></i></button>
<button class="btn btn-default btn-xs" {{action "scaleUp" target=this.service}}><i class="icon icon-plus"></i></button>
</div>
{{/if}}
{{/if}}
{{/if}}
</div>
</div>
Expand Down
40 changes: 37 additions & 3 deletions app/components/new-catalog/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default Component.extend(NewOrEdit, {
allTemplates: null,
templateResource: null,
stackResource: null,
projectId: null,
versionsArray: null,
versionsLinks: null,
actuallySave: true,
Expand All @@ -44,6 +45,29 @@ export default Component.extend(NewOrEdit, {
primaryResource: alias('stackResource'),
templateBase: alias('templateResource.templateBase'),
editing: notEmpty('stackResource.id'),
canSubmit: computed('actuallySave', 'editing', 'projectId', 'projects.current.id',
'projects.schemaProjectId', 'projects.schemaLoadGeneration',
'stackResource.actionLinks.upgrade', function() {
if ( !this.get('actuallySave') ) {
return true;
}

let projectId = this.get('projectId');
if ( !projectId || this.get('projects.current.id') !== projectId ||
this.get('projects.schemaProjectId') !== projectId ) {
return false;
}

return this.get('editing') ? !!this.get('stackResource.actionLinks.upgrade') :
this.get('projects').canCreateResource('stack');
}),
submissionErrorKey: computed('editing', 'projectId', 'projects.current.id', function() {
if ( this.get('editing') ) {
return 'newCatalog.upgradeUnavailable';
}
return this.get('projectId') === this.get('projects.current.id') ?
'newCatalog.permissionDenied' : 'newCatalog.projectChanged';
}),

previewOpen: false,
previewTab: null,
Expand Down Expand Up @@ -310,13 +334,13 @@ export default Component.extend(NewOrEdit, {
var errors = [];

if (!this.get('editing') && !this.get('stackResource.name')) {
errors.push('Name is required');
errors.push(this.get('intl').t('validation.required', {key: this.get('intl').t('generic.name')}));
}

if (this.get('selectedTemplateModel.questions')) {
this.get('selectedTemplateModel.questions').forEach((item) => {
if (item.required && isCatalogQuestionAnswerMissing(item.answer)) {
errors.push(`${item.label} is required`);
errors.push(this.get('intl').t('validation.required', {key: item.label}));
}
});
}
Expand Down Expand Up @@ -370,6 +394,10 @@ export default Component.extend(NewOrEdit, {

doSave() {
var stack = this.get('stackResource');
if ( !this.get('canSubmit') ) {
let messageKey = this.get('submissionErrorKey');
throw {status: 403, code: 'Forbidden', messageKey, message: this.get('intl').t(messageKey)};
}
if (this.get('editing')) {
return stack.doAction('upgrade', {
dockerCompose: stack.get('dockerCompose'),
Expand All @@ -383,7 +411,13 @@ export default Component.extend(NewOrEdit, {
},

doneSaving() {
var projectId = this.get('projects.current.id');
const projectId = this.get('projectId');
// A save can finish after the user has selected another environment.
// Never navigate a completed stack ID under that newer environment.
if (!projectId || this.isDestroying || this.isDestroyed ||
this.get('projects.current.id') !== projectId) {
return;
}
if ( this.get('stackResource.system') )
{
return this.get('router').transitionTo('stack', projectId, this.get('primaryResource.id'), {queryParams: {which: 'infra'}});
Expand Down
Loading
Loading