From 63964fa3a6da5cbd452cdc1061c1e18340055362 Mon Sep 17 00:00:00 2001 From: chen21019 Date: Wed, 30 Sep 2026 18:21:56 +0800 Subject: [PATCH] fix: explain certificate reference deletion failures --- COMPATIBILITY.md | 7 ++++ README.md | 10 +++++- app/services/growl.js | 2 +- app/utils/errors.js | 19 +++++++++- ...ass-replacement.node24-ignore-scripts.json | 4 +-- docs/releases/web-console-1.6.160.md | 26 ++++++++++++++ package-lock.json | 4 +-- package.json | 2 +- scripts/check-modernization-blockers | 4 +-- scripts/check-ui-console-workspace | 2 +- scripts/check-ui-critical-high-dependencies | 2 +- tests/unit/services/growl-test.js | 12 +++++++ tests/unit/utils/errors-test.js | 35 +++++++++++++++++++ translations/en-us.yaml | 1 + translations/ja-jp.yaml | 1 + translations/zh-tw.yaml | 1 + 16 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 docs/releases/web-console-1.6.160.md diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 1047e2c44a..0b26cb673f 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -4,6 +4,13 @@ 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.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 +how to release the reference. It never renders the response's load-balancer +names. `403`, `404`, and unrecognized `405` responses remain neutral. This is +display-only; API authorization, DELETE/remove and authentication are unchanged. + The published package identity is `@pasturestack/web-console`, while the Ember 2 runtime keeps the neutral internal `ui/` module prefix used by existing imports. The static server artifact must contain a fingerprinted `/assets/ui*.js` entry and matching `index.html` reference; changing either side requires a coordinated Server packaging test. Before release, validate login and logout, environment selection, hosts, stacks, services, containers, shell, logs, console, catalog, storage, networking, access control, settings, API errors, browser navigation, `en-US`, and `zh-TW` against an isolated compatible server. diff --git a/README.md b/README.md index 0155643b08..a916fc1664 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ PastureStack is an independent community effort to preserve, audit, and moderniz ## Project status -The current source compatibility target is `1.6.159`. It retains the existing Node 24, Ember, Sass, +The current source compatibility target 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 PKCE S256, staged configuration validation, a real test login before @@ -16,6 +16,14 @@ 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. +The `1.6.160` source adds a clear, localized explanation when the API rejects +deleting a certificate still referenced by a load balancer. It tells the user +to remove those references first, without exposing service names. Denied or +missing resources and unrelated action failures retain neutral messages; +authorization, delete behavior, session and MFA contracts do not change. See +the [release note](docs/releases/web-console-1.6.160.md). Publication and live +acceptance evidence are recorded separately from this source target. + Release `1.6.159` preserves an existing Registry credential's password when the editor changes only its username or leaves the password input blank. The editor submits a password only when a new nonempty value is entered, preserving diff --git a/app/services/growl.js b/app/services/growl.js index c9f9b70635..3e93e7b9ca 100644 --- a/app/services/growl.js +++ b/app/services/growl.js @@ -59,7 +59,7 @@ export default Service.extend({ // Growls also report deletes and resource actions, not just saves. // Keep denied/missing-resource details private without calling them saves. var body = status === 403 || status === 404 || status === 405 ? - this.get('intl').t('resourceSaveError.actionUnavailable') : + this.get('intl').t(Errors.actionMessageKey(err) || 'resourceSaveError.actionUnavailable') : Errors.stringify(err, this.get('intl')); this.error(title,body); }, diff --git a/app/utils/errors.js b/app/utils/errors.js index 91ce0be964..23af35be11 100644 --- a/app/utils/errors.js +++ b/app/utils/errors.js @@ -146,6 +146,20 @@ function nestedStatus(value, seen, depth) { return null; } +function actionMessageKey(err) { + // Only this established, post-authorization lifecycle rejection is specific. + // Do not expose the server's load balancer names, or specialize 403/404. + if ( nestedStatus(err, [], 0) !== 405 || + nestedStringField(err, 'code', [], 0) !== 'InvalidAction' ) { + return null; + } + + let message = nestedStringField(err, 'message', [], 0); + let prefix = 'Certificate is in use by load balancer services: '; + return message && message.startsWith(prefix) && message.length > prefix.length ? + 'resourceSaveError.certificateInUse' : null; +} + export default { stringify(err, intl) { if ( intl && typeof intl.t === 'function' ) { @@ -154,7 +168,8 @@ export default { if ( status === 403 || status === 404 || status === 405 ) { // A denied resource and a missing resource must have the same visible // explanation. Client-created errors can supply a more specific key. - let key = nonEmptyString(fieldValue(err, 'messageKey')) || 'resourceSaveError.unavailable'; + let key = actionMessageKey(err) || nonEmptyString(fieldValue(err, 'messageKey')) || + 'resourceSaveError.unavailable'; return intl.t(key); } @@ -283,4 +298,6 @@ export default { status(err) { return nestedStatus(err, [], 0); }, + + actionMessageKey, }; diff --git a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json index daeba9fb9a..a85968a51b 100644 --- a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json +++ b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.159", + "version": "1.6.160", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.159", + "version": "1.6.160", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/docs/releases/web-console-1.6.160.md b/docs/releases/web-console-1.6.160.md new file mode 100644 index 0000000000..de44715f33 --- /dev/null +++ b/docs/releases/web-console-1.6.160.md @@ -0,0 +1,26 @@ +# Web Console 1.6.160 + +Certificate deletion could correctly fail at the Engine's reference guard +while the console displayed only a generic unavailable-or-denied notification. +This release gives the known `405 / InvalidAction` certificate-in-use response +a reviewed explanation: remove the load-balancer service's certificate +references before deleting the certificate. + +The shared error formatter and growl service use the same classification. +English, Traditional Chinese and Japanese are covered. The server response's +service names and IDs are not displayed. A `403` or `404`, including one with a +nested certificate message, still receives the same neutral explanation. Other +`405` responses are not guessed to be certificate-in-use errors. + +No API status, authorization, Certificate mutation, authentication, session, +OIDC, MFA or proxy behavior is changed. Unit coverage includes direct API error +models, nested response envelopes, missing or unrelated codes/messages, +non-disclosure, all three locales, and the existing validation formatting. + +Focused native headless Chrome 153 QUnit validation passed 32/32 tests with the +`/errors|growl/` filter. This includes the formatter/growl tests and adjacent +existing error-display tests, not the full suite. Both localization-quality +and Traditional Chinese completeness checks passed with no missing keys. + +Release publication and native browser acceptance are separate gates. Full +resource/role-matrix completion is not implied by these formatter tests. diff --git a/package-lock.json b/package-lock.json index daeba9fb9a..a85968a51b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.159", + "version": "1.6.160", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.159", + "version": "1.6.160", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/package.json b/package.json index e21961feb4..f7d91bc2d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pasturestack/web-console", - "version": "1.6.159", + "version": "1.6.160", "private": true, "description": "PastureStack browser console for the compatible control platform.", "repository": { diff --git a/scripts/check-modernization-blockers b/scripts/check-modernization-blockers index 0e5b914c86..adaae6b7c5 100755 --- a/scripts/check-modernization-blockers +++ b/scripts/check-modernization-blockers @@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f: print(json.load(f).get('version', '')) PY ) -if [[ "$version" != "1.6.159" ]]; then - echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.159" +if [[ "$version" != "1.6.160" ]]; then + echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.160" failures=$((failures + 1)) fi diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace index 0fc9e0576d..d9fd1f39e8 100755 --- a/scripts/check-ui-console-workspace +++ b/scripts/check-ui-console-workspace @@ -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.159 browser-session broker-broadcast + 1.6.160 browser-session broker-broadcast diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies index ecc04e6ea1..8ec9140c60 100755 --- a/scripts/check-ui-critical-high-dependencies +++ b/scripts/check-ui-critical-high-dependencies @@ -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.159": +if package.get("version") != "1.6.160": 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')}") diff --git a/tests/unit/services/growl-test.js b/tests/unit/services/growl-test.js index 1a08d0917f..f7d4f0e62f 100644 --- a/tests/unit/services/growl-test.js +++ b/tests/unit/services/growl-test.js @@ -64,6 +64,18 @@ test('denied deletes and actions use neutral, private-safe copy in all supported }, `${locale} ${status} does not call a delete failure a save or expose its resource ID`); } + let inUse = {status: 405, code: 'InvalidAction', + message: 'Certificate is in use by load balancer services: private-balancer 1s-secret'}; + growl.fromError('Delete failed', inUse); + assert.ok(messages['resourceSaveError.certificateInUse'], `${locale} has certificate lifecycle copy`); + assert.strictEqual(notifications.pop().body, messages['resourceSaveError.certificateInUse'], + `${locale} explains the blocked delete without disclosing service names`); + for (let status of [403, 404]) { + growl.fromError('Delete failed', {status, body: inUse}); + assert.strictEqual(notifications.pop().body, messages['resourceSaveError.actionUnavailable'], + `${locale} denied/missing resources retain the same neutral growl`); + } + growl.fromError('Validation failed', {status: 422, fieldName: 'name', detail: 'already used'}); assert.ok(notifications.pop().body.startsWith(messages['resourceSaveError.validation']), `${locale} validation still uses the existing localized formatter`); diff --git a/tests/unit/utils/errors-test.js b/tests/unit/utils/errors-test.js index 136adc8ce5..8c977da5c8 100644 --- a/tests/unit/utils/errors-test.js +++ b/tests/unit/utils/errors-test.js @@ -30,6 +30,41 @@ test('finds authentication status codes in nested request failures', function(as assert.strictEqual(Errors.status({xhr: {status: 0}}), null, 'a network failure is not authentication failure'); }); +test('only recognized authorized certificate lifecycle errors have specific action copy', function(assert) { + let message = 'Certificate is in use by load balancer services: private-balancer'; + let body = {status: 405, code: 'InvalidAction', message}; + let key = 'resourceSaveError.certificateInUse'; + assert.strictEqual(Errors.actionMessageKey(body), key, 'the existing API contract is recognized'); + assert.strictEqual(Errors.actionMessageKey(ApiError.create(body)), key, 'API error models are supported'); + assert.strictEqual(Errors.actionMessageKey({xhr: {status: 405, responseJSON: body}}), key, + 'nested response envelopes use the same classification'); + for (let status of [403, 404]) { + assert.strictEqual(Errors.actionMessageKey({status, body}), null, + `${status} remains neutral even with a nested lifecycle message`); + } + for (let error of [ + {status: 405, message}, + {status: 405, code: 'ActionNotAvailable', message}, + {status: 405, code: 'InvalidAction', message: 'Another action is unavailable'}, + {status: 405, code: 'InvalidAction', message: 'Certificate is in use by load balancer services:'}, + ]) { + assert.strictEqual(Errors.actionMessageKey(error), null, 'other lifecycle failures are not guessed'); + } +}); + +test('certificate-in-use copy is localized and never reveals referenced service names', async function(assert) { + for (let locale of ['en-us', 'zh-tw', 'ja-jp']) { + let response = await fetch(`/translations/${locale}.json`); + let messages = await response.json(); + let key = 'resourceSaveError.certificateInUse'; + assert.ok(messages[key], `${locale} has a human-readable explanation`); + let error = ApiError.create({status: 405, code: 'InvalidAction', + message: 'Certificate is in use by load balancer services: 1s-secret'}); + assert.strictEqual(Errors.stringify(error, {t: k => messages[k]}), messages[key], + `${locale} shows only reviewed copy, not service names or raw API text`); + } +}); + test('save errors have useful reviewed copy in English, Traditional Chinese, and Japanese', async function(assert) { for (let locale of ['en-us', 'zh-tw', 'ja-jp']) { let response = await fetch(`/translations/${locale}.json`); diff --git a/translations/en-us.yaml b/translations/en-us.yaml index 07209e1402..c4b0b2edbd 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -1018,6 +1018,7 @@ resourceSaveError: scaleFailed: The service scale could not be updated. unavailable: The save could not be completed. The resource may be unavailable, or you may not have permission. Refresh to check what was saved before retrying. actionUnavailable: The action could not be completed. The resource may be unavailable, or you may not have permission. Refresh to check its current state before trying again. + certificateInUse: This certificate is still used by a load balancer service. Remove its certificate references before deleting it. validation: The server rejected the changes. Check the fields and try again. failed: The save could not be completed. Refresh to check what was saved before retrying. diff --git a/translations/ja-jp.yaml b/translations/ja-jp.yaml index b779c89be1..615fe5e915 100644 --- a/translations/ja-jp.yaml +++ b/translations/ja-jp.yaml @@ -932,6 +932,7 @@ resourceSaveError: scaleFailed: サービスの数を更新できませんでした。 unavailable: 保存を完了できませんでした。リソースを利用できないか、権限がない可能性があります。再試行する前にページを再読み込みして、保存済みの内容を確認してください。 actionUnavailable: 操作を完了できませんでした。リソースを利用できないか、権限がない可能性があります。再試行する前にページを再読み込みして、現在の状態を確認してください。 + certificateInUse: この証明書はロードバランサーサービスで使用中です。サービスの証明書参照を解除してから、この証明書を削除してください。 validation: サーバーが変更を受け付けませんでした。入力項目を確認して、もう一度お試しください。 failed: 保存を完了できませんでした。再試行する前にページを再読み込みして、保存済みの内容を確認してください。 haPage: diff --git a/translations/zh-tw.yaml b/translations/zh-tw.yaml index bf7c5db725..69e910dd86 100644 --- a/translations/zh-tw.yaml +++ b/translations/zh-tw.yaml @@ -988,6 +988,7 @@ resourceSaveError: scaleFailed: 無法更新服務數量。 unavailable: 無法完成儲存。資源可能無法使用,或您沒有權限。請先重新整理並確認已儲存的內容,再重試。 actionUnavailable: 無法完成此操作。資源可能無法使用,或您沒有權限。請先重新整理並確認目前狀態,再重試。 + certificateInUse: 此憑證仍被負載平衡服務使用。請先解除服務的憑證參照,再刪除此憑證。 validation: 伺服器未接受變更。請檢查欄位後重試。 failed: 無法完成儲存。請先重新整理並確認已儲存的內容,再重試。 haPage: