diff --git a/CHANGELOG.md b/CHANGELOG.md index 19f530e..ad274dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ All notable changes to **Tiger Core** (`webtigers/tiger-core`). Format follows ## [Unreleased] +## [1.36.0] — 2026-10-21 + +### Changed +- **TIGER-335 migration (batch 5)** — migrated the `_isAtLeastAdmin()` call sites (the worst-defect form) + in **comment** (TIGER-343) and **newsletter** (TIGER-344). Comment's "is a moderator" checks (edit/ + delete-another's, the edit-window bypass, the moderate + datatable gates) now ask the **moderation + capability** `_isAllowed('moderate')` instead of `inheritsRole('admin')`; newsletter's admin gates ask + `_isAllowed('')`. Behavior-preserving — `moderate`/`datatable`/the Audience grants resolve to + the same admin-and-inheriting-roles set the old helper matched. Updated the acl.ini comments that cited + the removed helper. After this, no non-test code calls `_isAtLeastAdmin()`. + ## [1.35.0] — 2026-10-21 ### Changed diff --git a/library/Tiger/Version.php b/library/Tiger/Version.php index d476547..d0942fc 100644 --- a/library/Tiger/Version.php +++ b/library/Tiger/Version.php @@ -9,5 +9,5 @@ class Tiger_Version { /** Current Tiger Core version. Keep in lockstep with the git tag cut for a release. */ - const VERSION = '1.35.0'; + const VERSION = '1.36.0'; } diff --git a/modules/comment/configs/acl.ini b/modules/comment/configs/acl.ini index 0ba8c5f..2e3c106 100644 --- a/modules/comment/configs/acl.ini +++ b/modules/comment/configs/acl.ini @@ -15,7 +15,7 @@ ; This matters for DISCOVERY as much as dispatch: the /api catalog + MCP `tools/list` reflect ; exactly the methods a role is ALLOWED, so a blanket guest allow advertised the admin ops ; (comment__comment__{moderate,datatable,delete}) to anonymous callers. Execution was already -; gated in-service by _isAtLeastAdmin()/ownership (defense-in-depth) — this stops ADVERTISING +; gated in-service by _isAllowed('moderate')/ownership (defense-in-depth) — this stops ADVERTISING ; them. Every call still re-checks the SUBJECT's own ACL resource, so a thread never exposes ; content the caller can't see. [production] @@ -44,7 +44,7 @@ acl.rules.comment_delete.resource = "Comment_Service_Comment" acl.rules.comment_delete.privilege = "delete" acl.rules.comment_delete.permission = "allow" -; Admin-only: the moderation queue's data + actions (also enforced in-service by _isAtLeastAdmin()). +; Admin-only: the moderation queue's data + actions (also enforced in-service by _isAllowed('moderate')). acl.rules.comment_moderate.role = "admin" acl.rules.comment_moderate.resource = "Comment_Service_Comment" acl.rules.comment_moderate.privilege = "moderate" diff --git a/modules/comment/services/Comment.php b/modules/comment/services/Comment.php index d6249c2..c38beda 100644 --- a/modules/comment/services/Comment.php +++ b/modules/comment/services/Comment.php @@ -179,9 +179,9 @@ public function edit(array $params): void $userId = (string) ($this->_user_id ?? ''); $mine = $userId !== '' && (string) $row->user_id === $userId; - if (!$mine && !$this->_isAtLeastAdmin()) { $this->_error('core.api.error.not_allowed'); return; } + if (!$mine && !$this->_isAllowed('moderate')) { $this->_error('core.api.error.not_allowed'); return; } - if ($mine && !$this->_isAtLeastAdmin() && (time() - strtotime((string) $row->created_at)) > Tiger_Comment::editWindow()) { + if ($mine && !$this->_isAllowed('moderate') && (time() - strtotime((string) $row->created_at)) > Tiger_Comment::editWindow()) { $this->_error('comment.error.edit_window'); return; } @@ -220,7 +220,7 @@ public function edit(array $params): void public function moderate(array $params): void { // Authorization BEFORE the feature flag: "not allowed" must never be masked by "not enabled". - if (!$this->_isAtLeastAdmin()) { $this->_error('core.api.error.not_allowed'); return; } + if (!$this->_isAllowed('moderate')) { $this->_error('core.api.error.not_allowed'); return; } if (!$this->_enabled()) { return; } $status = (string) ($params['status'] ?? ''); @@ -257,7 +257,7 @@ public function delete(array $params): void $userId = (string) ($this->_user_id ?? ''); $mine = $userId !== '' && (string) $row->user_id === $userId; - if (!$mine && !$this->_isAtLeastAdmin()) { $this->_error('core.api.error.not_allowed'); return; } + if (!$mine && !$this->_isAllowed('moderate')) { $this->_error('core.api.error.not_allowed'); return; } try { $this->_transaction(function () use ($model, $row) { @@ -279,7 +279,7 @@ public function delete(array $params): void */ public function datatable(array $params): void { - if (!$this->_isAtLeastAdmin()) { $this->_error('core.api.error.not_allowed'); return; } + if (!$this->_isAllowed('moderate')) { $this->_error('core.api.error.not_allowed'); return; } if (!$this->_enabled()) { return; } $dt = $this->_dtParams(); diff --git a/modules/comment/services/Settings.php b/modules/comment/services/Settings.php index 269127e..ce52082 100644 --- a/modules/comment/services/Settings.php +++ b/modules/comment/services/Settings.php @@ -18,7 +18,7 @@ class Comment_Service_Settings extends Tiger_Service_Service */ public function save(array $params): void { - if (!$this->_isAdmin()) { $this->_error('core.api.error.not_allowed'); return; } + if (!$this->_isAllowed('save')) { $this->_error('core.api.error.not_allowed'); return; } $wanted = !empty($params['spam_agent']); diff --git a/modules/newsletter/configs/acl.ini b/modules/newsletter/configs/acl.ini index b1688a6..cedc190 100644 --- a/modules/newsletter/configs/acl.ini +++ b/modules/newsletter/configs/acl.ini @@ -14,7 +14,7 @@ ; the admin method (datatable) and the audience service (segments/members) are granted only to ; admin+. This governs DISCOVERY as well as dispatch: the /api catalog + MCP tools/list reflect the ; methods a role is ALLOWED, so a blanket guest allow would advertise the admin ops to anonymous -; callers. Execution is also re-checked in-service by _isAtLeastAdmin() (defense-in-depth). +; callers. Execution is also re-checked in-service by _isAllowed() (defense-in-depth). [production] acl.resources.newsletter_subscribe_svc.resource = "Newsletter_Service_Subscribe" @@ -41,7 +41,7 @@ acl.rules.newsletter_index_ctrl.role = "guest" acl.rules.newsletter_index_ctrl.resource = "Newsletter_IndexController" acl.rules.newsletter_index_ctrl.permission = "allow" -; Admin-only: the subscriber grid's data (also re-checked in-service by _isAtLeastAdmin()). +; Admin-only: the subscriber grid's data (also re-checked in-service by _isAllowed('datatable')). acl.rules.newsletter_datatable.role = "admin" acl.rules.newsletter_datatable.resource = "Newsletter_Service_Subscribe" acl.rules.newsletter_datatable.privilege = "datatable" diff --git a/modules/newsletter/services/Audience.php b/modules/newsletter/services/Audience.php index 7685499..f750d2c 100644 --- a/modules/newsletter/services/Audience.php +++ b/modules/newsletter/services/Audience.php @@ -23,14 +23,14 @@ class Newsletter_Service_Audience extends Tiger_Service_Service /** /api (admin): the newsletter segments an email tool / export can target. */ public function segments(array $params): void { - if (!$this->_isAtLeastAdmin()) { $this->_error('core.api.error.not_allowed'); return; } + if (!$this->_isAllowed('segments')) { $this->_error('core.api.error.not_allowed'); return; } $this->_success(['segments' => self::listSegments((string) ($this->_org_id ?? ''))]); } /** /api (admin): resolve a segment to its contacts. @param array $params segment */ public function members(array $params): void { - if (!$this->_isAtLeastAdmin()) { $this->_error('core.api.error.not_allowed'); return; } + if (!$this->_isAllowed('members')) { $this->_error('core.api.error.not_allowed'); return; } $this->_success(['members' => self::resolveMembers((string) ($this->_org_id ?? ''), (string) ($params['segment'] ?? ''))]); } diff --git a/modules/newsletter/services/Subscribe.php b/modules/newsletter/services/Subscribe.php index d1c9add..175c242 100644 --- a/modules/newsletter/services/Subscribe.php +++ b/modules/newsletter/services/Subscribe.php @@ -162,7 +162,7 @@ public function unsubscribe(array $params): void */ public function datatable(array $params): void { - if (!$this->_isAtLeastAdmin()) { $this->_error('core.api.error.not_allowed'); return; } + if (!$this->_isAllowed('datatable')) { $this->_error('core.api.error.not_allowed'); return; } $dt = $this->_dtParams($params); $status = (string) ($params['status'] ?? '');