Skip to content
Open
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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,17 @@ CREATE TABLE sudo_users (
);
```

```sql
CREATE TABLE sudo_audit_logs (
id SERIAL PRIMARY KEY,
user_id BIGINT REFERENCES sudo_users(id),
action TEXT,
payload JSONB,
created_at TIMESTAMPTZ NOT NULL,
updated_at TIMESTAMPTZ NOT NULL
);
```

## Logging

```php
Expand Down
86 changes: 86 additions & 0 deletions bundle/src/Controller/SudoAuditLogController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

namespace Hyvor\Internal\Bundle\Controller;

use Hyvor\Internal\Auth\AuthInterface;
use Hyvor\Internal\Sudo\SudoAuditLogService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;

class SudoAuditLogController extends AbstractController
{

public function __construct(
private SudoAuditLogService $sudoAuditLogService,
private AuthInterface $auth
) {
}

#[Route("/api/sudo/audit-logs", methods: "GET")]
public function getAuditLogs(Request $request): JsonResponse
{
$limit = $request->query->getInt("limit", 50);
$offset = $request->query->getInt("offset", 0);
$userId = $request->query->getInt("user_id") ?: null;
$action = $request->query->getString("action") ?: null;
$dateStart = $request->query->getInt("date_start") ?: null;
$dateEnd = $request->query->getInt("date_end") ?: null;
$payloadParam = $request->query->getString("payload_param") ?: null;
$payloadValue = $request->query->getString("payload_value") ?: null;

if ($limit > 100 || $limit < 1) {
throw new BadRequestException("limit should be between 1 and 100");
}

if ($offset < 0) {
throw new BadRequestException("offset should not be less than 0");
}

if (($payloadParam != null && $payloadValue == null) || ($payloadValue != null && $payloadParam == null)) {
throw new BadRequestException("payload_param and payload_value are both required");
}

$dateStart = $dateStart !== null ? (new \DateTimeImmutable())->setTimestamp($dateStart) : null;
$dateEnd = $dateEnd !== null ? (new \DateTimeImmutable())->setTimestamp($dateEnd) : null;

if ($dateStart && $dateStart < new \DateTimeImmutable('@0')) {
throw new BadRequestException("date_start cannot be negative");
}

if ($dateEnd && $dateEnd < new \DateTimeImmutable('@0')) {
throw new BadRequestException("date_end cannot be negative");
}

if ($dateStart && $dateEnd && $dateStart >= $dateEnd) {
throw new BadRequestException("date_start must be before date_end");
}

$logs = $this->sudoAuditLogService->findLogs(
$userId,
$action,
$dateStart,
$dateEnd,
$payloadParam,
$payloadValue,
$limit,
$offset
);

$userIds = array_unique(array_map(fn($log) => $log->getUserId(), $logs));
$users = $this->auth->fromIds($userIds);

$data = array_map(fn($log) => [
"id" => $log->getId(),
"user" => $users[$log->getUserId()] ?? null,
"action" => $log->getAction(),
"payload" => $log->getPayload(),
"created_at" => $log->getCreatedAt()->format("c"),
"updated_at" => $log->getUpdatedAt()->format("c"),
], $logs);

return $this->json($data);
}
}
102 changes: 102 additions & 0 deletions bundle/src/Entity/SudoAuditLog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace Hyvor\Internal\Bundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Hyvor\Internal\Sudo\SudoAuditLogRepository;

