Roles stage 3: roles and agents in the API and Admin > API Keys rebuilt; new api/v1/roles/read.php where an admin key reads every role with its level per module and agent count, archived on request, and any other key reads only its own user's role, and new api/v1/technicians/read.php listing agents with their role under Support at Read; API role refusals now say what the call needs and what the key's user has, naming the module, level and role, and technicians/time.php needs Read instead of Modify; Admin > API Keys shows each key's agent and role, what it can open and for which clients, whether it works (works until, expires in N days, expired, or won't work with the reason when its agent is disabled, archived, has no role or is missing) and when it was last used, with search by key or agent name; New and Edit windows show the picked agent's access as they are picked and offer only active agents, Create before ticking the copied box opens the Key tab, new keys default to a year and must end after today, and an expired key can be renamed and stay expired; handlers moved to functions/api_keys.php with validation, escaped messages and audit entries naming the agent and what an edit changed, fixing empty names on revoke, delete and bulk delete, a backslash shown for apostrophes in names, the bulk count of keys ticked rather than deleted, keys created already expired and expired keys that could not be renamed; database 3.1.7 adds api_keys.api_key_last_used_at, set at most once a minute in the configured timezone; changelog updated

This commit is contained in:
johnnyq
2026-10-17 21:10:48 -04:00
parent 5d9b48e83e
commit 2a5533ea2b
13 changed files with 611 additions and 278 deletions

View File

