Skip to content

Task Issue 970 codetracker management Write Gating

github-actions[bot] edited this page Sep 25, 2026 · 1 revision

Task 970 — Implement codetracker_management write + UI-action gating in the Code Trackers screen (gap vs legacy)

Issue: #970 Status: IMPLEMENTED & VERIFIED (2026-09-25) — branch task/issue-970-codetracker-management-gate

The gap

Legacy delimited read from write capability on Code Trackers:

  • lib/codetrackers/codeTrackerView.php:24 → $gui->canManage = hasRight('codetracker_management').
  • gui/templates/dashio/codetrackers/codeTrackerView.tpl: the Create button (:91-96), the wrench "check connection" link + connection icons (:51-61), the edit-link around the tracker name (:64-70) and the delete icon (:76, additionally only when link_count == 0) are ALL rendered only {if $gui->canManage != ""}.
  • The write controller lib/codetrackers/codeTrackerEdit.php:181-184 is reachable only via checkRights() = codetracker_management.

The modern BFF api/codetracker/index.php (after the #969 read gate) allowed any viewer to write: POST / (create), PUT /{id} (update), DELETE /{id} and POST /test_github all executed for a user holding only codetracker_view — a privilege escalation.

Measured evidence gathered during INVESTIGATION (before the fix)

Fixture: ctviewonly (role 99, only codetracker_view right id 52, password admin).

  • POST / → 200, tracker INSERTed; PUT /1 rename → 200; DELETE /1 → 200; POST /test_github → 200.
  • GET list → {status:ok, items:[], total:0} — canManage key ABSENT.
  • UI: + Create Code Tracker button rendered for the view-only user.

Server-side enforcement

api/codetracker/index.php:

$canManage = ($user->hasRight($db, 'codetracker_management') == 'yes');

function denyWrite($user, $userId, $action) {
    logAuditEvent(TLS('audit_security_user_right_missing', $user->login,
                      'api/codetracker/index.php', $action),
                  'WRITE', $userId, 'codetrackers');   // trailed BEFORE the denial
    http_response_code(403);
    echo json_encode(['status' => 'error', 'message' => 'No permission']);
    exit;
}

Every write route starts with if (!$canManage) { denyWrite($user, $userId, '<op>'); }:

  • POST / (create) → create; PUT /{id} → update; DELETE /{id} → delete; POST /test_github → test_github.

GET / now returns "canManage" (mirrors legacy $gui->canManage) and each item gains "link_count" so the UI can mirror the legacy link_count == 0 delete condition.

Note: hasRight() returns 'yes'/null (roles.inc.php:254-274), so == 'yes' normalization is required for clean JSON true/false.

Client-side gating

gui/templates/codetracker/codetrackerView.html:

  • canManage = !!r.canManage from the GET payload.
  • $('#createBtn').toggle(canManage) — Create button management-only.
  • Edit icon only when canManage; Delete icon only when canManage && link_count === 0.
  • table.column(4).visible(canManage) collapses the Actions column for view-only users (mirrors legacy <th> gating).

No new i18n keys required — all gated labels reuse existing keys; no bundle changes.

Screenshots

View-only user ctviewonly — read-only list, no Create button, no Actions column

View-only: read-only list, no manage affordances

Admin (manager) — Create button + Actions (edit/delete) column

Admin: full manage UI

Verification (measured)

Call (as ctviewonly) Result
POST / create 403 No permission
PUT /2 update 403
DELETE /2 403
POST /test_github 403
GET / list 200 canManage:false

Admin: GET / → canManage:true; modal create → 200 (id 2); rename PUT → 200; linked fixture (link_count=1) hides delete icon; delete flow (id 3) → 200.

Event Viewer: log_level IN (1,2) → 0 rows; 13 events all AUDIT (16). Test suite Suite 970 — Task — Issue #970 in tl/TLU_Test_Cases.md: 10/10 PASS.

Clone this wiki locally