toolbox to show privileges added and removed between successive userclasses

This commit is contained in:
Spine
2025-06-28 00:17:37 +00:00
parent 5cb8f7afbd
commit 3bca2779ec
11 changed files with 182 additions and 5 deletions

View File

@@ -118,6 +118,40 @@ class Privilege extends \Gazelle\BaseManager {
return self::$db->to_array(false, MYSQLI_ASSOC);
}
/**
* Given a list of userclasses [A, B, C, D], return a list of pairs
* [ ['lower' => 0, 'higher' => A], ['lower' => A, 'higher' => B], ..., ['lower' => C, 'higher' => D] ]
* These will be used by compareUserclass()
*/
public function pairUserclass(array $userclassList): array {
$result = [];
array_unshift($userclassList, 0);
for ($n = 0, $end = count($userclassList) - 1; $n < $end; ++$n) {
$result[] = [
'lower' => $userclassList[$n],
'higher' => $userclassList[$n + 1],
];
}
return $result;
}
/**
* Given two userclasses (permissions.ID) return an array of
* all privileges that have been added to the second class over the first,
* and the privileges which are in the first class but not in the second.
*/
public function compareUserclass(int $lowerPermissionID, int $higherPermissionID): array {
$list = $this->privilege();
$lower = array_keys(array_filter($list, fn ($i) => in_array($lowerPermissionID, $i['can'])));
$higher = array_keys(array_filter($list, fn ($i) => in_array($higherPermissionID, $i['can'])));
sort($lower);
sort($higher);
return [
'add' => array_values(array_diff($higher, $lower)),
'remove' => array_values(array_diff($lower, $higher)),
];
}
/**
* The list of defined privileges. The `can` field
* in the returned array acts as a sparse matrix.

View File

@@ -829,6 +829,22 @@ defined('FORUM_MOD') or define('FORUM_MOD', 21);
defined('MOD') or define('MOD', 11);
defined('SYSOP') or define('SYSOP', 15);
// When adding a new privilege userclasses, care must be taken to
// ensure all classes beyond the initial class receive it. The
// Privileges Audit page is configured entirely by this definition.
defined('USERCLASS_AUDIT') or define('USERCLASS_AUDIT',
[
[
'heading' => 'User classes',
'compare' => [USER, MEMBER, POWER, ELITE, TORRENT_MASTER, POWER_TM, ELITE_TM],
],
[
'heading' => 'Staff classes',
'compare' => [FORUM_MOD, MOD, SYSOP],
],
]
);
// Permission ID of secondary class.
defined('DONOR') or define('DONOR', 42);
defined('FLS_TEAM') or define('FLS_TEAM', 23);

View File

@@ -1132,6 +1132,29 @@ form.analysis-case {
justify-self: end;
}
.compare-grid {
column-gap: 10px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.pair-grid {
border: 1px solid #808080;
column-gap: 5px;
display: grid;
grid-template-columns: 50% 50%;
margin-bottom: 12px;
}
.pair-grid div.added ul {
list-style-type: '+ ';
}
.pair-grid div.removed ul {
list-style-type: ' ';
}
.pair-grid .no-change {
padding: 5px;
text-align: left;
}
#more-news-loader {
cursor: pointer;
}

View File

@@ -152,6 +152,9 @@ switch ($_REQUEST['action'] ?? '') {
case 'privilege-alter':
include_once 'managers/userclass_alter.php';
break;
case 'privilege-audit':
include_once 'managers/privilege_audit.php';
break;
case 'privilege_matrix':
include_once 'managers/privilege_matrix.php';
break;

View File

@@ -0,0 +1,17 @@
<?php
/** @phpstan-var \Gazelle\User $Viewer */
/** @phpstan-var \Twig\Environment $Twig */
declare(strict_types=1);
namespace Gazelle;
if (!$Viewer->permitted('admin_manage_permissions')) {
Error403::error();
}
echo $Twig->render('admin/privilege-audit.twig', [
'privilege_manager' => new Manager\Privilege(),
'user_manager' => new Manager\User(),
'config' => USERCLASS_AUDIT,
]);

View File

