Restrict interaction with orphan visits when API key has that role

This commit is contained in:
Alejandro Celaya
2023-05-31 09:11:20 +02:00
parent 12da04ef37
commit eaba5edf7f
11 changed files with 78 additions and 20 deletions

View File

@@ -9,11 +9,15 @@ use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Repository\VisitRepositoryInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
{
public function __construct(private readonly VisitRepositoryInterface $repo, private readonly VisitsParams $params)
{
public function __construct(
private readonly VisitRepositoryInterface $repo,
private readonly VisitsParams $params,
private readonly ?ApiKey $apiKey,
) {
}
protected function doCount(): int
@@ -21,6 +25,7 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
return $this->repo->countOrphanVisits(new VisitsCountFiltering(
dateRange: $this->params->dateRange,
excludeBots: $this->params->excludeBots,
apiKey: $this->apiKey,
));
}
@@ -29,6 +34,7 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
return $this->repo->findOrphanVisits(new VisitsListFiltering(
dateRange: $this->params->dateRange,
excludeBots: $this->params->excludeBots,
apiKey: $this->apiKey,
limit: $length,
offset: $offset,
));

View File

@@ -17,6 +17,7 @@ use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsListFiltering;
use Shlinkio\Shlink\Core\Visit\Spec\CountOfNonOrphanVisits;
use Shlinkio\Shlink\Core\Visit\Spec\CountOfOrphanVisits;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use const PHP_INT_MAX;
@@ -139,6 +140,10 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
public function findOrphanVisits(VisitsListFiltering $filtering): array
{
if ($filtering->apiKey?->hasRole(Role::NO_ORPHAN_VISITS)) {
return [];
}
$qb = $this->createAllVisitsQueryBuilder($filtering);
$qb->andWhere($qb->expr()->isNull('v.shortUrl'));
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
@@ -146,6 +151,10 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
public function countOrphanVisits(VisitsCountFiltering $filtering): int
{
if ($filtering->apiKey?->hasRole(Role::NO_ORPHAN_VISITS)) {
return 0;
}
return (int) $this->matchSingleScalarResult(new CountOfOrphanVisits($filtering));
}

View File

@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Visit;
use Shlinkio\Shlink\Core\Model\BulkDeleteResult;
use Shlinkio\Shlink\Core\Visit\Repository\VisitDeleterRepositoryInterface;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsDeleter implements VisitsDeleterInterface
@@ -16,7 +17,7 @@ class VisitsDeleter implements VisitsDeleterInterface
public function deleteOrphanVisits(?ApiKey $apiKey = null): BulkDeleteResult
{
// TODO Check API key has permissions for orphan visits
return new BulkDeleteResult($this->repository->deleteOrphanVisits());
$affectedItems = $apiKey?->hasRole(Role::NO_ORPHAN_VISITS) ? 0 : $this->repository->deleteOrphanVisits();
return new BulkDeleteResult($affectedItems);
}
}

View File

@@ -43,11 +43,13 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
return new VisitsStats(
nonOrphanVisitsTotal: $visitsRepo->countNonOrphanVisits(VisitsCountFiltering::withApiKey($apiKey)),
orphanVisitsTotal: $visitsRepo->countOrphanVisits(new VisitsCountFiltering()),
orphanVisitsTotal: $visitsRepo->countOrphanVisits(VisitsCountFiltering::withApiKey($apiKey)),
nonOrphanVisitsNonBots: $visitsRepo->countNonOrphanVisits(
new VisitsCountFiltering(excludeBots: true, apiKey: $apiKey),
),
orphanVisitsNonBots: $visitsRepo->countOrphanVisits(new VisitsCountFiltering(excludeBots: true)),
orphanVisitsNonBots: $visitsRepo->countOrphanVisits(
new VisitsCountFiltering(excludeBots: true, apiKey: $apiKey),
),
);
}
@@ -114,12 +116,12 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
/**
* @return Visit[]|Paginator
*/
public function orphanVisits(VisitsParams $params): Paginator
public function orphanVisits(VisitsParams $params, ?ApiKey $apiKey = null): Paginator
{
/** @var VisitRepositoryInterface $repo */
$repo = $this->em->getRepository(Visit::class);
return $this->createPaginator(new OrphanVisitsPaginatorAdapter($repo, $params), $params);
return $this->createPaginator(new OrphanVisitsPaginatorAdapter($repo, $params, $apiKey), $params);
}
public function nonOrphanVisits(VisitsParams $params, ?ApiKey $apiKey = null): Paginator

View File

@@ -43,7 +43,7 @@ interface VisitsStatsHelperInterface
/**
* @return Visit[]|Paginator
*/
public function orphanVisits(VisitsParams $params): Paginator;
public function orphanVisits(VisitsParams $params, ?ApiKey $apiKey = null): Paginator;
/**
* @return Visit[]|Paginator