feat(api): add configurable limits to playlist and track creation (#545)

This commit is contained in:
paranarimasu
2023-03-12 15:32:31 -05:00
committed by GitHub
parent de084e4eda
commit 3dcd4e27fc
11 changed files with 400 additions and 0 deletions
+4
View File
@@ -210,6 +210,10 @@ NOVA_LICENSE_KEY=
NOVA_PASSWORDS=null
NOVA_PATH=/admin
# playlist
PLAYLIST_MAX_TRACKS=1000
USER_MAX_PLAYLISTS=1000
# queue
QUEUE_CONNECTION=sync
REDIS_QUEUE=default
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Constants\Config;
/**
* Class PlaylistConstants.
*/
class PlaylistConstants
{
final public const MAX_PLAYLISTS_QUALIFIED = 'playlist.user_max_playlists';
final public const MAX_TRACKS_QUALIFIED = 'playlist.playlist_max_tracks';
}
@@ -14,6 +14,7 @@ use App\Actions\Http\Api\ShowAction;
use App\Constants\Config\FlagConstants;
use App\Http\Api\Query\Query;
use App\Http\Controllers\Api\BaseController;
use App\Http\Middleware\Models\List\PlaylistExceedsTrackLimit;
use App\Http\Requests\Api\IndexRequest;
use App\Http\Requests\Api\ShowRequest;
use App\Http\Requests\Api\StoreRequest;
@@ -44,6 +45,7 @@ class TrackController extends BaseController
->__toString();
$this->middleware($isPlaylistManagementAllowed)->except(['index', 'show']);
$this->middleware(PlaylistExceedsTrackLimit::class)->only(['store', 'restore']);
}
/**
@@ -15,6 +15,7 @@ use App\Constants\Config\FlagConstants;
use App\Enums\Models\List\PlaylistVisibility;
use App\Http\Api\Query\Query;
use App\Http\Controllers\Api\BaseController;
use App\Http\Middleware\Models\List\UserExceedsPlaylistLimit;
use App\Http\Requests\Api\IndexRequest;
use App\Http\Requests\Api\ShowRequest;
use App\Http\Requests\Api\StoreRequest;
@@ -45,6 +46,7 @@ class PlaylistController extends BaseController
->__toString();
$this->middleware($isPlaylistManagementAllowed)->except(['index', 'show']);
$this->middleware(UserExceedsPlaylistLimit::class)->only(['store', 'restore']);
}
/**
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware\Models\List;
use App\Constants\Config\PlaylistConstants;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
use App\Models\List\Playlist;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
class PlaylistExceedsTrackLimit
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure(Request): mixed $next
* @return mixed
*/
public function handle(Request $request, Closure $next): mixed
{
$trackLimit = intval(Config::get(PlaylistConstants::MAX_TRACKS_QUALIFIED));
/** @var Playlist|null $playlist */
$playlist = $request->route('playlist');
/** @var User|null $user */
$user = $request->user('sanctum');
if (
intval($playlist?->tracks()?->count()) >= $trackLimit
&& empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS))
) {
abort(403, "Playlists cannot contain more than '$trackLimit' tracks.");
}
return $next($request);
}
}
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Http\Middleware\Models\List;
use App\Constants\Config\PlaylistConstants;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
class UserExceedsPlaylistLimit
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure(Request): mixed $next
* @return mixed
*/
public function handle(Request $request, Closure $next): mixed
{
$playlistLimit = intval(Config::get(PlaylistConstants::MAX_PLAYLISTS_QUALIFIED));
/** @var User|null $user */
$user = $request->user('sanctum');
if (
intval($user?->playlists()?->count()) >= $playlistLimit
&& empty($user?->can(SpecialPermission::BYPASS_FEATURE_FLAGS))
) {
abort(403, "User cannot have more than '$playlistLimit' playlists.");
}
return $next($request);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
return [
/*
|--------------------------------------------------------------------------
| Limits
|--------------------------------------------------------------------------
|
| These values represent caps on playlists to prevent spam. By default,
| an individual playlist is permitted 1000 tracks, and a user
| is permitted 1000 playlists.
|
*/
'playlist_max_tracks' => (int) env('PLAYLIST_MAX_TRACKS', 1000),
'user_max_playlists' => (int) env('USER_MAX_PLAYLISTS', 1000),
];
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Tests\Feature\Http\Api\List\Playlist;
use App\Constants\Config\FlagConstants;
use App\Constants\Config\PlaylistConstants;
use App\Enums\Auth\ExtendedCrudPermission;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
@@ -183,4 +184,68 @@ class PlaylistRestoreTest extends TestCase
$response->assertOk();
static::assertNotSoftDeleted($playlist);
}
/**
* The Playlist Restore Endpoint shall forbid users from restoring playlists that exceed the user playlist limit.
*
* @return void
*/
public function testMaxTrackLimit(): void
{
$playlistLimit = $this->faker->randomDigitNotNull();
Config::set(PlaylistConstants::MAX_PLAYLISTS_QUALIFIED, $playlistLimit);
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
$user = User::factory()
->has(Playlist::factory()->count($playlistLimit))
->withPermissions(ExtendedCrudPermission::RESTORE()->format(Playlist::class))
->createOne();
$playlist = Playlist::factory()
->for($user)
->createOne();
$playlist->delete();
Sanctum::actingAs($user);
$response = $this->patch(route('api.playlist.restore', ['playlist' => $playlist]));
$response->assertForbidden();
}
/**
* The Playlist Restore Endpoint shall permit users with bypass feature flag permission
* to restore playlists that exceed the user playlist limit.
*
* @return void
*/
public function testMaxTrackLimitPermittedForBypass(): void
{
$playlistLimit = $this->faker->randomDigitNotNull();
Config::set(PlaylistConstants::MAX_PLAYLISTS_QUALIFIED, $playlistLimit);
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
$user = User::factory()
->has(Playlist::factory()->count($playlistLimit))
->withPermissions(
ExtendedCrudPermission::RESTORE()->format(Playlist::class),
SpecialPermission::BYPASS_FEATURE_FLAGS
)
->createOne();
$playlist = Playlist::factory()
->for($user)
->createOne();
$playlist->delete();
Sanctum::actingAs($user);
$response = $this->patch(route('api.playlist.restore', ['playlist' => $playlist]));
$response->assertOk();
}
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Tests\Feature\Http\Api\List\Playlist;
use App\Constants\Config\FlagConstants;
use App\Constants\Config\PlaylistConstants;
use App\Enums\Auth\CrudPermission;
use App\Enums\Auth\SpecialPermission;
use App\Enums\Models\List\PlaylistVisibility;
@@ -158,4 +159,66 @@ class PlaylistStoreTest extends TestCase
$response->assertCreated();
}
/**
* The Playlist Store Endpoint shall forbid users from creating playlists that exceed the user playlist limit.
*
* @return void
*/
public function testMaxTrackLimit(): void
{
$playlistLimit = $this->faker->randomDigitNotNull();
Config::set(PlaylistConstants::MAX_PLAYLISTS_QUALIFIED, $playlistLimit);
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
$parameters = array_merge(
Playlist::factory()->raw(),
[Playlist::ATTRIBUTE_VISIBILITY => PlaylistVisibility::getRandomInstance()->description],
);
$user = User::factory()
->has(Playlist::factory()->count($playlistLimit))
->withPermissions(CrudPermission::CREATE()->format(Playlist::class))
->createOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.playlist.store', $parameters));
$response->assertForbidden();
}
/**
* The Playlist Store Endpoint shall permit users with bypass feature flag permission
* to create playlists that exceed the user playlist limit.
*
* @return void
*/
public function testMaxTrackLimitPermittedForBypass(): void
{
$playlistLimit = $this->faker->randomDigitNotNull();
Config::set(PlaylistConstants::MAX_PLAYLISTS_QUALIFIED, $playlistLimit);
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
$parameters = array_merge(
Playlist::factory()->raw(),
[Playlist::ATTRIBUTE_VISIBILITY => PlaylistVisibility::getRandomInstance()->description],
);
$user = User::factory()
->has(Playlist::factory()->count($playlistLimit))
->withPermissions(
CrudPermission::CREATE()->format(Playlist::class),
SpecialPermission::BYPASS_FEATURE_FLAGS
)
->createOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.playlist.store', $parameters));
$response->assertCreated();
}
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Tests\Feature\Http\Api\List\Playlist\Track;
use App\Constants\Config\FlagConstants;
use App\Constants\Config\PlaylistConstants;
use App\Enums\Auth\CrudPermission;
use App\Enums\Auth\ExtendedCrudPermission;
use App\Enums\Auth\SpecialPermission;
@@ -400,4 +401,80 @@ class TrackRestoreTest extends TestCase
$response->assertOk();
}
/**
* The Track Restore Endpoint shall forbid users from restoring playlist tracks that exceed the max track limit.
*
* @return void
*/
public function testMaxTrackLimit(): void
{
$trackLimit = $this->faker->randomDigitNotNull();
Config::set(PlaylistConstants::MAX_TRACKS_QUALIFIED, $trackLimit);
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
$user = User::factory()
->withPermissions(
CrudPermission::DELETE()->format(PlaylistTrack::class),
ExtendedCrudPermission::RESTORE()->format(PlaylistTrack::class)
)
->createOne();
$playlist = Playlist::factory()
->tracks($trackLimit)
->for($user)
->createOne();
$track = PlaylistTrack::factory()
->for($playlist)
->createOne();
Sanctum::actingAs($user);
$this->delete(route('api.playlist.track.destroy', ['playlist' => $playlist, 'track' => $track]));
$response = $this->patch(route('api.playlist.track.restore', ['playlist' => $playlist, 'track' => $track]));
$response->assertForbidden();
}
/**
* The Track Restore Endpoint shall permit users with bypass feature flag permission
* to restore playlist tracks that exceed the max track limit.
*
* @return void
*/
public function testMaxTrackLimitPermittedForBypass(): void
{
$trackLimit = $this->faker->randomDigitNotNull();
Config::set(PlaylistConstants::MAX_TRACKS_QUALIFIED, $trackLimit);
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
$user = User::factory()
->withPermissions(
CrudPermission::DELETE()->format(PlaylistTrack::class),
ExtendedCrudPermission::RESTORE()->format(PlaylistTrack::class),
SpecialPermission::BYPASS_FEATURE_FLAGS
)
->createOne();
$playlist = Playlist::factory()
->tracks($trackLimit)
->for($user)
->createOne();
$track = PlaylistTrack::factory()
->for($playlist)
->createOne();
Sanctum::actingAs($user);
$this->delete(route('api.playlist.track.destroy', ['playlist' => $playlist, 'track' => $track]));
$response = $this->patch(route('api.playlist.track.restore', ['playlist' => $playlist, 'track' => $track]));
$response->assertOk();
}
}
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Tests\Feature\Http\Api\List\Playlist\Track;
use App\Constants\Config\FlagConstants;
use App\Constants\Config\PlaylistConstants;
use App\Enums\Auth\CrudPermission;
use App\Enums\Auth\SpecialPermission;
use App\Models\Auth\User;
@@ -517,4 +518,72 @@ class TrackStoreTest extends TestCase
$response->assertCreated();
}
/**
* The Track Store Endpoint shall forbid users from creating playlists that exceed the max track limit.
*
* @return void
*/
public function testMaxTrackLimit(): void
{
$trackLimit = $this->faker->randomDigitNotNull();
Config::set(PlaylistConstants::MAX_TRACKS_QUALIFIED, $trackLimit);
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
$user = User::factory()->withPermissions(CrudPermission::CREATE()->format(PlaylistTrack::class))->createOne();
$playlist = Playlist::factory()
->tracks($trackLimit)
->for($user)
->createOne();
$track = PlaylistTrack::factory()
->for($playlist)
->for(Video::factory())
->makeOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.playlist.track.store', ['playlist' => $playlist] + $track->toArray()));
$response->assertForbidden();
}
/**
* The Track Store Endpoint shall permit users with bypass feature flag permission
* to create playlists that exceed the max track limit.
*
* @return void
*/
public function testMaxTrackLimitPermittedForBypass(): void
{
$trackLimit = $this->faker->randomDigitNotNull();
Config::set(PlaylistConstants::MAX_TRACKS_QUALIFIED, $trackLimit);
Config::set(FlagConstants::ALLOW_PLAYLIST_MANAGEMENT_QUALIFIED, true);
$user = User::factory()
->withPermissions(
CrudPermission::CREATE()->format(PlaylistTrack::class),
SpecialPermission::BYPASS_FEATURE_FLAGS
)
->createOne();
$playlist = Playlist::factory()
->tracks($trackLimit)
->for($user)
->createOne();
$track = PlaylistTrack::factory()
->for($playlist)
->for(Video::factory())
->makeOne();
Sanctum::actingAs($user);
$response = $this->post(route('api.playlist.track.store', ['playlist' => $playlist] + $track->toArray()));
$response->assertCreated();
}
}