Skip to content

Commit 21c8b46

Browse files
Transparency log: track when version is created (#1627)
* Track VersionCreated event * Add the display classes for the `VersionCreated` record * Store dist/source reference of the created version * Store new version's metadata * Use auditLog.packageLink macro
1 parent 3a7071a commit 21c8b46

File tree

8 files changed

+149
-22
lines changed

8 files changed

+149
-22
lines changed

src/Audit/AuditRecordType.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ enum AuditRecordType: string
2323
case PackageCreated = 'package_created';
2424
case PackageDeleted = 'package_deleted';
2525
case CanonicalUrlChanged = 'canonical_url_changed';
26+
case VersionCreated = 'version_created';
2627
case VersionDeleted = 'version_deleted';
2728

2829
case VersionReferenceChanged = 'version_reference_changed';

src/Audit/Display/AuditLogDisplayFactory.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ public function buildSingle(AuditRecord $record): AuditLogDisplayInterface
7272
$record->attributes['repository_to'],
7373
$this->buildActor($record->attributes['actor']),
7474
),
75+
AuditRecordType::VersionCreated => new VersionCreatedDisplay(
76+
$record->datetime,
77+
$record->attributes['name'],
78+
$record->attributes['version'],
79+
$record->attributes['metadata']['source']['reference'] ?? null,
80+
$record->attributes['metadata']['dist']['reference'] ?? null,
81+
$this->buildActor($record->attributes['actor']),
82+
),
7583
AuditRecordType::VersionDeleted => new VersionDeletedDisplay(
7684
$record->datetime,
7785
$record->attributes['name'],
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is part of Packagist.
5+
*
6+
* (c) Jordi Boggiano <j.boggiano@seld.be>
7+
* Nils Adermann <naderman@naderman.de>
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
namespace App\Audit\Display;
14+
15+
use App\Audit\AuditRecordType;
16+
17+
readonly class VersionCreatedDisplay extends AbstractAuditLogDisplay
18+
{
19+
public function __construct(
20+
\DateTimeImmutable $datetime,
21+
public string $packageName,
22+
public string $version,
23+
public ?string $sourceReference,
24+
public ?string $distReference,
25+
ActorDisplay $actor,
26+
) {
27+
parent::__construct($datetime, $actor);
28+
}
29+
30+
public function getType(): AuditRecordType
31+
{
32+
return AuditRecordType::VersionCreated;
33+
}
34+
35+
public function getTemplateName(): string
36+
{
37+
return 'audit_log/display/version_created.html.twig';
38+
}
39+
}

src/Entity/AuditRecord.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
use Doctrine\ORM\Mapping as ORM;
1818
use Symfony\Component\Uid\Ulid;
1919

20+
/**
21+
* @phpstan-import-type VersionArray from Version
22+
*/
2023
#[ORM\Entity(repositoryClass: AuditRecordRepository::class)]
2124
#[ORM\Table(name: 'audit_log')]
2225
#[ORM\Index(name: 'type_idx', columns: ['type'])]
@@ -80,6 +83,16 @@ public static function packageTransferred(Package $package, ?User $actor, array
8083
return new self(AuditRecordType::PackageTransferred, ['name' => $package->getName(), 'actor' => self::getUserData($actor, 'admin'), 'previous_maintainers' => $previous, 'current_maintainers' => $current], $actor?->getId(), $package->getVendor(), $package->getId());
8184
}
8285

86+
/**
87+
* @param VersionArray $metadata
88+
*/
89+
public static function versionCreated(Version $version, array $metadata, ?User $actor): self
90+
{
91+
$package = $version->getPackage();
92+
93+
return new self(AuditRecordType::VersionCreated, ['name' => $package->getName(), 'version' => $version->getVersion(), 'actor' => self::getUserData($actor, 'automation'), 'metadata' => $metadata], $actor?->getId(), $package->getVendor(), $package->getId());
94+
}
95+
8396
public static function versionDeleted(Version $version, ?User $actor): self
8497
{
8598
$package = $version->getPackage();

src/EventListener/VersionListener.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
#[AsEntityListener(event: 'preRemove', entity: Version::class)]
2727
#[AsEntityListener(event: 'preUpdate', entity: Version::class)]
28+
#[AsEntityListener(event: 'postPersist', entity: Version::class)]
2829
#[AsEntityListener(event: 'postUpdate', entity: Version::class)]
2930
class VersionListener
3031
{
@@ -39,6 +40,16 @@ public function __construct(
3940
) {
4041
}
4142

43+
/**
44+
* @param LifecycleEventArgs<EntityManager> $event
45+
*/
46+
public function postPersist(Version $version, LifecycleEventArgs $event): void
47+
{
48+
$data = $version->toV2Array([]);
49+
$record = AuditRecord::versionCreated($version, $data, $this->getUser());
50+
$this->getEM()->getRepository(AuditRecord::class)->insert($record);
51+
}
52+
4253
/**
4354
* @param LifecycleEventArgs<EntityManager> $event
4455
*/
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{% import 'audit_log/macros.html.twig' as auditLog %}
2+
3+
<strong>{{ auditLog.packageLink(display.packageName) }}</strong> {{ display.version }}<br>
4+
{% if display.sourceReference %}
5+
Source: {{ display.sourceReference }}<br>
6+
{% endif %}
7+
{% if display.distReference %}
8+
Dist: {{ display.distReference }}<br>
9+
{% endif %}
10+
Created by: {{ display.actor.username }}

tests/Audit/VersionAuditRecordTest.php

Lines changed: 66 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
namespace App\Tests\Controller;
1414

1515
use App\Audit\AuditRecordType;
16+
use App\Entity\AuditRecord;
1617
use App\Entity\Package;
18+
use App\Entity\RequireLink;
1719
use App\Entity\Version;
1820
use Doctrine\DBAL\Connection;
1921
use Doctrine\Persistence\ManagerRegistry;
@@ -36,53 +38,61 @@ protected function tearDown(): void
3638
parent::tearDown();
3739
}
3840

39-
public function testVersionChangesGetRecorded(): void
41+
public function testVersionCreationGetsRecorded(): void
4042
{
4143
$container = static::getContainer();
4244
$em = $container->get(ManagerRegistry::class)->getManager();
4345

44-
$package = new Package();
45-
$package->setRepository('https://github.com/composer/composer');
46+
$version = $this->createPackageAndVersion();
47+
48+
$log = $em->getRepository(AuditRecord::class)->findOneBy([
49+
'type' => AuditRecordType::VersionCreated,
50+
'packageId' => $version->getPackage()->getId(),
51+
]);
52+
53+
self::assertNotNull($log, 'No audit record created for new version');
54+
self::assertSame('composer', $log->vendor);
55+
$attributes = $log->attributes;
56+
self::assertSame('composer/composer', $attributes['name']);
57+
self::assertSame('automation', $attributes['actor']);
58+
self::assertSame('1.0.0', $attributes['version']);
59+
self::assertSame('dist-ref', $attributes['metadata']['dist']['reference']);
60+
self::assertSame('source-ref', $attributes['metadata']['source']['reference']);
61+
self::assertSame('^1.5.0', $attributes['metadata']['require']['composer/ca-bundle']);
62+
}
4663

47-
$version = new Version();
48-
$version->setPackage($package);
49-
$version->setName($package->getName());
50-
$version->setVersion('1.0.0');
51-
$version->setNormalizedVersion('1.0.0.0');
52-
$version->setDevelopment(false);
53-
$version->setLicense([]);
54-
$version->setAutoload([]);
55-
$version->setDist(['reference' => 'old-dist-ref', 'type' => 'zip', 'url' => 'https://example.org/dist.zip']);
64+
public function testVersionChangesGetRecorded(): void
65+
{
66+
$container = static::getContainer();
67+
$em = $container->get(ManagerRegistry::class)->getManager();
5668

57-
$em->persist($package);
58-
$em->persist($version);
59-
$em->flush();
69+
$version = $this->createPackageAndVersion();
6070

6171
$version->setDist(['reference' => 'new-dist-ref', 'type' => 'zip', 'url' => 'https://example.org/dist.zip']);
6272
$version->setSource(['reference' => 'new-source-ref', 'type' => 'git', 'url' => 'git://example.org/dist.zip']);
6373
$em->persist($version);
6474
$em->flush();
6575

6676
$logs = $container->get(Connection::class)->fetchAllAssociative('SELECT * FROM audit_log ORDER BY id DESC');
67-
self::assertCount(2, $logs); // package creation + version reference change
77+
self::assertCount(3, $logs); // package creation + version creation + version reference change
6878
self::assertSame(AuditRecordType::VersionReferenceChanged->value, $logs[0]['type']);
69-
self::assertSame('{"name": "composer/composer", "dist_to": "new-dist-ref", "version": "1.0.0", "dist_from": "old-dist-ref", "source_to": "new-source-ref", "source_from": null}', $logs[0]['attributes']);
79+
self::assertSame('{"name": "composer/composer", "dist_to": "new-dist-ref", "version": "1.0.0", "dist_from": "dist-ref", "source_to": "new-source-ref", "source_from": "source-ref"}', $logs[0]['attributes']);
7080

7181
// verify that unrelated changes do not create new audit logs
7282
$version->setLicense(['MIT']);
7383
$em->persist($version);
7484
$em->flush();
7585

7686
$logs = $container->get(Connection::class)->fetchAllAssociative('SELECT * FROM audit_log ORDER BY id DESC');
77-
self::assertCount(2, $logs); // package creation + version reference change
87+
self::assertCount(3, $logs); // package creation + version creation + version reference change
7888

7989
// verify that changing dist only without ref change does not create new audit log and does not crash
8090
$version->setDist(['reference' => 'new-dist-ref', 'type' => 'zip2', 'url' => 'https://example.org/dist.zip2']);
8191
$em->persist($version);
8292
$em->flush();
8393

8494
$logs = $container->get(Connection::class)->fetchAllAssociative('SELECT * FROM audit_log ORDER BY id DESC');
85-
self::assertCount(2, $logs); // package creation + version reference change
95+
self::assertCount(3, $logs); // package creation + version creation + version reference change
8696

8797
// verify that only reference changes triggers a new audit log
8898
$version->setDist(['reference' => 'new-dist-ref', 'type' => 'zip3', 'url' => 'https://example.org/dist.zip2']);
@@ -91,13 +101,47 @@ public function testVersionChangesGetRecorded(): void
91101
$em->flush();
92102

93103
$logs = $container->get(Connection::class)->fetchAllAssociative('SELECT * FROM audit_log ORDER BY id DESC');
94-
self::assertCount(2, $logs);
104+
self::assertCount(3, $logs);
95105

96-
$em->remove($version);
106+
$em->getRepository(Version::class)->remove($version);
97107
$em->flush();
98108

99109
$logs = $container->get(Connection::class)->fetchAllAssociative('SELECT * FROM audit_log ORDER BY id DESC');
100-
self::assertCount(3, $logs);
110+
self::assertCount(4, $logs);
101111
self::assertSame(AuditRecordType::VersionDeleted->value, $logs[0]['type']);
102112
}
113+
114+
private function createPackageAndVersion(): Version
115+
{
116+
$container = static::getContainer();
117+
$em = $container->get(ManagerRegistry::class)->getManager();
118+
119+
$package = new Package();
120+
$package->setName('composer/composer');
121+
$package->setRepository('https://github.com/composer/composer');
122+
123+
$version = new Version();
124+
$version->setPackage($package);
125+
$version->setName($package->getName());
126+
$version->setVersion('1.0.0');
127+
$version->setNormalizedVersion('1.0.0.0');
128+
$version->setDevelopment(false);
129+
$version->setLicense([]);
130+
$version->setAutoload([]);
131+
$version->setDist(['reference' => 'dist-ref', 'type' => 'zip', 'url' => 'https://example.org/dist.zip']);
132+
$version->setSource(['reference' => 'source-ref', 'type' => 'git', 'url' => 'https://example.org/dist.git']);
133+
134+
$link = new RequireLink();
135+
$link->setVersion($version);
136+
$link->setPackageVersion('^1.5.0');
137+
$link->setPackageName('composer/ca-bundle');
138+
$version->addRequireLink($link);
139+
140+
$em->persist($link);
141+
$em->persist($package);
142+
$em->persist($version);
143+
$em->flush();
144+
145+
return $version;
146+
}
103147
}

translations/messages.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,5 +190,6 @@ audit_log:
190190
user_created: User created
191191
user_deleted: User deleted
192192
username_changed: Username changed
193+
version_created: Version created
193194
version_deleted: Version deleted
194195
version_reference_changed: Version reference changed

0 commit comments

Comments
 (0)