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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@ 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
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
Expand Down
7 changes: 7 additions & 0 deletions app/mixins/require-create-permission.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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');
});
},
Expand Down
1 change: 1 addition & 0 deletions config/translation-fallback-prefixes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
24 changes: 24 additions & 0 deletions docs/releases/web-console-1.6.154.md
Original file line number Diff line number Diff line change
@@ -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.
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.153",
"version": "1.6.154",
"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.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

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.153 browser-session broker-broadcast
1.6.154 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.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')}")
Expand Down
1 change: 1 addition & 0 deletions scripts/check-ui-localization-quality
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
59 changes: 55 additions & 4 deletions tests/unit/mixins/require-create-permission-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
Expand All @@ -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');
Expand All @@ -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++;
Expand All @@ -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');
Expand All @@ -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());
});
});
5 changes: 5 additions & 0 deletions translations/en-us.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
##############################
Expand Down
5 changes: 5 additions & 0 deletions translations/ja-jp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ uiText:
pastureStackCompose: PastureStack Compose
branding:
recoveryNode: '{appName} 復旧ノード'
routePermission:
title: 操作を実行できません
denied: この環境でこのリソースを作成する権限がありません。
updateDenied: この環境でこのリソースを更新する権限がありません。

generic:
actions: アクション
columns: 列
Expand Down
5 changes: 5 additions & 0 deletions translations/zh-tw.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ uiText:
pastureStackCompose: PastureStack Compose
branding:
recoveryNode: '{appName} 復原節點'
routePermission:
title: 無法執行此操作
denied: 您沒有權限在此環境中建立此資源。
updateDenied: 您沒有權限在此環境中更新此資源。

generic:
actions: 操作
columns: 欄位
Expand Down
Loading