Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/packages/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ security:
ROLE_SPAMMER: ~
ROLE_DISABLE_2FA: ~
ROLE_AUDITOR: ~ # grants access to full audit records including PII (IP, emails, ..) as well as wildcard search in audit log
ROLE_FILTER_LIST_ADMIN: ~
ROLE_ADMIN_ORGS: ~ # grants access to the organizations feature (create/manage orgs) plus moderation

ROLE_ADMIN: [ ROLE_USER, ROLE_UPDATE_PACKAGES, ROLE_EDIT_PACKAGES, ROLE_DELETE_PACKAGES, ROLE_ANTISPAM, ROLE_DISABLE_2FA, ROLE_AUDITOR, ROLE_ADMIN_ORGS ]
ROLE_ADMIN: [ ROLE_USER, ROLE_UPDATE_PACKAGES, ROLE_EDIT_PACKAGES, ROLE_DELETE_PACKAGES, ROLE_ANTISPAM, ROLE_DISABLE_2FA, ROLE_AUDITOR, ROLE_FILTER_LIST_ADMIN, ROLE_ADMIN_ORGS ]
ROLE_SUPERADMIN: [ ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ]

when@test:
Expand Down
1 change: 1 addition & 0 deletions js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import notifier from './notifier';
import './search';
import './view';
import './submitPackage';
import './filterListAdmin';
import '../css/app.scss';
import 'bootstrap';

Expand Down
28 changes: 28 additions & 0 deletions js/filterListAdmin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import jQuery from "jquery";

const init = function ($) {
"use strict";

const form = $('#filter-list-bulk-form');
const selectAll = form.find('[data-bulk-select-all]');
const checkboxes = form.find('[data-bulk-select]');

function refresh() {
const checked = checkboxes.filter(':checked').length;
selectAll.prop('checked', checked > 0 && checked === checkboxes.length);
selectAll.prop('indeterminate', checked > 0 && checked < checkboxes.length);
}

selectAll.on('change', function () {
checkboxes.prop('checked', selectAll.prop('checked'));
refresh();
});

checkboxes.on('change', refresh);

refresh();
};

if (document.querySelector('#filter-list-bulk-form')) {
init(jQuery);
}
20 changes: 20 additions & 0 deletions migrations/2026_07_audit_log_filter_list_public_id_backfill.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- Backfill audit_log attributes.entry.public_id for filter list entries
--
-- The new filter list edit view lists the audit records that belong to an entry by matching on
-- attributes.$.entry.public_id. Older filter_list_entry_added / filter_list_entry_deleted records
-- were written before publicId was tracked, so they carry no public_id and would not appear.
--
-- This copies each entry's current publicId onto its historical records, matching on the
-- package name / list / version snapshot stored in the audit attributes. The public_id IS NULL
-- guard keeps it idempotent and skips rows already written by the new code path.
--
-- Run AFTER deploying the new code (it relies on filter_list_entry.publicId being populated).

UPDATE audit_log a
INNER JOIN filter_list_entry fle
ON fle.packageName = JSON_UNQUOTE(JSON_EXTRACT(a.attributes, '$.entry.package_name'))
AND fle.list = JSON_UNQUOTE(JSON_EXTRACT(a.attributes, '$.entry.list'))
AND fle.version = JSON_UNQUOTE(JSON_EXTRACT(a.attributes, '$.entry.version'))
SET a.attributes = JSON_SET(a.attributes, '$.entry.public_id', fle.publicId)
WHERE a.type IN ('filter_list_entry_added', 'filter_list_entry_deleted')
AND JSON_EXTRACT(a.attributes, '$.entry.public_id') IS NULL;
24 changes: 24 additions & 0 deletions migrations/2026_07_filter_list_entry_admin_controls.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- Filter list entry admin controls (enable/disable, version overwrite, internal notes)
--
-- Adds the columns backing the new admin UI on filter_list_entry:
-- disabled soft on/off toggle so an entry can be suspended without deleting it
-- overwriteVersion admin-supplied constraint that overrides the upstream-reported one
-- (the upstream identity always stays in `version`, so the resolver still
-- recognises the entry across syncs regardless of the override)
-- internalNote free-form admin note, not shown publicly
--
-- `list` and `source` are also shrunk from the default VARCHAR(255) to VARCHAR(32): the new
-- UNIQUE index spans four columns and the full-width version would exceed the 3072-byte key
-- limit (ERROR 1071). Both are enum-backed, so 32 chars is ample. `source` is promoted into
-- the unique key so the same (list, packageName, version) can exist for different sources.

