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
8 changes: 8 additions & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Web Console preserves compatible API paths, schema and resource names, action na

Visible branding, product-owned assets, icon identifiers, package metadata, and operator documentation use PastureStack. Historical identifiers remain only where they are server data or protocol contracts and must not be mechanically replaced.

Web Console `1.6.161` preserves an existing Certificate's masked key when only
name/description are changed. Explicit update-validation options apply only to
persisted records and explicitly omitted fields; other required and supplied
value checks remain strict. The editor omits unchanged material from metadata
PUTs, without modifying authorization, storage, create or replacement contracts.
Its existing hint and key marker distinguish metadata edits from replacements.
The earlier failed native editor receipt is not promoted to a pass.

Web Console `1.6.160` recognizes the established post-authorization Certificate
in-use response (`405`, `InvalidAction`, and the known API message prefix),
showing reviewed English, Traditional Chinese or Japanese copy that explains
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ PastureStack is an independent community effort to preserve, audit, and moderniz

## Project status

Release `1.6.161` is being prepared to fix existing Certificate metadata edits
blocked by the masked private key. Name/description-only edits omit certificate
material from the PUT body; new certificates and material replacements keep
full validation. The editor explains this distinction in all supported locales.
Focused real-model and rendered three-language UI tests passed 25/25; final CI,
publication and isolated browser acceptance are still pending. See the
[preparation note](docs/releases/web-console-1.6.161.md).

