Skip to content

Commit 3879084

Browse files
authored
Render permissions changes in KB history timeline (#23205)
1 parent 302432d commit 3879084

4 files changed

Lines changed: 239 additions & 2 deletions

File tree

css/includes/components/_kb.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@
9292

9393
[data-glpi-knowbase-side-panel-offcanvas] {
9494
width: min(400px, 85vw);
95+
96+
.steps-vertical {
97+
--tblr-steps-padding: 1.5rem;
98+
99+
margin-left: 0;
100+
}
95101
}
96102

97103
// KB Documents footer section

src/Glpi/Knowbase/History/HistoryBuilder.php

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,13 @@
3535
namespace Glpi\Knowbase\History;
3636

3737
use Document;
38+
use Entity;
39+
use Group;
3840
use KnowbaseItem;
3941
use KnowbaseItem_Revision;
4042
use Log;
43+
use Profile;
44+
use User;
4145

4246
final class HistoryBuilder
4347
{
@@ -56,6 +60,7 @@ public function buildHistory(): HistoryEventList
5660
$this->addFaqStatusChangesToHistory();
5761
$this->addAssociatedItemChangesToHistory();
5862
$this->addDocumentChangesToHistory();
63+
$this->addPermissionChangesToHistory();
5964
$this->history->sort();
6065
return $this->history;
6166
}
@@ -136,6 +141,45 @@ private function addCurrentVersionToHistory(): void
136141
));
137142
}
138143

144+
private function addPermissionChangesToHistory(): void
145+
{
146+
global $DB;
147+
148+
$target_types = [
149+
Entity::class,
150+
Group::class,
151+
Profile::class,
152+
User::class,
153+
];
154+
155+
$logs = $DB->request([
156+
'SELECT' => ['date_mod', 'user_name', 'linked_action', 'old_value', 'new_value'],
157+
'FROM' => Log::getTable(),
158+
'WHERE' => [
159+
'itemtype' => KnowbaseItem::class,
160+
'items_id' => $this->kb->getID(),
161+
'linked_action' => [Log::HISTORY_ADD_RELATION, Log::HISTORY_DEL_RELATION],
162+
'itemtype_link' => $target_types,
163+
],
164+
'ORDER' => 'id DESC',
165+
]);
166+
167+
foreach ($logs as $row) {
168+
$is_add = (int) $row['linked_action'] === Log::HISTORY_ADD_RELATION;
169+
$target_name = $is_add ? $row['new_value'] : $row['old_value'];
170+
$description = $is_add
171+
? sprintf(__("Access granted to %s by"), $target_name)
172+
: sprintf(__("Access revoked from %s by"), $target_name);
173+
174+
$this->history->addEvent(new LogEvent(
175+
label: __("Permissions updated"),
176+
description: $description,
177+
date: $row['date_mod'],
178+
author: $row['user_name'],
179+
));
180+
}
181+
}
182+
139183
private function addFaqStatusChangesToHistory(): void
140184
{
141185
global $DB;
@@ -156,7 +200,6 @@ private function addFaqStatusChangesToHistory(): void
156200
'ORDER' => 'id DESC',
157201
]);
158202