ALTER TABLE filter_list_entry
ADD COLUMN disabled TINYINT(1) NOT NULL DEFAULT 0,
ADD COLUMN overwriteVersion VARCHAR(255) DEFAULT NULL,
ADD COLUMN internalNote LONGTEXT DEFAULT NULL;

ALTER TABLE filter_list_entry
MODIFY `list` VARCHAR(32) NOT NULL,
MODIFY `source` VARCHAR(32) NOT NULL,
DROP INDEX list_package_version_idx,
ADD UNIQUE INDEX list_package_version_source_idx (`list`, `packageName`, `version`, `source`);
14 changes: 14 additions & 0 deletions migrations/2026_07c_audit_log_filter_list_package_id_backfill.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- Backfill audit_log.packageId for filter list entries

UPDATE audit_log a
JOIN package p
ON p.name = JSON_UNQUOTE(JSON_EXTRACT(a.attributes, '$.entry.package_name'))
SET a.packageId = p.id
WHERE a.type IN (
'filter_list_entry_added',
'filter_list_entry_deleted',
'filter_list_entry_disabled',
'filter_list_entry_enabled',
'filter_list_entry_edited'
)
AND a.packageId IS NULL;
7 changes: 6 additions & 1 deletion src/Audit/AuditRecordType.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ enum AuditRecordType: string
// filterlist
case FilterListEntryAdded = 'filter_list_entry_added';
case FilterListEntryDeleted = 'filter_list_entry_deleted';
case FilterListEntryDisabled = 'filter_list_entry_disabled';
case FilterListEntryEnabled = 'filter_list_entry_enabled';
case FilterListEntryEdited = 'filter_list_entry_edited';

// organization
case OrganizationCreated = 'organization_created';
Expand All @@ -69,7 +72,9 @@ public function category(): string
self::EmailChanged, self::UsernameChanged, self::GitHubLinkedWithUser,
self::GitHubDisconnectedFromUser, self::TwoFaAuthenticationActivated,
self::TwoFaAuthenticationDeactivated => 'user',
self::FilterListEntryAdded, self::FilterListEntryDeleted => 'filterlist',
self::FilterListEntryAdded, self::FilterListEntryDeleted,
self::FilterListEntryDisabled, self::FilterListEntryEnabled,
self::FilterListEntryEdited => 'filterlist',
self::OrganizationCreated => 'organization',
};
}
Expand Down
37 changes: 37 additions & 0 deletions src/Audit/Display/AuditLogDisplayFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,43 @@ public function buildSingle(AuditRecord $record): AuditLogDisplayInterface
$this->buildActor($record->attributes['actor']),
$record->ip,
),
AuditRecordType::FilterListEntryDisabled => new FilterListEntryDisabledDisplay(
$record->datetime,
$record->attributes['entry']['package_name'],
$record->attributes['entry']['version'],
FilterLists::from($record->attributes['entry']['list']),
FilterSources::from($record->attributes['entry']['source']),
$record->attributes['entry']['reason'] ?? null,
$this->buildActor($record->attributes['actor'] ?? null),
$record->ip
),
AuditRecordType::FilterListEntryEnabled => new FilterListEntryEnabledDisplay(
$record->datetime,
$record->attributes['entry']['package_name'],
$record->attributes['entry']['version'],
FilterLists::from($record->attributes['entry']['list']),
FilterSources::from($record->attributes['entry']['source']),
$record->attributes['entry']['reason'] ?? null,
$this->buildActor($record->attributes['actor'] ?? null),
$record->ip
),
AuditRecordType::FilterListEntryEdited => new FilterListEntryEditedDisplay(
$record->datetime,
$record->attributes['entry']['package_name'],
$record->attributes['entry']['version'],
$record->attributes['previous']['version'] ?? $record->attributes['entry']['version'],
$record->attributes['entry']['reason'] ?? null,
$record->attributes['previous']['reason'] ?? $record->attributes['entry']['reason'] ?? null,
$record->attributes['entry']['link'] ?? null,
$record->attributes['previous']['link'] ?? $record->attributes['entry']['link'] ?? null,
$record->attributes['entry']['internal_note'] ?? null,
$record->attributes['previous']['internal_note'] ?? null,
FilterLists::from($record->attributes['entry']['list']),
FilterLists::from($record->attributes['previous']['list'] ?? $record->attributes['entry']['list']),
FilterSources::from($record->attributes['entry']['source']),
$this->buildActor($record->attributes['actor'] ?? null),
$record->ip
),
};
}