The current published release is `1.6.160`. 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
Expand Down
29 changes: 26 additions & 3 deletions app/components/edit-certificate/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,38 @@ export default ModalBase.extend(NewOrEdit, CertificateKeyValidation, {
this.set('model', this.get('originalModel').clone());
},

isMetadataOnlyUpdate() {
const model = this.get('model');
const original = this.get('originalModel');
const value = (record, field) => record?.get?.(field);
const normalize = (material) => typeof material === 'string' ? material.trim() : material == null ? '' : material;
const cert = value(original, 'cert');

// Schema validation trims the clone. Compare both sides consistently, but
// never send those trimmed, unchanged PEM values back in a metadata PUT.
return Boolean(value(original, 'id') && value(model, 'id') === value(original, 'id') &&
value(model, 'type') === 'certificate' && value(original, 'type') === 'certificate' &&
typeof cert === 'string' && cert.trim() && normalize(value(model, 'cert')) === normalize(cert) &&
normalize(value(model, 'certChain')) === normalize(value(original, 'certChain')) &&
normalize(value(original, 'key')) === '' &&
normalize(value(model, 'key')) === '');
},

validate() {
return this._super(this.isMetadataOnlyUpdate() ? {updateOmittedFields: ['key']} : undefined);
},

doSave() {
const model = this.get('model');
const data = {
name: model.get('name'),
description: model.get('description'),
cert: model.get('cert'),
key: model.get('key'),
certChain: model.get('certChain'),
};
if ( !this.isMetadataOnlyUpdate() ) {
data.cert = model.get('cert');
data.key = model.get('key');
data.certChain = model.get('certChain');
}

return this._super({data});
},
Expand Down
2 changes: 1 addition & 1 deletion app/components/edit-certificate/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<div class="alert alert-info">{{t 'editCertificate.noteKeyWriteOnly'}}</div>
</div>

{{input-certificate model=this.model}}
{{input-certificate model=this.model keyRequired=false}}
</section>

{{top-errors errors=this.errors}}
Expand Down
1 change: 1 addition & 0 deletions app/components/input-certificate/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Component from '@ember/component';

export default Component.extend({
model: null,
keyRequired: true,

tagName: 'div',
classNames: ['row'],
Expand Down
2 changes: 1 addition & 1 deletion app/components/input-certificate/template.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="col-md-4 form-group">
<div class="clearfix r-mb10 r-mt15">
<label class="r-pt5">{{t 'inputCertificate.key.label'}}*</label>
<label class="r-pt5">{{t 'inputCertificate.key.label'}}{{#if this.keyRequired}}*{{/if}}</label>
</div>
{{input-text-file
value=this.model.key
Expand Down
6 changes: 4 additions & 2 deletions app/mixins/cattle-transitioning-resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ export default Mixin.create({
}
},

validationErrors: function() {
validationErrors: function(options) {
let intl = this.get('intl');

var errors = [];
Expand Down Expand Up @@ -478,7 +478,9 @@ export default Mixin.create({

var len = (val ? get(val,'length') : 0);

if ( field.required && (val === null || (typeof val === 'string' && len === 0) || (isArray(val) && len === 0) ) )
const omittedOnUpdate = this.get('id') && Array.isArray(options?.updateOmittedFields) &&
options.updateOmittedFields.includes(key);
if ( field.required && !omittedOnUpdate && (val === null || (typeof val === 'string' && len === 0) || (isArray(val) && len === 0) ) )
{
errors.push(intl.t('validation.required', {key: displayKey}));
continue;
Expand Down
4 changes: 2 additions & 2 deletions app/mixins/new-or-edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ export default Mixin.create({
this.set('saving',false);
},

validate: function() {
validate: function(options) {
var model = this.get('primaryResource');
var errors = model.validationErrors();
var errors = model.validationErrors(options);
if ( errors.get('length') )
{
this.set('errors', errors);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.160",
"version": "1.6.161",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
"version": "1.6.160",
"version": "1.6.161",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
Expand Down
37 changes: 37 additions & 0 deletions docs/releases/web-console-1.6.161.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Web Console 1.6.161

Preparation only; publication and packaged browser acceptance are pending.

The existing Certificate editor applied the create-schema required-key check
even though the API does not return the private key. The native owner editor
on isolated Server `v1.6.494` failed with `KEY_REQUIRED` before any resource
write. This patch fixes that editor rather than bypassing its validation.

For a persisted matching Certificate with a masked key and unchanged
certificate/chain, validation explicitly permits omitting the key, and the PUT
body contains only `name` and `description`. The original PEM material is not
resent after the shared validator trims the clone. The decision is recomputed
when saving; no persistent bypass flag is used.

New certificates, changed or cleared certificate/chain values, and supplied
replacement keys keep the ordinary validation and five-field replacement body.
Encrypted private keys remain rejected. The shared validator's default remains
strict; this is not a blanket exception for write-only or required fields.

The existing hint now explains metadata preservation and the corresponding-key
requirement for replacements in all 13 locales. A masked edit key is no longer
marked unconditionally required; the create form retains its required marker.

Focused native Chrome 153 QUnit validation passed 25/25 tests including the final
hint and template changes. Coverage uses the real Certificate clone, schema and trim
chain, metadata-only and replacement payloads, required/format checks, encrypted
keys, sync/async save failures and the complete save/callback/lock lifecycle.
Rendered English, Traditional Chinese and Japanese controls preserve the create
required marker and remove it for masked edit keys. The existing hint is present
in all 13 locales; locale checks report zero missing, orphan or invalid ICU keys.
Full official CI, deterministic archive publication and owner/member packaged
browser acceptance remain pending.

No API, authorization, authentication, session, OIDC, MFA, proxy, firewall or
stored-data contract changes are introduced. Earlier failed browser receipts
remain failures; component tests do not complete the resource/role matrix.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.160",
"version": "1.6.161",
"private": true,
"description": "PastureStack browser console for the compatible control platform.",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-modernization-blockers
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f:
print(json.load(f).get('version', ''))
PY
)
if [[ "$version" != "1.6.160" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.160"
if [[ "$version" != "1.6.161" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.161"
failures=$((failures + 1))
fi

Expand Down
2 changes: 1 addition & 1 deletion scripts/check-ui-console-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,4 @@ if [[ -n ${PASTURESTACK_PRIVATE_MARKER:-} ]] && grep -RInF -- "$PASTURESTACK_PRI
fi

printf 'UI_CONSOLE_WORKSPACE_OK version=%s persistence=%s cross_tab=%s\n' \
1.6.160 browser-session broker-broadcast
1.6.161 browser-session broker-broadcast
2 changes: 1 addition & 1 deletion scripts/check-ui-critical-high-dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ if lock_bytes != baseline_bytes:
lock = json.loads(lock_bytes)
packages = lock.get("packages", {})
root = packages.get("", {})
if package.get("version") != "1.6.160":
if package.get("version") != "1.6.161":
fail(f"unexpected Web Console version: {package.get('version')}")
if root.get("version") != package.get("version"):
fail(f"lock root version differs: {root.get('version')}")
Expand Down
50 changes: 50 additions & 0 deletions tests/integration/components/input-certificate-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import EmberObject from '@ember/object';
import Component from '@ember/component';
import { precompileTemplate } from '@ember/template-compilation';
import { findAll, render, setupContext, setupRenderingContext, teardownContext } from '@ember/test-helpers';
import { module, test } from 'qunit';
import resolver from '../../helpers/resolver';
import { initialize as initializePodLayouts } from 'ui/initializers/pod-component-layouts';
import { initialize as initializeIntl } from 'ui/instance-initializers/intl';

module('Integration | Component | input certificate', function(hooks) {
hooks.beforeEach(async function() {
this.testRoot = document.createElement('div');
this.testRoot.id = 'ember-testing';
document.body.appendChild(this.testRoot);
await setupContext(this, {resolver});
initializePodLayouts();
initializeIntl(this.owner);
this.intl = this.owner.lookup('service:intl');
this.owner.register('component:input-text-file', Component.extend({tagName: 'div'}));
await setupRenderingContext(this);
this.model = EmberObject.create({key: null, cert: 'SYNTHETIC', certChain: null});
});

hooks.afterEach(async function() {
await teardownContext(this);
this.testRoot.remove();
});

test('create and edit key indicators follow their explicit validation context in three locales', async function(assert) {
for (const locale of ['en-us', 'zh-tw', 'ja-jp']) {
this.intl.addTranslations(locale, await (await fetch(`/translations/${locale}.json`)).json());
this.intl.setLocale([locale, 'en-us']);
await render(precompileTemplate('{{input-certificate model=this.model}}'));
assert.true(findAll('label')[0].textContent.trim().endsWith('*'), `${locale}: create requires a key`);
await render(precompileTemplate('{{input-certificate model=this.model keyRequired=false}}'));
const labels = findAll('label').map(label => label.textContent.trim());
assert.false(labels[0].endsWith('*'), `${locale}: masked edit key is not unconditionally required`);
assert.true(labels[1].endsWith('*'), `${locale}: certificate remains required`);
assert.false(labels[2].endsWith('*'), `${locale}: chain remains optional`);
assert.strictEqual(this.model.get('key'), null, 'rendering never fills a masked private key');
}
});

test('the metadata-preservation explanation exists in every supported locale', async function(assert) {
for (const locale of ['de-de', 'en-us', 'fa-ir', 'fil-ph', 'fr-fr', 'hu-hu', 'ja-jp', 'ko-kr', 'pt-br', 'ru-ru', 'uk-ua', 'zh-hans', 'zh-tw']) {
const messages = await (await fetch(`/translations/${locale}.json`)).json();
assert.ok(messages['editCertificate.noteKeyWriteOnly'], `${locale}: existing hint is translated`);
}
});
});
Loading
Loading