159-
160203
foreach ($logs as $row) {
161204
$label = $row['new_value']
162205
? __("Added to the FAQ")
@@ -176,6 +219,16 @@ private function addAssociatedItemChangesToHistory(): void
176219
{
177220
global $DB, $CFG_GLPI;
178221

222+
// Exclude permission types (Entity, Group, Profile, User) as they
223+
// are handled separately by addPermissionChangesToHistory().
224+
$permission_types = [
225+
Entity::class,
226+
Group::class,
227+
Profile::class,
228+
User::class,
229+
];
230+
$item_types = array_values(array_diff($CFG_GLPI['kb_types'], $permission_types));
231+
179232
$logs = $DB->request([
180233
'SELECT' => [
181234
'date_mod',
@@ -189,7 +242,7 @@ private function addAssociatedItemChangesToHistory(): void
189242
'WHERE' => [
190243
'itemtype' => KnowbaseItem::class,
191244
'items_id' => $this->kb->getID(),
192-
'itemtype_link' => $CFG_GLPI['kb_types'],
245+
'itemtype_link' => $item_types,
193246
'linked_action' => [
194247
Log::HISTORY_ADD_RELATION,
195248
Log::HISTORY_DEL_RELATION,

tests/e2e/specs/Knowbase/revisions.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,32 @@ test('Associated item changes appear in history', async ({ page, profile, api })
122122
await expect(events.filter({ hasText: 'Computer' })).toBeVisible();
123123
});
124124

125+
test('Permission changes appear in history', async ({ page, profile, api }) => {
126+
await profile.set(Profiles.SuperAdmin);
127+
const kb = new KnowbaseItemPage(page);
128+
129+
const entity_id = getWorkerEntityId();
130+
const id = await api.createItem('KnowbaseItem', {
131+
name: 'KB entry for permission history test',
132+
entities_id: entity_id,
133+
answer: 'Test content',
134+
});
135+
await api.createItem('Entity_KnowbaseItem', {
136+
knowbaseitems_id: id,
137+
entities_id: entity_id,
138+
is_recursive: 0,
139+
});
140+
141+
await kb.goto(id);
142+
await page.getByTitle('More actions').click();
143+
await kb.getButton('History').click();
144+
await expect(kb.getHeading('History')).toBeVisible();
145+
146+
const events = page.getByTestId('history-event');
147+
await expect(events.filter({ hasText: /Permissions updated/ })).toBeVisible();
148+
await expect(events.filter({ hasText: /Access granted to/ })).toBeVisible();
149+
});
150+
125151
test('Document attachment changes appear in history', async ({ page, profile, api }) => {
126152
await profile.set(Profiles.SuperAdmin);
127153
const kb = new KnowbaseItemPage(page);

tests/functional/Glpi/Knowbase/History/HistoryBuilderTest.php

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@
3737
use Computer;
3838
use Document;
3939
use Document_Item;
40+
use Entity;
41+
use Entity_KnowbaseItem;
4042
use Glpi\Tests\DbTestCase;
4143
use KnowbaseItem;
4244
use KnowbaseItem_Item;
4345
use KnowbaseItem_Revision;
46+
use KnowbaseItem_User;
4447
use Ticket;
48+
use User;
4549

4650
final class HistoryBuilderTest extends DbTestCase
4751
{
@@ -229,6 +233,154 @@ public function testRevisionEventDescriptionDistinguishesCreationFromUpdate(): v
229233
$this->assertEquals("Created by", $version_1->getDescription());
230234
}
231235

236+
public function testPermissionAdded(): void
237+
{
238+
$this->login();
239+
$this->setCurrentTime("2026-01-15 10:00:00");
240+
241+
$kb = $this->createItem(KnowbaseItem::class, [
242+
'users_id' => 2,
243+
'entities_id' => $this->getTestRootEntity(only_id: true),
244+
'name' => 'Test article',
245+
'answer' => 'Test content',
246+
]);
247+
248+
$this->setCurrentTime("2026-01-15 11:00:00");
249+
$this->createItem(Entity_KnowbaseItem::class, [
250+
'knowbaseitems_id' => $kb->getID(),
251+
'entities_id' => $this->getTestRootEntity(only_id: true),
252+
'is_recursive' => 1,
253+
]);
254+
255+
$root_entity = new Entity();
256+
$root_entity->getFromDB($this->getTestRootEntity(only_id: true));
257+
$entity_name = $root_entity->getNameID(['forceid' => true]);
258+
259+
$kb->getFromDB($kb->getID());
260+
$history = (new HistoryBuilder($kb))->buildHistory();
261+
$events = $history->getEvents();
262+
263+
$this->assertEquals([
264+
new LogEvent(
265+
label: "Permissions updated",
266+
description: sprintf("Access granted to %s by", $entity_name),
267+
date: "2026-01-15 11:00:00",
268+
author: "_test_user (8)",
269+
),
270+
new CreationEvent(
271+
date: "2026-01-15 10:00:00",
272+
author: 2,
273+
),
274+
], $events);
275+
}
276+
277+
public function testPermissionRemoved(): void
278+
{
279+
$this->login();
280+
$this->setCurrentTime("2026-01-15 10:00:00");
281+
282+
$kb = $this->createItem(KnowbaseItem::class, [
283+
'users_id' => 2,
284+
'entities_id' => $this->getTestRootEntity(only_id: true),
285+
'name' => 'Test article',
286+
'answer' => 'Test content',
287+
]);
288+
289+
$this->setCurrentTime("2026-01-15 11:00:00");
290+
$relation = $this->createItem(Entity_KnowbaseItem::class, [
291+
'knowbaseitems_id' => $kb->getID(),
292+
'entities_id' => $this->getTestRootEntity(only_id: true),
293+
'is_recursive' => 1,
294+
]);
295+
296+
$this->setCurrentTime("2026-01-15 12:00:00");
297+
$this->deleteItem(Entity_KnowbaseItem::class, $relation->getID());
298+
299+
$root_entity = new Entity();
300+
$root_entity->getFromDB($this->getTestRootEntity(only_id: true));
301+
$entity_name = $root_entity->getNameID(['forceid' => true]);
302+
303+
$kb->getFromDB($kb->getID());
304+
$history = (new HistoryBuilder($kb))->buildHistory();
305+
$events = $history->getEvents();
306+
307+
$this->assertEquals([
308+
new LogEvent(
309+
label: "Permissions updated",
310+
description: sprintf("Access revoked from %s by", $entity_name),
311+
date: "2026-01-15 12:00:00",
312+
author: "_test_user (8)",
313+
),
314+
new LogEvent(
315+
label: "Permissions updated",
316+
description: sprintf("Access granted to %s by", $entity_name),
317+
date: "2026-01-15 11:00:00",
318+
author: "_test_user (8)",
319+
),
320+
new CreationEvent(
321+
date: "2026-01-15 10:00:00",
322+
author: 2,
323+
),
324+
], $events);
325+
}
326+
327+
public function testMultiplePermissionTypes(): void
328+
{
329+
$this->login();
330+
$this->setCurrentTime("2026-01-15 10:00:00");
331+
332+
$kb = $this->createItem(KnowbaseItem::class, [
333+
'users_id' => 2,
334+
'entities_id' => $this->getTestRootEntity(only_id: true),
335+
'name' => 'Test article',
336+
'answer' => 'Test content',
337+
]);
338+
339+
$this->setCurrentTime("2026-01-15 11:00:00");
340+
$this->createItem(Entity_KnowbaseItem::class, [
341+
'knowbaseitems_id' => $kb->getID(),
342+
'entities_id' => $this->getTestRootEntity(only_id: true),
343+
'is_recursive' => 1,
344+
]);
345+
346+
$this->setCurrentTime("2026-01-15 12:00:00");
347+
$this->createItem(KnowbaseItem_User::class, [
348+
'knowbaseitems_id' => $kb->getID(),
349+
'users_id' => 2,
350+
]);
351+
352+
$root_entity = new Entity();
353+
$root_entity->getFromDB($this->getTestRootEntity(only_id: true));
354+
$entity_name = $root_entity->getNameID(['forceid' => true]);
355+
356+
$user = new User();
357+
$user->getFromDB(2);
358+
$user_name = $user->getNameID(['forceid' => true]);
359+
360+
$kb->getFromDB($kb->getID());
361+
$history = (new HistoryBuilder($kb))->buildHistory();
362+
$events = $history->getEvents();
363+
364+
$this->assertEquals([
365+
new LogEvent(
366+
label: "Permissions updated",
367+
description: sprintf("Access granted to %s by", $user_name),
368+
date: "2026-01-15 12:00:00",
369+
author: "_test_user (8)",
370+
),
371+
new LogEvent(
372+
label: "Permissions updated",
373+
description: sprintf("Access granted to %s by", $entity_name),
374+
date: "2026-01-15 11:00:00",
375+
author: "_test_user (8)",
376+
),
377+
new CreationEvent(
378+
date: "2026-01-15 10:00:00",
379+
author: 2,
380+
),
381+
], $events);
382+
}
383+
232384
public function testFaqChanges(): void
233385
{
234386
// Act: create KB item with 2 FAQ status changes

0 commit comments

Comments
 (0)