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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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('<method>')`. 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
Expand Down
2 changes: 1 addition & 1 deletion library/Tiger/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
4 changes: 2 additions & 2 deletions modules/comment/configs/acl.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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"
Expand Down
10 changes: 5 additions & 5 deletions modules/comment/services/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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'] ?? '');
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion modules/comment/services/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);

Expand Down
4 changes: 2 additions & 2 deletions modules/newsletter/configs/acl.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions modules/newsletter/services/Audience.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'] ?? ''))]);
}

Expand Down
2 changes: 1 addition & 1 deletion modules/newsletter/services/Subscribe.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'] ?? '');
Expand Down
Loading