fix(core) : harden review findings (me-provider null guard, audit-ignore plainpassword, rbac self-edit guard, module id dedup, audit pagination guard)
This commit is contained in:
@@ -98,7 +98,9 @@ const entityTypeOptions = computed<{ value: string, label: string }[]>(() =>
|
|||||||
entityTypes.value.map((value) => ({ value, label: entityTypeLabel(value) })),
|
entityTypes.value.map((value) => ({ value, label: entityTypeLabel(value) })),
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasNextPage = computed(() => page.value * PAGE_SIZE < totalItems.value)
|
// PAGE_SIZE must match the API default page size. The full-page guard keeps the
|
||||||
|
// "next" button accurate even on the last (partial) page.
|
||||||
|
const hasNextPage = computed(() => rows.value.length >= PAGE_SIZE && page.value * PAGE_SIZE < totalItems.value)
|
||||||
|
|
||||||
function entityTypeLabel(value: string): string {
|
function entityTypeLabel(value: string): string {
|
||||||
const key = `audit.entity.${value}`
|
const key = `audit.entity.${value}`
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, SharedU
|
|||||||
private ?string $password = null;
|
private ?string $password = null;
|
||||||
|
|
||||||
#[Groups(['user:write'])]
|
#[Groups(['user:write'])]
|
||||||
|
#[AuditIgnore]
|
||||||
private ?string $plainPassword = null;
|
private ?string $plainPassword = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use ApiPlatform\Metadata\Operation;
|
|||||||
use ApiPlatform\State\ProviderInterface;
|
use ApiPlatform\State\ProviderInterface;
|
||||||
use App\Module\Core\Domain\Entity\User;
|
use App\Module\Core\Domain\Entity\User;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @implements ProviderInterface<User>
|
* @implements ProviderInterface<User>
|
||||||
@@ -20,7 +21,11 @@ final readonly class MeProvider implements ProviderInterface
|
|||||||
|
|
||||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): User
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): User
|
||||||
{
|
{
|
||||||
// @var User $user
|
$user = $this->security->getUser();
|
||||||
return $this->security->getUser();
|
if (!$user instanceof User) {
|
||||||
|
throw new UnauthorizedHttpException('Bearer', 'Not authenticated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use ApiPlatform\Metadata\Operation;
|
|||||||
use ApiPlatform\State\ProcessorInterface;
|
use ApiPlatform\State\ProcessorInterface;
|
||||||
use App\Module\Core\Domain\Entity\User;
|
use App\Module\Core\Domain\Entity\User;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
|
|
||||||
use function assert;
|
use function assert;
|
||||||
|
|
||||||
@@ -16,12 +18,23 @@ use function assert;
|
|||||||
*/
|
*/
|
||||||
final readonly class UserRbacProcessor implements ProcessorInterface
|
final readonly class UserRbacProcessor implements ProcessorInterface
|
||||||
{
|
{
|
||||||
public function __construct(private EntityManagerInterface $em) {}
|
public function __construct(
|
||||||
|
private EntityManagerInterface $em,
|
||||||
|
private Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User
|
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User
|
||||||
{
|
{
|
||||||
assert($data instanceof User);
|
assert($data instanceof User);
|
||||||
|
|
||||||
|
// Defense-in-depth: a user may never edit their OWN RBAC assignment
|
||||||
|
// through this endpoint, even with core.users.manage — this prevents
|
||||||
|
// self-escalation if the permission is ever delegated to a non-admin.
|
||||||
|
$current = $this->security->getUser();
|
||||||
|
if ($current instanceof User && $current->getId() === $data->getId()) {
|
||||||
|
throw new AccessDeniedHttpException('You cannot edit your own RBAC assignment.');
|
||||||
|
}
|
||||||
|
|
||||||
$this->em->persist($data);
|
$this->em->persist($data);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,14 @@ final class ModuleRegistry
|
|||||||
{
|
{
|
||||||
$ids = [];
|
$ids = [];
|
||||||
foreach ($moduleClasses as $moduleClass) {
|
foreach ($moduleClasses as $moduleClass) {
|
||||||
if (is_a($moduleClass, ModuleInterface::class, true)) {
|
if (!is_a($moduleClass, ModuleInterface::class, true)) {
|
||||||
$ids[] = $moduleClass::id();
|
continue;
|
||||||
}
|
}
|
||||||
|
$id = $moduleClass::id();
|
||||||
|
if (in_array($id, $ids, true)) {
|
||||||
|
throw new InvalidArgumentException(sprintf('Module ID "%s" déclaré plusieurs fois dans la configuration des modules.', $id));
|
||||||
|
}
|
||||||
|
$ids[] = $id;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $ids;
|
return $ids;
|
||||||
|
|||||||
Reference in New Issue
Block a user