@@ -1,20 +1,21 @@
<?php
// Default Column Sortby Filter
$sort = "api_key_name";
$order = "ASC";
/*
* Admin > API Keys - the keys other systems use the API with. Each acts as
* an agent, so each shows who, what their role opens and which clients
* they see, whether the key works (it expires, and an agent who is
* disabled, archived or has no role turns every call away) and when it was
* last used. The work is in functions/api_keys.php; the handlers are
* admin/post/api_keys.php.
*/
require_once "includes/inc_all_admin.php";
$sql = mysqli_query(
$mysqli,
"SELECT SQL_CALC_FOUND_ROWS api_key_created_at, api_key_expire, api_key_id, api_key_name, api_key_secret, user_name FROM api_keys
LEFT JOIN users on api_key_user_id = user_id
WHERE (api_key_name LIKE '%$q%')
ORDER BY $sort $order LIMIT $record_from, $record_to"
);
$num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
$search = trim((string) ($_GET['q'] ?? ''));
$keys = apiKeyRows($search);
$roles = roleRows();
$access = userClientAccess(array_filter(array_map('intval', array_column($keys, 'api_key_user_id'))));
$csrf = $_SESSION['csrf_token'];
?>
@@ -26,149 +27,120 @@ $num_rows = mysqli_fetch_row(mysqli_query($mysqli, "SELECT FOUND_ROWS()"));
</div>
</div>
<div class="card-header py-3">
<form autocomplete="off">
<div class="row g-2 align-items-center">
<div class="col-md-4">
<div class="input-group">
<input type="search" class="form-control" name="q" value="<?php if (isset($q)) { echo stripslashes(escapeHtml($q)); } ?>" placeholder="Search keys">
<button class="btn btn-primary" title="Search"><i class="fa fa-search"></i></button>
</div>
</div>
<div class="col-md-8">
<div class="btn-group float-end">
<div class="dropdown ms-2" id="bulkActionButton" hidden>
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-layer-group me-2"></i>Bulk Action (<span id="selectedCount">0</span>)
</button>
<div class="dropdown-menu">
<button class="dropdown-item text-danger text-bold"
type="submit" form="bulkActions" name="bulk_delete_api_keys">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</button>
</div>
</div>
</div>
</div>
<div class="p-2 border-bottom">
<p class="small text-secondary mb-2">A key lets another system use ITFlow's API as one of your agents - it can do what their <a href="roles.php">role</a> allows, for the clients they see, until it expires.</p>
<form autocomplete="off" class="d-flex flex-wrap align-items-center gap-2">
<div class="input-group input-group-sm w-auto me-auto">
<input type="search" class="form-control" name="q" value="<?= escapeHtml($search) ?>" placeholder="Search keys and agents">
<button class="btn btn-primary" title="Search"><i class="fa fa-search"></i></button>
</div>
<div class="dropdown" id="bulkActionButton" hidden>
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown">
<i class="fas fa-fw fa-layer-group me-1"></i>Bulk Action (<span id="selectedCount">0</span>)
</button>
<div class="dropdown-menu dropdown-menu-end">
<button class="dropdown-item text-danger text-bold confirm-link" type="submit" form="bulkActions" name="bulk_delete_api_keys"
data-confirm-title="Delete these keys?" data-confirm-text="Anything using them stops working." data-confirm-button="Delete">
<i class="fas fa-fw fa-trash me-2"></i>Delete
</button>
</div>
</div>
</form>
</div>
<div class="table-responsive-sm">
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<table class="table table-striped table-borderless table-hover mb-0">
<thead class="text-dark <?php if ($num_rows[0] == 0) { echo "d-none"; } ?>">
<tr>
<td class="checkbox-column border-end">
<div class="form-check">
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" onclick="checkAll(this)">
</div>
</td>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_name&order=<?= $disp ?>">
Name <?php if ($sort == 'api_key_name') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_user_id&order=<?= $disp ?>">
User <?php if ($sort == 'api_key_user_id') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_secret&order=<?= $disp ?>">
Secret <?php if ($sort == 'api_key_secret') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_created_at&order=<?= $disp ?>">
Created <?php if ($sort == 'api_key_created_at') { echo $order_icon; } ?>
</a>
</th>
<th>
<a class="text-dark" href="?<?= $url_query_strings_sort ?>&sort=api_key_expire&order=<?= $disp ?>">
Expires <?php if ($sort == 'api_key_expire') { echo $order_icon; } ?>
</a>
</th>
<th class="text-center">Action</th>
</tr>
</thead>
<tbody>
<?php
while ($row = mysqli_fetch_assoc($sql)) {
$api_key_id = intval($row['api_key_id']);
$api_key_name = escapeHtml($row['api_key_name']);
$api_key_secret = escapeHtml("************" . substr($row['api_key_secret'], -4));
$api_key_created_at = escapeHtml($row['api_key_created_at']);
$api_key_expire = escapeHtml($row['api_key_expire']);
if ($api_key_expire < date("Y-m-d H:i:s")) {
$api_key_expire = $api_key_expire . " (Expired)";
}
$api_key_user = !empty($row['user_name']) ? escapeHtml($row['user_name']) : "<i>None</i>";
?>
<form id="bulkActions" action="post.php" method="post">
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
<div class="table-responsive-sm">
<table class="table table-borderless table-hover align-middle mb-0">
<?php if ($keys) { ?>
<thead class="text-dark">
<tr>
<td class="checkbox-column bg-light border-end">
<div class="form-check">
<input class="form-check-input bulk-select" type="checkbox" name="api_key_ids[]" value="<?= $api_key_id ?>">
<th class="ps-3" style="width: 1%">
<div class="form-check mb-0">
<input class="form-check-input" id="selectAllCheckbox" type="checkbox" title="Select all">
</div>
</th>
<th>Key</th>
<th class="d-none d-md-table-cell">Acts as</th>
<th class="d-none d-lg-table-cell">Can open</th>
<th class="d-none d-sm-table-cell">Last used</th>
<th class="text-center" style="width: 1%">Action</th>
</tr>
</thead>
<?php } ?>
<tbody>
<?php foreach ($keys as $api_key_id => $row) {
$name = escapeHtml($row['api_key_name']);
$role = $roles[intval($row['user_role_id'])] ?? null;
$expired = apiKeyExpired($row);
$agent_name = trim(str_replace(' (archived)', '', (string) $row['user_name']));
$last_used = $row['api_key_last_used_at'] ? '<span title="' . escapeHtml($row['api_key_last_used_at']) . '">' . escapeHtml(timeAgo($row['api_key_last_used_at'])) . '</span>' : '<span class="text-secondary">Never</span>';
ob_start(); ?>
<?php if ($row['user_name'] !== null && intval($row['user_type']) === 1) { ?>
<div class="d-flex align-items-center gap-2">
<?= userAvatar($row['api_key_user_id'], $agent_name, $row['user_avatar'], 'sm') ?>
<div class="min-w-0">
<div class="text-truncate"><?= escapeHtml($agent_name) ?></div>
<div class="small text-body-secondary text-truncate"><i class="fas fa-fw <?= $role && intval($role['role_is_admin']) ? 'fa-user-shield' : 'fa-user-tag' ?> me-1"></i><?= $role ? escapeHtml($role['role_name']) : 'No role' ?></div>
</div>
</div>
<?php } else { ?>
<span class="small text-danger-emphasis"><i class="fas fa-exclamation-triangle me-1"></i>Nobody</span>
<?php }
$agent_html = ob_get_clean();
$can_html = $role ? roleAccessHtml($role) . '<div class="small text-body-secondary mt-1"><i class="fas fa-fw fa-building me-1"></i>' . escapeHtml(userClientAccessText($access[intval($row['api_key_user_id'])] ?? ['allow' => [], 'deny' => []], intval($role['role_is_admin']))) . '</div>' : '<span class="small text-secondary">Nothing</span>';
?>
<tr data-api-key-id="<?= $api_key_id ?>" class="<?= $expired ? 'text-body-secondary' : '' ?>">
<td class="ps-3">
<div class="form-check mb-0">
<input class="form-check-input bulk-select" type="checkbox" name="api_key_ids[]" value="<?= $api_key_id ?>" aria-label="Select <?= $name ?>">
</div>
</td>
<td class="text-bold"><?= $api_key_name ?></td>
<td><?= $api_key_user ?></td>
<td><?= $api_key_secret ?></td>
<td><?= $api_key_created_at ?></td>
<td><?= $api_key_expire ?></td>
<td>
<a class="ajax-modal fw-semibold <?= $expired ? 'text-body-secondary' : 'text-body' ?>" href="#" data-modal-url="modals/api/api_key_edit.php?id=<?= $api_key_id ?>" title="Edit <?= $name ?>"><?= $name ?></a>
<div class="small text-body-secondary"><span class="font-monospace">••••<?= escapeHtml(substr((string) $row['api_key_secret'], -4)) ?></span> · made <?= date('M j, Y', strtotime($row['api_key_created_at'])) ?></div>
<div class="mt-1"><?= apiKeyStatePill($row, $role) ?></div>
<!-- On a phone, who it acts as and when it was used come along -->
<div class="d-md-none mt-2"><?= $agent_html ?></div>
<div class="d-lg-none mt-2"><?= $can_html ?></div>
<div class="d-sm-none small mt-1">Last used <?= $last_used ?></div>
</td>
<td class="d-none d-md-table-cell"><?= $agent_html ?></td>
<td class="d-none d-lg-table-cell"><?= $can_html ?></td>
<td class="d-none d-sm-table-cell small text-nowrap"><?= $last_used ?></td>
<td>
<div class="dropdown dropstart text-center">
<button class="btn btn-secondary btn-sm" title="More actions" type="button" data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-h"></i>
</button>
<button class="btn btn-secondary btn-sm" title="More actions" type="button" data-bs-toggle="dropdown"><i class="fas fa-ellipsis-h"></i></button>
<div class="dropdown-menu">
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/api/api_key_edit.php?id=<?= $api_key_id ?>">
<i class="fas fa-fw fa-edit me-2"></i>Edit
</a>
<?php if ($api_key_expire > date("Y-m-d H:i:s")) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?revoke_api_key=<?= $api_key_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-times me-2"></i>Revoke
</a>
<?php } ?>
<?php if ($api_key_expire < date("Y-m-d H:i:s")) { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_api_key=<?= $api_key_id ?>&csrf_token=<?= $_SESSION['csrf_token'] ?>">
<i class="fas fa-fw fa-times me-2"></i>Delete
</a>
<a class="dropdown-item ajax-modal" href="#" data-modal-url="modals/api/api_key_edit.php?id=<?= $api_key_id ?>"><i class="fas fa-fw fa-edit me-2"></i>Edit</a>
<div class="dropdown-divider"></div>
<?php if (!$expired) { // revoked first, then deleted ?>
<a class="dropdown-item text-danger confirm-link" href="post.php?revoke_api_key=<?= $api_key_id ?>&csrf_token=<?= $csrf ?>"
data-confirm-title="Revoke <?= $name ?>?" data-confirm-text="It stops working now. Anything using it is turned away." data-confirm-button="Revoke"><i class="fas fa-fw fa-ban me-2"></i>Revoke</a>
<?php } else { ?>
<a class="dropdown-item text-danger text-bold confirm-link" href="post.php?delete_api_key=<?= $api_key_id ?>&csrf_token=<?= $csrf ?>"
data-confirm-title="Delete <?= $name ?>?" data-confirm-text="It has expired, so nothing can be using it." data-confirm-button="Delete"><i class="fas fa-fw fa-trash me-2"></i>Delete</a>
<?php } ?>
</div>
</div>
</td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</form>
</form>
</div>
<?php
$empty_state = [
'thing' => 'API keys',
'icon' => 'fa-key',
'text' => 'A key lets another system read and write ITFlow through its API.',
'actions' => [['label' => 'New API Key', 'modal' => 'modals/api/api_key_add.php']],
];
?>
<?php require_once "../includes/filter_footer.php"; ?>
<?php if (!$keys) { ?>
<?= emptyStateHtml([
'thing' => 'API keys',
'icon' => 'fa-key',
'text' => 'A key lets another system read and write ITFlow through its API, as one of your agents.',
'filtered' => $search !== '',
'clear_url' => 'api_keys.php',
'actions' => [['label' => 'New API Key', 'modal' => 'modals/api/api_key_add.php']],
]) ?>
<?php } ?>
</div>
<script src="../js/bulk_actions.js"></script>

View File

@@ -0,0 +1,11 @@
<?php
/*
* ITFlow - Database update to version 3.1.7 (from 3.1.6)
* Included by admin/database_updates.php - do not access directly
*/
defined('FROM_DB_UPDATER') || die("Direct file access is not allowed");
// When each API key was last used - Admin > API Keys, to find the ones nothing uses
mysqli_query($mysqli, "ALTER TABLE `api_keys` ADD `api_key_last_used_at` DATETIME NULL DEFAULT NULL AFTER `api_key_user_id`");

View File

@@ -1,5 +1,11 @@
<?php
/*
* New API Key - its name, the day it stops working and the agent it acts as
* (with what they can open, shown as they are picked), then the key and the
* credential decryption password to copy - shown only here.
*/
require_once '../../includes/modal_header.php';
$key = randomString(32);
@@ -10,98 +16,93 @@ ob_start();
?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-key me-2"></i>New Key</h5>
<h5 class="modal-title"><i class="fas fa-fw fa-key me-2"></i>New API Key</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="key" value="<?= $key ?>">
<input type="hidden" name="password" value="<?= $decryptPW ?>">
<div class="modal-body">
<ul class="nav nav-pills nav-justified mb-3">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="pill" href="#pills-api-details">Details</a>
<a class="nav-link active" data-bs-toggle="pill" href="#pills-api-details"><i class="fas fa-fw fa-sliders-h me-1"></i>Details</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="pill" href="#pills-api-keys">Keys</a>
<a class="nav-link" data-bs-toggle="pill" href="#pills-api-keys"><i class="fas fa-fw fa-key me-1"></i>Key</a>
</li>
</ul>
<hr>
<div class="tab-content">
<div class="tab-pane fade show active" id="pills-api-details">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="key" value="<?= $key ?>">
<input type="hidden" name="password" value="<?= $decryptPW ?>">
<div class="mb-3">
<label>Name <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-sticky-note"></i></span>
<input type="text" class="form-control" name="name" placeholder="Key Name" maxlength="255" required autofocus>
<span class="input-group-text"><i class="fa fa-fw fa-sticky-note"></i></span>
<input type="text" class="form-control" name="name" placeholder="What uses it - the RMM, a PSA sync" maxlength="255" required autofocus>
</div>
</div>
<div class="mb-3">
<label>Expiration Date <strong class="text-danger">*</strong></label>
<label>Expires <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
<input type="date" class="form-control" name="expire" min="<?= date('Y-m-d') ?>" max="2999-12-31" required>
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
<input type="date" class="form-control" name="expire" min="<?= date('Y-m-d', strtotime('+1 day')) ?>" max="2999-12-31" value="<?= date('Y-m-d', strtotime('+1 year')) ?>" required>
</div>
<div class="form-text">It works until the start of that day.</div>
</div>
<div class="mb-3">
<label>Run as User <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-user-shield"></i></span>
<select class="form-select select2" name="run_as_user" required>
<option value="">- Select a user -</option>
<?php
$sql_run_users = mysqli_query($mysqli, "SELECT user_id, user_name, user_avatar FROM users WHERE user_type = 1 AND user_status = 1 AND user_archived_at IS NULL ORDER BY user_name ASC");
while ($run_user = mysqli_fetch_assoc($sql_run_users)) {
$run_user_id = intval($run_user['user_id']);
$run_user_name = escapeHtml($run_user['user_name']); ?>
<option value="<?= $run_user_id ?>"<?= selectOptionUser($run_user['user_id'], $run_user['user_name'], $run_user['user_avatar'] ?? '') ?>><?= $run_user_name ?></option>
<?php } ?>
</select>
</div>
<small class="form-text text-muted">The key inherits this user's module permissions and client access.</small>
</div>
<?= apiKeyAgentFieldHtml() ?>
</div>
<div class="tab-pane fade" id="pills-api-keys">
<div class="alert alert-warning small py-2"><i class="fas fa-fw fa-exclamation-triangle me-1"></i>Copy both now - they aren't shown again.</div>
<div class="mb-3">
<label>API Key <strong class="text-danger">*</strong></label>
<label>API key</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
<input type="text" class="form-control" value="<?= $key ?>" required disabled>
<button class="btn btn-outline-secondary clipboardjs" title="Copy API key" type="button" data-clipboard-text="<?= $key ?>"><i class="far fa-fw fa-copy"></i></button>
<span class="input-group-text"><i class="fa fa-fw fa-key"></i></span>
<input type="text" class="form-control font-monospace" value="<?= $key ?>" readonly>
<button class="btn btn-outline-secondary clipboardjs" title="Copy API key" type="button" data-clipboard-text="<?= $key ?>"><i class="far fa-fw fa-copy"></i></button>
</div>
</div>
<div class="mb-3">
<label>Login credential decryption password <strong class="text-danger">*</strong></label>
<label>Credential decryption password</label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-unlock-alt"></i></span>
<input type="text" class="form-control" value="<?= $decryptPW ?>" required disabled>
<button class="btn btn-outline-secondary clipboardjs" title="Copy decryption password" type="button" data-clipboard-text="<?= $decryptPW ?>"><i class="far fa-fw fa-copy"></i></button>
<span class="input-group-text"><i class="fa fa-fw fa-unlock-alt"></i></span>
<input type="text" class="form-control font-monospace" value="<?= $decryptPW ?>" readonly>
<button class="btn btn-outline-secondary clipboardjs" title="Copy decryption password" type="button" data-clipboard-text="<?= $decryptPW ?>"><i class="far fa-fw fa-copy"></i></button>
</div>
<div class="form-text">Sent with calls that read or write credentials.</div>
</div>
<br>
<div class="mb-3">
<label>I have made a copy of the key(s)<strong class="text-danger">*</strong></label>
<div class="input-group">
<input class="form-check-input" type="checkbox" name="ack" value="1" required>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="ack" value="1" id="apiKeyAck" required>
<label class="form-check-label" for="apiKeyAck">I have copied the key and the password <strong class="text-danger">*</strong></label>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" name="add_api_key" class="btn btn-primary text-bold"><i class="fa fa-check me-2"></i>Create</button>
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
</div>
<script>
// Create on the Details tab with the copied box not ticked: show the Key tab, so the browser can point at it
(function () {
const form = document.currentScript.closest('form');
form.addEventListener('invalid', function (event) {
const pane = event.target.closest('.tab-pane');
if (pane && !pane.classList.contains('active')) {
bootstrap.Tab.getOrCreateInstance(form.querySelector('[href="#' + pane.id + '"]')).show();
}
}, true);
})();
</script>
</form>
<?php

View File

@@ -1,71 +1,61 @@
<?php
/*
* Edit an API key - its name, the day it stops working and the agent it
* acts as. The key itself can't be shown or changed: make a new one.
*/
require_once '../../includes/modal_header.php';
$api_key_id = intval($_GET['id']);
$sql = mysqli_query($mysqli, "SELECT api_key_expire, api_key_name, api_key_user_id FROM api_keys WHERE api_key_id = $api_key_id LIMIT 1");
$row = mysqli_fetch_assoc($sql);
$api_key_name = escapeHtml($row['api_key_name']);
$api_key_expire = escapeHtml($row['api_key_expire']);
$api_key_user_id = intval($row['api_key_user_id']);
$row = apiKeyRow($_GET['id'] ?? 0);
ob_start();
?>
if (!$row) { ?>
<div class="modal-body text-center text-secondary py-5">That key is gone - it may have been deleted.</div>
<?php } else {
$api_key_id = intval($row['api_key_id']);
$role = roleRow($row['user_role_id'] ?? 0); ?>
<div class="modal-header bg-dark">
<h5 class="modal-title"><i class="fas fa-fw fa-key me-2"></i>Editing API Key:
<strong><?= $api_key_name ?></strong></h5>
<h5 class="modal-title"><i class="fas fa-fw fa-key me-2"></i>API Key: <strong><?= escapeHtml($row['api_key_name']) ?></strong></h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form action="post.php" method="post" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="api_key_id" value="<?= $api_key_id ?>">
<div class="modal-body">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="hidden" name="api_key_id" value="<?= $api_key_id ?>">
<div class="d-flex flex-wrap align-items-center gap-2 mb-3">
<?= apiKeyStatePill($row, $role) ?>
<span class="small text-body-secondary"><span class="font-monospace">••••<?= escapeHtml(substr((string) $row['api_key_secret'], -4)) ?></span> · last used <?= $row['api_key_last_used_at'] ? escapeHtml(timeAgo($row['api_key_last_used_at'])) : 'never' ?></span>
</div>
<div class="mb-3">
<label>Name <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-sticky-note"></i></span>
<input type="text" class="form-control" name="name" placeholder="Key Name" maxlength="255"
value="<?= $api_key_name ?>" required autofocus>
<span class="input-group-text"><i class="fa fa-fw fa-sticky-note"></i></span>
<input type="text" class="form-control" name="name" placeholder="What uses it" maxlength="255" value="<?= escapeHtml($row['api_key_name']) ?>" required autofocus>
</div>
</div>
<div class="mb-3">
<label>Expiration Date <strong class="text-danger">*</strong></label>
<label>Expires <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
<input type="date" class="form-control" name="expire" min="<?= date('Y-m-d') ?>" max="2999-12-31"
value="<?= $api_key_expire ?>" required>
<span class="input-group-text"><i class="fa fa-fw fa-calendar"></i></span>
<input type="date" class="form-control" name="expire" max="2999-12-31" value="<?= escapeHtml($row['api_key_expire']) ?>" required>
</div>
<div class="form-text">It works until the start of that day. A later day brings an expired key back.</div>
</div>
<div class="mb-3">
<label>Run as User <strong class="text-danger">*</strong></label>
<div class="input-group">
<span class="input-group-text"><i class="fa fa-fw fa-user-shield"></i></span>
<select class="form-select select2" name="run_as_user" required>
<option value="">- Select a user -</option>
<?php
$sql_run_users = mysqli_query($mysqli, "SELECT user_id, user_name, user_avatar FROM users WHERE user_type = 1 AND user_status = 1 AND user_archived_at IS NULL ORDER BY user_name ASC");
while ($run_user = mysqli_fetch_assoc($sql_run_users)) {
$run_user_id = intval($run_user['user_id']);
$run_user_name = escapeHtml($run_user['user_name']); ?>
<option value="<?= $run_user_id ?>" <?php if ($run_user_id == $api_key_user_id) { echo "selected"; } ?><?= selectOptionUser($run_user['user_id'], $run_user['user_name'], $run_user['user_avatar'] ?? '') ?>><?= $run_user_name ?></option>
<?php } ?>
</select>
</div>
<small class="form-text text-muted">The key inherits this user's module permissions and client access.</small>
</div>
<?= apiKeyAgentFieldHtml(isset(apiKeyAgents()[intval($row['api_key_user_id'])]) ? $row['api_key_user_id'] : 0) ?>
</div>
<div class="modal-footer">
<button type="submit" name="edit_api_key" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save</button>
<button type="submit" name="edit_api_key" class="btn btn-primary text-bold"><i class="fas fa-check me-2"></i>Save changes</button>
<button type="button" class="btn btn-light" data-bs-dismiss="modal"><i class="fas fa-times me-2"></i>Cancel</button>
</div>
</form>
<?php
require_once "../../../includes/modal_footer.php";
<?php }
require_once '../../../includes/modal_footer.php';

View File

@@ -1,7 +1,8 @@
<?php
/*
* ITFlow - GET/POST request handler for API settings
* API keys (Admin > API Keys) - making, changing, revoking and deleting
* them. The checks are in functions/api_keys.php.
*/
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
@@ -10,21 +11,29 @@ if (isset($_POST['add_api_key'])) {
validateCSRFToken();
$name = escapeSql($_POST['name']);
$expire = escapeSql($_POST['expire']);
$user_id = intval($_POST['run_as_user']);
$secret = escapeSql($_POST['key']); // API Key
[$fields, $error] = apiKeyFromInput($_POST);
$secret = (string) ($_POST['key'] ?? '');
$password = trim((string) ($_POST['password'] ?? ''));
if ($error === '' && (!preg_match('/^[A-Za-z0-9_-]{32}$/', $secret) || $password === '')) {
$error = 'The window was missing its key - open New API Key again.';
}
if ($error !== '') {
flashAlert(escapeHtml($error), 'error');
redirect();
}
// Credential decryption password
$apikey_specific_encryption_ciphertext = encryptUserSpecificKey(trim($_POST['password']));
$apikey_specific_encryption_ciphertext = encryptUserSpecificKey($password);
mysqli_query($mysqli,"INSERT INTO api_keys SET api_key_name = '$name', api_key_secret = '$secret', api_key_decrypt_hash = '$apikey_specific_encryption_ciphertext', api_key_expire = '$expire', api_key_user_id = $user_id");
mysqli_query($mysqli, "INSERT INTO api_keys SET api_key_name = '" . escapeSql($fields['name']) . "', api_key_secret = '" . escapeSql($secret) . "',
api_key_decrypt_hash = '$apikey_specific_encryption_ciphertext', api_key_expire = '" . escapeSql($fields['expire']) . "', api_key_user_id = " . intval($fields['user_id']));
$api_key_id = mysqli_insert_id($mysqli);
$agent = apiKeyAgents()[$fields['user_id']]['user_name'];
logAudit("API Key", "Create", "$session_name created API key $name set to expire on $expire", 0, $api_key_id);
logAudit("API Key", "Create", "$session_name created API key " . escapeSql($fields['name']) . " acting as " . escapeSql($agent) . ", expiring " . $fields['expire'], 0, $api_key_id);
flashAlert("API Key <strong>$name</strong> created");
flashAlert("API key <strong>" . escapeHtml($fields['name']) . "</strong> created");
redirect();
@@ -34,16 +43,38 @@ if (isset($_POST['edit_api_key'])) {
validateCSRFToken();
$api_key_id = intval($_POST['api_key_id']);
$name = escapeSql($_POST['name']);
$expire = escapeSql($_POST['expire']);
$user_id = intval($_POST['run_as_user']);
$row = apiKeyRow($_POST['api_key_id'] ?? 0);
if (!$row) {
flashAlert("That key is gone - it may have been deleted", 'error');
redirect();
}
$api_key_id = intval($row['api_key_id']);
mysqli_query($mysqli,"UPDATE api_keys SET api_key_name = '$name', api_key_expire = '$expire', api_key_user_id = $user_id WHERE api_key_id = $api_key_id");
[$fields, $error] = apiKeyFromInput($_POST, $row['api_key_expire']);
if ($error !== '') {
flashAlert(escapeHtml($error), 'error');
redirect();
}
logAudit("API Key", "Edit", "$session_name edited API key $name", 0, $api_key_id);
mysqli_query($mysqli, "UPDATE api_keys SET api_key_name = '" . escapeSql($fields['name']) . "', api_key_expire = '" . escapeSql($fields['expire']) . "',
api_key_user_id = " . intval($fields['user_id']) . " WHERE api_key_id = $api_key_id");
flashAlert("API Key <strong>$name</strong> updated");
// What changed, for the audit log
$changes = [];
if ($row['api_key_name'] !== $fields['name']) {
$changes[] = 'renamed from ' . $row['api_key_name'];
}
if ((string) $row['api_key_expire'] !== $fields['expire']) {
$changes[] = 'expiring ' . $fields['expire'] . ' (was ' . $row['api_key_expire'] . ')';
}
if (intval($row['api_key_user_id']) !== $fields['user_id']) {
$changes[] = 'acting as ' . apiKeyAgents()[$fields['user_id']]['user_name'] . ' (was ' . ($row['user_name'] ?? 'nobody') . ')';
}
if ($changes) {
logAudit("API Key", "Edit", "$session_name edited API key " . escapeSql($fields['name']) . " - " . escapeSql(implode(', ', $changes)), 0, $api_key_id);
}
flashAlert("API key <strong>" . escapeHtml($fields['name']) . "</strong> " . ($changes ? 'saved' : 'unchanged'));
redirect();
@@ -53,17 +84,18 @@ if (isset($_GET['revoke_api_key'])) {
validateCSRFToken();
$api_key_id = intval($_GET['revoke_api_key']);
$row = apiKeyRow($_GET['revoke_api_key']);
if (!$row) {
flashAlert("That key is gone - it may have been deleted", 'error');
redirect();
}
// Get API Key Name
$row = mysqli_fetch_assoc(mysqli_query($mysqli,"SELECT api_key_name FROM api_keys WHERE api_key_id = $api_key_id"));
$api_key_name = escapeSql($row['api_key_name']);
// Expires today - it works until the start of its expiry day
mysqli_query($mysqli, "UPDATE api_keys SET api_key_expire = CURDATE() WHERE api_key_id = " . intval($row['api_key_id']));
mysqli_query($mysqli,"UPDATE api_keys SET api_key_expire = NOW() WHERE api_key_id = $api_key_id");
logAudit("API Key", "Revoke", "$session_name revoked API key " . escapeSql($row['api_key_name']), 0, $row['api_key_id']);
logAudit("API Key", "Revoke", "$session_name revoked API key $name", 0);
flashAlert("API Key <strong>$name</strong> revoked", 'error');
flashAlert("API key <strong>" . escapeHtml($row['api_key_name']) . "</strong> revoked", 'error');
redirect();
@@ -73,17 +105,17 @@ if (isset($_GET['delete_api_key'])) {
validateCSRFToken();
$api_key_id = intval($_GET['delete_api_key']);
$row = apiKeyRow($_GET['delete_api_key']);
if (!$row) {
flashAlert("That key is gone - it may have been deleted", 'error');
redirect();
}
// Get API Key Name
$row = mysqli_fetch_assoc(mysqli_query($mysqli,"SELECT api_key_name FROM api_keys WHERE api_key_id = $api_key_id"));
$api_key_name = escapeSql($row['api_key_name']);
mysqli_query($mysqli, "DELETE FROM api_keys WHERE api_key_id = " . intval($row['api_key_id']));
mysqli_query($mysqli,"DELETE FROM api_keys WHERE api_key_id = $api_key_id");
logAudit("API Key", "Delete", "$session_name deleted API key " . escapeSql($row['api_key_name']), 0, $row['api_key_id']);
logAudit("API Key", "Delete", "$session_name deleted API key $name", 0);
flashAlert("API Key <strong>$name</strong> deleted", 'error');
flashAlert("API key <strong>" . escapeHtml($row['api_key_name']) . "</strong> deleted", 'error');
redirect();
@@ -93,31 +125,19 @@ if (isset($_POST['bulk_delete_api_keys'])) {
validateCSRFToken();
if (isset($_POST['api_key_ids'])) {
$count = count($_POST['api_key_ids']);
// Cycle through array and delete each record
foreach ($_POST['api_key_ids'] as $api_key_id) {
$api_key_id = intval($api_key_id);
// Get API Key Name
$row = mysqli_fetch_assoc(mysqli_query($mysqli,"SELECT api_key_name FROM api_keys WHERE api_key_id = $api_key_id"));
$api_key_name = escapeSql($row['api_key_name']);
mysqli_query($mysqli, "DELETE FROM api_keys WHERE api_key_id = $api_key_id");
logAudit("API Key", "Delete", "$session_name deleted API key $name", 0);
$count = 0;
foreach (array_unique(array_map('intval', (array) ($_POST['api_key_ids'] ?? []))) as $api_key_id) {
$row = apiKeyRow($api_key_id);
if (!$row) {
continue;
}
logAudit("API Key", "Bulk Delete", "$session_name deleted $count API key(s)");
flashAlert("Deleted <strong>$count</strong> API keys(s)", 'error');
mysqli_query($mysqli, "DELETE FROM api_keys WHERE api_key_id = $api_key_id");
logAudit("API Key", "Delete", "$session_name deleted API key " . escapeSql($row['api_key_name']), 0, $api_key_id);
$count++;
}
flashAlert("Deleted <strong>$count</strong> API key" . ($count === 1 ? '' : 's'), 'error');
redirect();
}