#[ORM\Entity(repositoryClass: SudoAuditLogRepository::class)]
#[ORM\Table(name: "sudo_audit_logs")]
class SudoAuditLog
{

#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: "integer")]
private int $id;

#[ORM\Column(type: "bigint")]
private int $user_id;

#[ORM\Column(type: "text")]
private string $action;

/** @var array<string,scalar> $payload */
#[ORM\Column(type: "json", options: ["jsonb" => true])]
private array $payload;

#[ORM\Column(type: "datetime_immutable")]
private \DateTimeImmutable $created_at;

#[ORM\Column(type: "datetime_immutable")]
private \DateTimeImmutable $updated_at;

public function getId(): int
{
return $this->id;
}

public function setId(int $id): static
{
$this->id = $id;
return $this;
}

public function getUserId(): int
{
return $this->user_id;
}

public function setUserId(int $user_id): static
{
$this->user_id = $user_id;
return $this;
}

public function getAction(): string
{
return $this->action;
}

public function setAction(string $action): static
{
$this->action = $action;
return $this;
}

/** @return array<string,scalar> */
public function getPayload(): array
{
return $this->payload;
}

/** @param array<string,scalar> $payload */
public function setPayload(array $payload): static
{
$this->payload = $payload;
return $this;
}

public function getCreatedAt(): \DateTimeImmutable
{
return $this->created_at;
}

public function setCreatedAt(\DateTimeImmutable $created_at): static
{
$this->created_at = $created_at;
return $this;
}

public function getUpdatedAt(): \DateTimeImmutable
{
return $this->updated_at;
}

public function setUpdatedAt(\DateTimeImmutable $updated_at): static
{
$this->updated_at = $updated_at;
return $this;
}

}
52 changes: 52 additions & 0 deletions bundle/src/Testing/SudoAuditLogTestingTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace Hyvor\Internal\Bundle\Testing;

use Hyvor\Internal\Bundle\Entity\SudoAuditLog;
use Hyvor\Internal\Sudo\SudoAuditLogRepository;
use Doctrine\ORM\EntityManagerInterface;

trait SudoAuditLogTestingTrait
{
/**
* Assert that a SudoAuditLog with the given action and payload exists.
* @param array<string,scalar> $payload
*/
public function assertSudoLogged(string $action, array $payload): void
{
/** @var EntityManagerInterface $em */
$em = $this->getContainer()->get(EntityManagerInterface::class);

/** @var SudoAuditLogRepository $repo */
$repo = $em->getRepository(SudoAuditLog::class);

$logs = $repo->findLogs(
userId: null,
action: $action,
dateStart: null,
dateEnd: null,
payloadParam: null,
payloadValue: null,
limit: null,
offset: null
);

$found = false;

foreach ($logs as $log) {
if ($log->getPayload() === $payload) {
$found = true;
break;
}
}

$this->assertTrue(
$found,
sprintf(
'Expected SudoAuditLog with action "%s" and payload %s.',
$action,
json_encode($payload)
)
);
}
}
2 changes: 1 addition & 1 deletion src/Sudo/Event/SudoAddedEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ public function getSudoUser(): SudoUser
return $this->sudoUser;
}

}
}
85 changes: 85 additions & 0 deletions src/Sudo/SudoAuditLogRepository.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

namespace Hyvor\Internal\Sudo;

use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Hyvor\Internal\Bundle\Entity\SudoAuditLog;

/**
* @extends ServiceEntityRepository<SudoAuditLog>
* @codeCoverageIgnore
*/
class SudoAuditLogRepository extends ServiceEntityRepository
{

public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, SudoAuditLog::class);
}

/**
* @param ?int $userId
* @param ?string $action
* @param ?\DateTimeImmutable $dateStart
* @param ?\DateTimeImmutable $dateEnd
* @param ?string $payloadParam
* @param ?scalar $payloadValue
* @param ?int $limit
* @param ?int $offset
* @return SudoAuditLog[]
*/
public function findLogs(
?int $userId,
?string $action,
?\DateTimeImmutable $dateStart,
?\DateTimeImmutable $dateEnd,
?string $payloadParam,
mixed $payloadValue,
?int $limit,
?int $offset
): array {
$qb = $this->createQueryBuilder("s");

if ($userId !== null) {
$qb->andWhere("s.user_id = :userId")
->setParameter("userId", $userId);
}

if ($action !== null) {
$qb->andWhere("s.action = :action")
->setParameter("action", $action);
}

if ($dateStart !== null) {
$qb->andWhere("s.created_at >= :dateStart")
->setParameter("dateStart", $dateStart);
}

if ($dateEnd !== null) {
$qb->andWhere("s.created_at <= :dateEnd")
->setParameter("dateEnd", $dateEnd);
}

if ($payloadParam !== null && $payloadValue !== null) {
$qb->andWhere("s.payload @> :payloadFilter")
->setParameter(
"payloadFilter",
json_encode([$payloadParam => $payloadValue])
);
}

$qb->orderBy("s.created_at", "DESC");

if ($limit !== null) {
$qb->setMaxResults($limit);
}

if ($offset !== null) {
$qb->setFirstResult($offset);
}

return $qb->getQuery()->getResult();
}

}
Loading