@@ -0,0 +1,60 @@
{{ header('Audit Userclass Privileges') }}
<div class="thin">
<div class="header">
<div class="linkbox">
<a href="tools.php?action=privilege_matrix" class="brackets">Privileges Matrix</a>
<a href="tools.php?action=userclass" class="brackets">Userclass list</a>
<a href="tools.php?action=staff_groups" class="brackets">Staff Groups</a>
</div>
</div>
</div>
<div class="box pad">
{% for section in config %}
<h2>{{ section.heading }}</h2>
{% for comparison in privilege_manager.pairUserclass(section.compare) %}
{% if loop.first %}
<div class="compare-grid">
{% endif %}
<div class="compare">
<h3>
{%- if comparison.lower -%}
{{ user_manager.userclassName(comparison.lower) }} 🠞 {{ user_manager.userclassName(comparison.higher) -}}
{% else -%}
Initial 🠞 {{ user_manager.userclassName(comparison.higher) -}}
{% endif -%}
</h3>
{% set result = privilege_manager.compareUserclass(comparison.lower, comparison.higher) %}
<div class="pair-grid">
<div class="added">
<ul>
{% for r in result.add %}
<li>{{ r }}</li>
{% else %}
{% if result.remove %}
<li><i>None</i></li>
{% endif %}
{% endfor %}
</ul>
</div>
<div class="removed">
<ul>
{% for r in result.remove %}
<li>{{ r }}</li>
{% else %}
{% if result.add %}
<li><i>None</i></li>
{% endif %}
{% endfor %}
</ul>
</div>
{% if not (result.add or result.remove) %}
<span class="no-change">No change</span>
{% endif %}
</div>
</div>
{% if loop.last %}
</div>
{% endif %}
{% endfor %}
{% endfor %}
{{ footer() }}

View File

@@ -2,8 +2,9 @@
{{ header('Manage Permissions') }}
<div class="header">
<div class="linkbox">
<a href="tools.php?action=userclass" class="brackets">Userclass List</a>
<a href="tools.php?action=privilege_matrix" class="brackets">Privilege Matrix</a>
<a href="tools.php?action=privilege-audit" class="brackets">Privileges Audit</a>
<a href="tools.php?action=userclass" class="brackets">Userclass List</a>
<a href="tools.php?action=staff_groups" class="brackets">Staff Groups</a>
</div>
</div>

View File

@@ -1,14 +1,15 @@
{{ header('Privilege Matrix') }}
{{ header('Privileges Matrix') }}
<div class="thin">
<div class="header">
<div class="linkbox">
<a href="tools.php?action=privilege-audit" class="brackets">Privileges audit</a>
<a href="tools.php?action=userclass" class="brackets">Userclass list</a>
<a href="tools.php?action=staff_groups" class="brackets">Staff Groups</a>
<br />
<a href="tools.php?action=permissions&amp;id=new" class="brackets">Create a new userclass</a>
</div>
</div>
<h2>Privilege Matrix</h2>
<h2>Privileges Matrix</h2>
<table width="100%">
<tr class="colhead">
<td rowspan="2" style="vertical-align: bottom">Primary Class ★<br /><br />Privilege</td>

View File

@@ -2,7 +2,8 @@
<div class="thin">
<div class="header">
<div class="linkbox">
<a href="tools.php?action=privilege_matrix" class="brackets">Privilege Matrix</a>
<a href="tools.php?action=privilege_matrix" class="brackets">Privileges Matrix</a>
<a href="tools.php?action=privilege-audit" class="brackets">Privileges Audit</a>
<a href="tools.php?action=staff_groups" class="brackets">Staff Groups</a>
<br />
<a href="tools.php?action=userclass&amp;id=new" class="brackets">Create a new userclass</a>

View File

@@ -1,8 +1,9 @@
{{ header('Staff Group Management') }}
<div class="header">
<div class="linkbox">
<a href="tools.php?action=userclass" class="brackets">Userclass List</a>
<a href="tools.php?action=privilege_matrix" class="brackets">Privilege Matrix</a>
<a href="tools.php?action=privilege-audit" class="brackets">Privileges Audit</a>
<a href="tools.php?action=userclass" class="brackets">Userclass List</a>
</div>
</div>
<h2>Staff Group Management</h2>

View File

@@ -152,6 +152,26 @@ class PrivilegeTest extends TestCase {
$this->assertEquals($total + 1, $flsList[0]['total'], 'privilege-one-new-fls');
}
public function testPrivilegeCompare(): void {
$manager = new Manager\Privilege();
$compare = $manager->compareUserclass(USER, MEMBER);
$this->assertCount(2, $compare, 'privilege-compare-count');
$this->assertEquals([], $compare['remove'], 'privilege-compare-remove');
$this->assertEquals(
[
'edit_unknowns',
'site_advanced_top10',
'site_collages_manage',
'site_collages_subscribe',
'site_make_bookmarks',
'site_submit_requests',
'zip_downloader',
],
$compare['add'],
'privilege-compare-add',
);
}
public function testCustomPrivilege(): void {
$admin = $this->userList['admin'];
$user = $this->userList['user'];