From 1b865c95280b87b014503d24aa0659e209d27016 Mon Sep 17 00:00:00 2001 From: chen21019 Date: Wed, 30 Sep 2026 00:24:24 +0800 Subject: [PATCH 1/2] Show localized permission notice for denied creation routes --- README.md | 10 +++- app/mixins/require-create-permission.js | 7 +++ config/translation-fallback-prefixes.js | 1 + docs/releases/web-console-1.6.154.md | 24 ++++++++ package-lock.json | 4 +- package.json | 2 +- scripts/check-ui-localization-quality | 1 + .../mixins/require-create-permission-test.js | 59 +++++++++++++++++-- translations/en-us.yaml | 5 ++ translations/ja-jp.yaml | 5 ++ translations/zh-tw.yaml | 5 ++ 11 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 docs/releases/web-console-1.6.154.md diff --git a/README.md b/README.md index 3f1535c337..dbd13d4596 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.153`. It retains the existing Node 24, Ember, Sass, +The current source compatibility target is `1.6.154`. 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. +Release `1.6.154` shows a localized, persistent permission error when a direct +Stack or Service creation URL is denied, then returns to the Stacks list. +Service upgrade URLs continue to use update permission and receive an update +error only when that permission is absent. The shared message covers all +resource types using the route guard. See the +[release note](docs/releases/web-console-1.6.154.md) for source test evidence; +packaged browser and Server acceptance remain pending. + 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 diff --git a/app/mixins/require-create-permission.js b/app/mixins/require-create-permission.js index 131558e908..d7cdcfea81 100644 --- a/app/mixins/require-create-permission.js +++ b/app/mixins/require-create-permission.js @@ -1,7 +1,10 @@ import Mixin from '@ember/object/mixin'; +import { service } from '@ember/service'; import { resolve } from 'rsvp'; export default Mixin.create({ + growl: service(), + intl: service(), requiredCreateType: null, requiredUpdateType: null, updateWhenQueryParam: null, @@ -30,6 +33,10 @@ export default Mixin.create({ return; } + this.get('growl').error( + this.get('intl').t('routePermission.title'), + this.get('intl').t(isUpdate ? 'routePermission.updateDenied' : 'routePermission.denied') + ); return this.get('router').replaceWith('stacks'); }); }, diff --git a/config/translation-fallback-prefixes.js b/config/translation-fallback-prefixes.js index ad39751158..40c89cf787 100644 --- a/config/translation-fallback-prefixes.js +++ b/config/translation-fallback-prefixes.js @@ -16,6 +16,7 @@ module.exports = Object.freeze([ // marker. Keep reviewed English copy as the fallback outside zh-tw. 'resourceLoadError.', 'resourceSaveError.', + 'routePermission.', 'infoMultiStats.', 'newCatalog.permissionDenied', 'newCatalog.projectChanged', diff --git a/docs/releases/web-console-1.6.154.md b/docs/releases/web-console-1.6.154.md new file mode 100644 index 0000000000..4e7ba436f0 --- /dev/null +++ b/docs/releases/web-console-1.6.154.md @@ -0,0 +1,24 @@ +# Web Console 1.6.154 source changes + +Direct navigation to a Stack or Service creation route without the effective +resource schema's create permission previously returned to the Stacks list +without explaining why. The shared route guard now shows one sticky error +before that redirect. Its wording applies to Stack and the Service variants +that use the guard, and is translated in English, Traditional Chinese, and +Japanese. Other shipped locales inherit the English base translation. + +The Service upgrade query continues to check update permission. An allowed +upgrade shows no permission notice; a denied upgrade shows an update-specific +notice, rather than a create error. The redirect target and API behavior are +unchanged. + +Source verification: Chrome 153 QUnit tests for the shared route guard passed +(5/5), covering denied and allowed creation, allowed and denied upgrades, and +an explicit false upgrade query. The localization quality check passed across +12 shipped locales with no missing keys, orphan keys, or invalid ICU messages. +The Node 24 package lock changes only its two root version fields from +1.6.153 to 1.6.154; dependency entries are unchanged. + +Packaged browser acceptance and Server 8080 acceptance remain pending. Source +tests do not establish that a new immutable Server image contains this UI or +that its live permission matrix has passed. diff --git a/package-lock.json b/package-lock.json index 1e1851abcf..80871a793c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.153", + "version": "1.6.154", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.153", + "version": "1.6.154", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/package.json b/package.json index b372e8b49f..cc193ee983 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pasturestack/web-console", - "version": "1.6.153", + "version": "1.6.154", "private": true, "description": "PastureStack browser console for the compatible control platform.", "repository": { diff --git a/scripts/check-ui-localization-quality b/scripts/check-ui-localization-quality index 57ee0f286c..e520f28aba 100755 --- a/scripts/check-ui-localization-quality +++ b/scripts/check-ui-localization-quality @@ -22,6 +22,7 @@ const requiredLocales = [ // These Japanese sign-in and write-error messages are reviewed locally. Other // locales, and unrelated Japanese authentication keys, retain English fallback. const requiredJapanesePrefixes = [ + 'routePermission.', 'resourceLoadError.', 'resourceSaveError.', 'loginPage.localRecovery.', diff --git a/tests/unit/mixins/require-create-permission-test.js b/tests/unit/mixins/require-create-permission-test.js index bffe449e75..4d8f2e320e 100644 --- a/tests/unit/mixins/require-create-permission-test.js +++ b/tests/unit/mixins/require-create-permission-test.js @@ -7,10 +7,13 @@ import RequireCreatePermission from 'ui/mixins/require-create-permission'; module('Unit | Mixin | require create permission'); test('allows a route only when the effective schema exposes POST', function(assert) { - assert.expect(3); + assert.expect(4); let redirects = 0; + let notifications = []; let route = Route.extend(RequireCreatePermission).create({ requiredCreateType: 'stack', + intl: {t: (key) => key}, + growl: {error(title, body) { notifications.push([title, body]); }}, router: EmberObject.create({ replaceWith() { redirects++; @@ -25,15 +28,19 @@ test('allows a route only when the effective schema exposes POST', function(asse }); return route.beforeModel({}).then(() => { assert.strictEqual(redirects, 0, 'an authorized route is not redirected'); + assert.deepEqual(notifications, [], 'an authorized route has no permission notice'); assert.strictEqual(route.get('requiredCreateType'), 'stack', 'the capability is explicit'); run(() => route.destroy()); }); }); test('redirects direct navigation when POST is absent', function(assert) { - assert.expect(3); + assert.expect(4); + let notifications = []; let route = Route.extend(RequireCreatePermission).create({ requiredCreateType: 'service', + intl: {t: (key) => key}, + growl: {error(title, body) { notifications.push([title, body]); }}, router: EmberObject.create({ replaceWith(target) { assert.strictEqual(target, 'stacks', 'the denied route returns to the safe read-only list'); @@ -49,17 +56,22 @@ test('redirects direct navigation when POST is absent', function(assert) { }); return route.beforeModel({}).then((result) => { assert.strictEqual(result, 'redirected', 'the redirect transition is returned'); + assert.deepEqual(notifications, [['routePermission.title', 'routePermission.denied']], + 'a denied create shows exactly one permission notice'); run(() => route.destroy()); }); }); test('an upgrade uses PUT capability without opening create-only routes', function(assert) { - assert.expect(4); + assert.expect(5); let redirects = 0; + let notifications = []; let route = Route.extend(RequireCreatePermission).create({ requiredCreateType: 'service', requiredUpdateType: 'service', updateWhenQueryParam: 'upgrade', + intl: {t: (key) => key}, + growl: {error(title, body) { notifications.push([title, body]); }}, router: EmberObject.create({ replaceWith() { redirects++; @@ -78,17 +90,54 @@ test('an upgrade uses PUT capability without opening create-only routes', functi }); return route.beforeModel({to: {queryParams: {upgrade: 'true'}}}).then(() => { assert.strictEqual(redirects, 0, 'PUT capability preserves the upgrade workflow'); + assert.deepEqual(notifications, [], 'an authorized upgrade has no create permission notice'); assert.strictEqual(route.get('updateWhenQueryParam'), 'upgrade', 'only explicit upgrade flows use PUT'); run(() => route.destroy()); }); }); +test('denied upgrades use the update notice rather than the create notice', function(assert) { + assert.expect(4); + let notifications = []; + let route = Route.extend(RequireCreatePermission).create({ + requiredCreateType: 'service', + requiredUpdateType: 'service', + updateWhenQueryParam: 'upgrade', + intl: {t: (key) => key}, + growl: {error(title, body) { notifications.push([title, body]); }}, + router: EmberObject.create({ + replaceWith(target) { + assert.strictEqual(target, 'stacks', 'the denied upgrade returns to the safe list'); + return 'redirected'; + }, + }), + store: EmberObject.create({ + canCreate() { + assert.ok(false, 'an upgrade must not be evaluated as a create'); + }, + getById(type, id) { + assert.deepEqual([type, id], ['schema', 'service'], 'the update resource type is checked'); + return EmberObject.create({resourceMethods: ['GET']}); + }, + }), + }); + return route.beforeModel({to: {queryParams: {upgrade: 'true'}}}).then((result) => { + assert.strictEqual(result, 'redirected', 'the redirect transition is returned'); + assert.deepEqual(notifications, [['routePermission.title', 'routePermission.updateDenied']], + 'the denied upgrade shows exactly one update permission notice'); + run(() => route.destroy()); + }); +}); + test('upgrade=false remains a create request', function(assert) { - assert.expect(2); + assert.expect(3); + let notifications = []; let route = Route.extend(RequireCreatePermission).create({ requiredCreateType: 'service', requiredUpdateType: 'service', updateWhenQueryParam: 'upgrade', + intl: {t: (key) => key}, + growl: {error(title, body) { notifications.push([title, body]); }}, router: EmberObject.create({ replaceWith(target) { assert.strictEqual(target, 'stacks', 'a denied create request is redirected'); @@ -106,6 +155,8 @@ test('upgrade=false remains a create request', function(assert) { }), }); return route.beforeModel({to: {queryParams: {upgrade: 'false'}}}).then(() => { + assert.deepEqual(notifications, [['routePermission.title', 'routePermission.denied']], + 'upgrade=false uses the create permission notice exactly once'); run(() => route.destroy()); }); }); diff --git a/translations/en-us.yaml b/translations/en-us.yaml index 25863dbeb4..ac136f0cae 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -16,6 +16,11 @@ uiText: branding: recoveryNode: "{appName} recovery node" +routePermission: + title: Action unavailable + denied: You do not have permission to create this resource in this environment. + updateDenied: You do not have permission to update this resource in this environment. + ############################## # Really generic things used in multiple places (use sparingly) ############################## diff --git a/translations/ja-jp.yaml b/translations/ja-jp.yaml index 320374702d..3b7d3e8f28 100644 --- a/translations/ja-jp.yaml +++ b/translations/ja-jp.yaml @@ -13,6 +13,11 @@ uiText: pastureStackCompose: PastureStack Compose branding: recoveryNode: '{appName} 復旧ノード' +routePermission: + title: 操作を実行できません + denied: この環境でこのリソースを作成する権限がありません。 + updateDenied: この環境でこのリソースを更新する権限がありません。 + generic: actions: アクション columns: 列 diff --git a/translations/zh-tw.yaml b/translations/zh-tw.yaml index 213fe77338..1245322828 100644 --- a/translations/zh-tw.yaml +++ b/translations/zh-tw.yaml @@ -13,6 +13,11 @@ uiText: pastureStackCompose: PastureStack Compose branding: recoveryNode: '{appName} 復原節點' +routePermission: + title: 無法執行此操作 + denied: 您沒有權限在此環境中建立此資源。 + updateDenied: 您沒有權限在此環境中更新此資源。 + generic: actions: 操作 columns: 欄位 From 31e148de0aa1e523ae7f5804199753000dd7622f Mon Sep 17 00:00:00 2001 From: chen21019 Date: Wed, 30 Sep 2026 00:28:20 +0800 Subject: [PATCH 2/2] Align Web Console 1.6.154 release gates --- ...m-package-lock.sass-replacement.node24-ignore-scripts.json | 4 ++-- scripts/check-modernization-blockers | 4 ++-- scripts/check-ui-console-workspace | 2 +- scripts/check-ui-critical-high-dependencies | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) 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 1e1851abcf..80871a793c 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.153", + "version": "1.6.154", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.153", + "version": "1.6.154", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/scripts/check-modernization-blockers b/scripts/check-modernization-blockers index 49e2b63402..15b2ae2594 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.153" ]]; then - echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.153" +if [[ "$version" != "1.6.154" ]]; then + echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.154" failures=$((failures + 1)) fi diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace index 4ce16fa820..b4ce7cf3ea 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.153 browser-session broker-broadcast + 1.6.154 browser-session broker-broadcast diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies index d6972f7555..e9a6c92917 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.153": +if package.get("version") != "1.6.154": 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')}")