Expand Down
43 changes: 43 additions & 0 deletions src/Audit/Display/FilterListEntryDisabledDisplay.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php declare(strict_types=1);

/*
* This file is part of Packagist.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
* Nils Adermann <naderman@naderman.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Audit\Display;

use App\Audit\AuditRecordType;
use App\FilterList\FilterLists;
use App\FilterList\FilterSources;

readonly class FilterListEntryDisabledDisplay extends AbstractAuditLogDisplay
{
public function __construct(
\DateTimeImmutable $datetime,
public string $packageName,
public string $version,
public FilterLists $list,
public FilterSources $source,
public ?string $reason,
ActorDisplay $actor,
?string $ip,
) {
parent::__construct($datetime, $actor, $ip);
}

public function getType(): AuditRecordType
{
return AuditRecordType::FilterListEntryDisabled;
}

public function getTemplateName(): string
{
return 'audit_log/display/filter_list_entry_disabled.html.twig';
}
}
50 changes: 50 additions & 0 deletions src/Audit/Display/FilterListEntryEditedDisplay.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php declare(strict_types=1);

/*
* This file is part of Packagist.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
* Nils Adermann <naderman@naderman.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Audit\Display;

use App\Audit\AuditRecordType;
use App\FilterList\FilterLists;
use App\FilterList\FilterSources;

readonly class FilterListEntryEditedDisplay extends AbstractAuditLogDisplay
{
public function __construct(
\DateTimeImmutable $datetime,
public string $packageName,
public string $version,
public string $previousVersion,
public ?string $reason,
public ?string $previousReason,
public ?string $link,
public ?string $previousLink,
public ?string $internalNote,
public ?string $previousInternalNote,
public FilterLists $list,
public FilterLists $previousList,
public FilterSources $source,
ActorDisplay $actor,
?string $ip,
) {
parent::__construct($datetime, $actor, $ip);
}

public function getType(): AuditRecordType
{
return AuditRecordType::FilterListEntryEdited;
}

public function getTemplateName(): string
{
return 'audit_log/display/filter_list_entry_edited.html.twig';
}
}
43 changes: 43 additions & 0 deletions src/Audit/Display/FilterListEntryEnabledDisplay.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php declare(strict_types=1);

/*
* This file is part of Packagist.
*
* (c) Jordi Boggiano <j.boggiano@seld.be>
* Nils Adermann <naderman@naderman.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace App\Audit\Display;

use App\Audit\AuditRecordType;
use App\FilterList\FilterLists;
use App\FilterList\FilterSources;

readonly class FilterListEntryEnabledDisplay extends AbstractAuditLogDisplay
{
public function __construct(
\DateTimeImmutable $datetime,
public string $packageName,
public string $version,
public FilterLists $list,
public FilterSources $source,
public ?string $reason,
ActorDisplay $actor,
?string $ip,
) {
parent::__construct($datetime, $actor, $ip);
}

public function getType(): AuditRecordType
{
return AuditRecordType::FilterListEntryEnabled;
}

public function getTemplateName(): string
{
return 'audit_log/display/filter_list_entry_enabled.html.twig';
}
}
Loading