Restrict API keys to public-visibility tickets by default (#70)
api/tickets_api.php (both the single-ticket read and the list/triage path) bypassed ticket visibility entirely for any 'read'-scope key, regardless of who it was issued to or what it was for — any key got blanket read access to Confidential and Internal ticket titles, descriptions, and comments, with no way to scope a key more narrowly. Added see_all_visibility to api_keys (migration 006), defaulting to false for both new and existing keys — the prior blanket-access behavior is what's being restricted here, so unlike scope's own un-migrated-database fallback (which defaults toward preserving old behavior), a missing/null value here defaults to the new, restrictive one. An admin can opt a specific key in via a new checkbox in the API Key Management UI when it genuinely needs the full queue. tickets_api.php now builds a synthetic "no special access" user and runs it through TicketModel's existing per-user visibility plumbing (getVisibilityFilter/canUserAccessTicket) instead of a separate SQL path, so this stays in lockstep with however visibility rules evolve for real users. That synthetic user_id is -1, not 0: testing surfaced that canUserAccessTicket()'s confidential-ticket check does a PHP-level (int) cast, and (int)null === 0, so an unassigned confidential ticket's NULL assigned_to would otherwise false-positive-match a user_id of 0. Verified against real MariaDB with public/confidential/internal test tickets: a public-only-scoped key's list only returns the public ticket, and canUserAccessTicket() correctly returns false for both the confidential ticket (unassigned, then reassigned to a real user — both cases) and the internal one; a see_all_visibility key sees all three, unchanged from the prior behavior. Also verified createKey()/ validateKey()'s default-false and explicit-true paths round-trip correctly through the real DB. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117oBw2jN4kALYeS8HPq4zV
This commit is contained in:
@@ -45,10 +45,18 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<option value="read">read</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="lt-form-group" style="flex:1;margin:0">
|
||||
<label class="lt-label" style="display:flex;align-items:center;gap:0.4rem;cursor:pointer">
|
||||
<input type="checkbox" id="keySeeAllVisibility">
|
||||
See all visibility
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="lt-btn lt-btn-primary" style="margin-bottom:0">GENERATE KEY</button>
|
||||
</form>
|
||||
<p class="lt-text-xs lt-text-muted" style="margin-top:0.5rem">
|
||||
Scope: <strong>read</strong> = GET only; <strong>read_write</strong> = create/comment/close.
|
||||
By default a key only sees <strong>public</strong>-visibility tickets — check
|
||||
<strong>See all visibility</strong> only if this key genuinely needs Confidential/Internal tickets too.
|
||||
</p>
|
||||
|
||||
<!-- New key display (hidden by default) -->
|
||||
@@ -74,6 +82,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Key Prefix</th>
|
||||
<th scope="col">Scope</th>
|
||||
<th scope="col">Visibility</th>
|
||||
<th scope="col">Created By</th>
|
||||
<th scope="col">Created</th>
|
||||
<th scope="col">Expires</th>
|
||||
@@ -86,7 +95,7 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<?php
|
||||
$apiKeysList = $apiKeys['keys'] ?? [];
|
||||
if (empty($apiKeysList)) : ?>
|
||||
<tr><td colspan="9" class="lt-empty">No API keys found. Generate one above.</td></tr>
|
||||
<tr><td colspan="10" class="lt-empty">No API keys found. Generate one above.</td></tr>
|
||||
<?php else :
|
||||
foreach ($apiKeysList as $key) : ?>
|
||||
<?php
|
||||
@@ -103,6 +112,13 @@ include __DIR__ . '/../../views/layout_header.php';
|
||||
<span class="lt-status lt-status-open"><?= htmlspecialchars($scope) ?></span>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td data-label="Visibility">
|
||||
<?php if (!empty($key['see_all_visibility'])) : ?>
|
||||
<span class="lt-status lt-status-open" title="Bypasses ticket visibility — sees Confidential/Internal tickets too">all</span>
|
||||
<?php else : ?>
|
||||
<span class="lt-status lt-status-closed" title="Only sees public-visibility tickets">public only</span>
|
||||
<?php endif ?>
|
||||
</td>
|
||||
<td data-label="Created By" class="lt-text-xs"><?= htmlspecialchars($key['display_name'] ?? $key['username'] ?? 'Unknown') ?></td>
|
||||
<td data-label="Created" class="lt-text-xs lt-text-muted"><?= date('Y-m-d H:i', strtotime($key['created_at'])) ?></td>
|
||||
<td data-label="Expires" class="lt-text-xs <?= $expired ? 'lt-text-danger' : 'lt-text-cyan' ?>">
|
||||
@@ -239,11 +255,17 @@ document.addEventListener('click', function (e) {
|
||||
|
||||
document.getElementById('generateKeyForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
var keyName = document.getElementById('keyName').value.trim();
|
||||
var expiresIn = document.getElementById('expiresIn').value;
|
||||
var keyScope = document.getElementById('keyScope').value;
|
||||
var keyName = document.getElementById('keyName').value.trim();
|
||||
var expiresIn = document.getElementById('expiresIn').value;
|
||||
var keyScope = document.getElementById('keyScope').value;
|
||||
var seeAllVisibility = document.getElementById('keySeeAllVisibility').checked;
|
||||
if (!keyName) { lt.toast.error('Please enter a key name'); return; }
|
||||
lt.api.post('/api/generate_api_key.php', { key_name: keyName, expires_in_days: expiresIn || null, scope: keyScope })
|
||||
lt.api.post('/api/generate_api_key.php', {
|
||||
key_name: keyName,
|
||||
expires_in_days: expiresIn || null,
|
||||
scope: keyScope,
|
||||
see_all_visibility: seeAllVisibility
|
||||
})
|
||||
.then(function (data) {
|
||||
if (data.success) {
|
||||
document.getElementById('newKeyValue').value = data.api_key;
|
||||
|
||||
Reference in New Issue
Block a user