Skip to content

Commit 621ad60

Browse files
committed
Move access to middleware
1 parent e22663c commit 621ad60

File tree

8 files changed

+342
-34
lines changed

8 files changed

+342
-34
lines changed

config/laravel-media-secure.php

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,87 @@
11
<?php
22

33
use CleaniqueCoders\LaravelMediaSecure\Http\Controllers\MediaController;
4+
use CleaniqueCoders\LaravelMediaSecure\Http\Middleware\ValidateMediaAccess;
45

56
return [
67
/**
7-
* Spatie's Model Class Name
8+
* Spatie's Media Library Model Class
9+
*
10+
* This specifies the Media model class from Spatie's MediaLibrary package
11+
* that will be used for media file management and database operations.
812
*/
913
'model' => \Spatie\MediaLibrary\MediaCollections\Models\Media::class,
1014

1115
/**
12-
* Spatie's Model Media Policy
16+
* Media Access Policy Class
17+
*
18+
* This policy class handles authorization logic for media access.
19+
* It determines whether a user can view, download, or stream specific media files.
1320
*/
1421
'policy' => \CleaniqueCoders\LaravelMediaSecure\Policies\MediaPolicy::class,
1522

1623
/**
17-
* Controller to manage access to the media.
24+
* Media Controller Configuration
25+
*
26+
* Defines the controller class and method responsible for handling
27+
* media requests after middleware validation and authorization.
1828
*/
1929
'controller' => [
2030
MediaController::class, '__invoke',
2131
],
2232

2333
/**
24-
* Middleware want to apply to the media route.
34+
* Route Middleware Stack
35+
*
36+
* Middleware applied to media routes in the specified order:
37+
* - 'auth': Ensures user is authenticated
38+
* - 'verified': Ensures user has verified their email (optional)
39+
* - ValidateMediaAccess: Validates media access type, authorizes user, and prepares media (MANDATORY)
40+
*
41+
* Note: The ValidateMediaAccess middleware is required and cannot be removed
42+
* as it handles critical security validation and media preparation.
2543
*/
2644
'middleware' => [
27-
'auth', 'verified',
45+
'auth',
46+
'verified',
47+
ValidateMediaAccess::class, // Mandatory - handles validation, authorization, and media preparation
2848
],
2949

3050
/**
31-
* Media URI.
51+
* Media Route URI Prefix
52+
*
53+
* The URL prefix for all media routes. Media files will be accessible
54+
* at URLs like: /media/{type}/{uuid}
55+
*
56+
* Example: /media/view/abc123-def456-ghi789
3257
*/
3358
'prefix' => 'media',
3459

3560
/**
36-
* Route name.
61+
* Media Route Name
62+
*
63+
* The named route identifier for media access routes.
64+
* Used for generating URLs with route() helper: route('media', ['type' => 'view', 'uuid' => $uuid])
3765
*/
3866
'route_name' => 'media',
3967

4068
/**
41-
* By default, all media require to be login.
69+
* Authentication Requirement
70+
*
71+
* Determines whether authentication is required for media access.
72+
* When true, users must be logged in to access any media files.
73+
* Set to false only if you want to allow public media access.
4274
*/
4375
'require_auth' => env('LARAVEL_MEDIA_SECURE_REQUIRE_AUTH', true),
4476

4577
/**
46-
* Strict mode - by default all media's model require policy.
78+
* Strict Authorization Mode
79+
*
80+
* When enabled, all media access requests must pass through the MediaPolicy
81+
* authorization checks. This provides fine-grained control over media access.
82+
*
83+
* When disabled, basic authentication (if required) is sufficient.
84+
* Recommended to keep enabled for maximum security.
4785
*/
4886
'strict' => env('LARAVEL_MEDIA_SECURE_STRICT', true),
4987
];

routes/web.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
config('laravel-media-secure.route_name')
1111
)
1212
->middleware(
13-
config('laravel-media-secure.middleware', ['auth', 'verified'])
13+
config('laravel-media-secure.middleware', ['auth', 'verified', 'validate-media-access'])
1414
);

src/Http/Controllers/MediaController.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,8 @@ class MediaController extends Controller
1414
*/
1515
public function __invoke(Request $request, string $type, string $uuid)
1616
{
17-
abort_if(! MediaAccess::acceptable($type), 422, 'Invalid request type of '.$type);
18-
19-
$media = config('laravel-media-secure.model')::whereUuid($uuid)->firstOrFail();
20-
21-
abort_if($request->user()->cannot($type, $media), 403, 'Unauthorized Access.');
17+
// Media has been fetched and authorized in the ValidateMediaAccess middleware
18+
$media = $request->attributes->get('media');
2219

2320
if ($type == MediaAccess::VIEW->value || $type == MediaAccess::STREAM->value) {
2421
return response()->make(file_get_contents($media->getPath()), 200, [
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace CleaniqueCoders\LaravelMediaSecure\Http\Middleware;
4+
5+
use CleaniqueCoders\LaravelMediaSecure\Enums\MediaAccess;
6+
use Closure;
7+
use Illuminate\Http\Request;
8+
9+
class ValidateMediaAccess
10+
{
11+
/**
12+
* Handle an incoming request.
13+
*
14+
* @return mixed
15+
*/
16+
public function handle(Request $request, Closure $next)
17+
{
18+
$type = $request->route('type');
19+
$uuid = $request->route('uuid');
20+
21+
// Validate media access type
22+
abort_if(! MediaAccess::acceptable($type), 422, 'Invalid request type of '.$type);
23+
24+
// Get the media model
25+
$media = config('laravel-media-secure.model')::whereUuid($uuid)->firstOrFail();
26+
27+
// Check authorization
28+
abort_if($request->user()->cannot($type, $media), 403, 'Unauthorized Access.');
29+
30+
// Add media to the request for use in the controller
31+
$request->attributes->add(['media' => $media]);
32+
33+
return $next($request);
34+
}
35+
}

src/LaravelMediaSecureServiceProvider.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,17 @@ public function bootingPackage()
2727
config('laravel-media-secure.model'),
2828
config('laravel-media-secure.policy'),
2929
);
30+
31+
$this->registerMiddleware();
32+
}
33+
34+
protected function registerMiddleware()
35+
{
36+
$router = $this->app['router'];
37+
38+
// Register the middleware with its alias if using Laravel < 10.0
39+
if (method_exists($router, 'aliasMiddleware')) {
40+
$router->aliasMiddleware('validate-media-access', \CleaniqueCoders\LaravelMediaSecure\Http\Middleware\ValidateMediaAccess::class);
41+
}
3042
}
3143
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<?php
2+
3+
use CleaniqueCoders\LaravelMediaSecure\Http\Middleware\ValidateMediaAccess;
4+
use Illuminate\Foundation\Testing\RefreshDatabase;
5+
use Illuminate\Http\Request;
6+
use Illuminate\Support\Facades\Artisan;
7+
use Illuminate\Support\Facades\Gate;
8+
use Illuminate\Support\Facades\Route;
9+
use Illuminate\Support\Facades\Schema;
10+
use Spatie\MediaLibrary\MediaCollections\Models\Media;
11+
use Symfony\Component\HttpKernel\Exception\HttpException;
12+
13+
uses(RefreshDatabase::class);
14+
15+
// Set up a login route for redirects and migrations
16+
beforeEach(function () {
17+
// Set up migrations if they don't exist
18+
if (! Schema::hasTable('users')) {
19+
Artisan::call('migrate', [
20+
'--path' => '../../../../tests/database/migrations/',
21+
]);
22+
}
23+
24+
Route::get('/login', fn () => 'login')->name('login');
25+
});
26+
27+
it('validates access type in the middleware', function () {
28+
// Set up test route with middleware
29+
Route::get('/test-media/{type}/{uuid}', function (Request $request, $type, $uuid) {
30+
return 'passed';
31+
})->middleware(ValidateMediaAccess::class);
32+
33+
// Remove exception handling to see the actual exceptions
34+
$this->withoutExceptionHandling();
35+
36+
// Create a real user for testing
37+
$user = user();
38+
login($user);
39+
40+
// Test with invalid type - should throw an exception
41+
$this->expectException(HttpException::class);
42+
$this->expectExceptionMessage('Invalid request type of invalid-type');
43+
44+
$this->get('/test-media/invalid-type/123-uuid');
45+
})->group('middleware');
46+
47+
it('checks authorization in the middleware', function () {
48+
// Set up test route with middleware
49+
Route::get('/test-media/{type}/{uuid}', function (Request $request, $type, $uuid) {
50+
return 'passed';
51+
})->middleware(ValidateMediaAccess::class);
52+
53+
// Remove exception handling
54+
$this->withoutExceptionHandling();
55+
56+
// Create a real user for testing
57+
$user = user();
58+
login($user);
59+
60+
// Create a fake media UUID that doesn't exist
61+
$fakeUuid = 'fake-uuid-123';
62+
63+
// Should throw model not found exception because media doesn't exist
64+
$this->expectException(\Illuminate\Database\Eloquent\ModelNotFoundException::class);
65+
66+
$this->get('/test-media/view/'.$fakeUuid);
67+
})->group('middleware');
68+
69+
it('adds media to the request attributes', function () {
70+
// Create test route that checks for media in request attributes
71+
Route::get('/test-media-attributes/{type}/{uuid}', function (Request $request, $type, $uuid) {
72+
return $request->attributes->has('media') ? 'has media' : 'no media';
73+
})->middleware(ValidateMediaAccess::class);
74+
75+
// Create a real user for testing
76+
$user = user();
77+
login($user);
78+
79+
// Create a real media record in the database
80+
$media = new \Spatie\MediaLibrary\MediaCollections\Models\Media([
81+
'model_type' => get_class($user),
82+
'model_id' => $user->id,
83+
'uuid' => 'test-uuid-123',
84+
'collection_name' => 'default',
85+
'name' => 'test-file',
86+
'file_name' => 'test.txt',
87+
'mime_type' => 'text/plain',
88+
'disk' => 'public',
89+
'conversions_disk' => 'public',
90+
'size' => 100,
91+
'manipulations' => '[]',
92+
'custom_properties' => '{}',
93+
'generated_conversions' => '{}',
94+
'responsive_images' => '{}',
95+
]);
96+
$media->save();
97+
98+
// Mock the Gate to allow access
99+
Gate::shouldReceive('forUser')->andReturnSelf();
100+
Gate::shouldReceive('check')->andReturn(true);
101+
102+
// Test that media was added to the request attributes
103+
$this->get('/test-media-attributes/view/'.$media->uuid)
104+
->assertSuccessful()
105+
->assertSee('has media');
106+
})->group('middleware');
107+
108+
// Test for each media type
109+
it('handles view media type correctly', function () {
110+
// Set up test route with middleware
111+
Route::get('/test-media/{type}/{uuid}', function (Request $request, $type, $uuid) {
112+
// Check that the type is correctly passed
113+
return 'Type: '.$type;
114+
})->middleware(ValidateMediaAccess::class);
115+
116+
// Create a real user for testing
117+
$user = user();
118+
login($user);
119+
120+
// Create a real media record in the database
121+
$media = new \Spatie\MediaLibrary\MediaCollections\Models\Media([
122+
'model_type' => get_class($user),
123+
'model_id' => $user->id,
124+
'uuid' => 'test-uuid-view',
125+
'collection_name' => 'default',
126+
'name' => 'test-file',
127+
'file_name' => 'test.txt',
128+
'mime_type' => 'text/plain',
129+
'disk' => 'public',
130+
'conversions_disk' => 'public',
131+
'size' => 100,
132+
'manipulations' => '[]',
133+
'custom_properties' => '{}',
134+
'generated_conversions' => '{}',
135+
'responsive_images' => '{}',
136+
]);
137+
$media->save();
138+
139+
// Mock the Gate to allow access
140+
Gate::shouldReceive('forUser')->andReturnSelf();
141+
Gate::shouldReceive('check')->andReturn(true);
142+
143+
$this->get('/test-media/view/'.$media->uuid)
144+
->assertSuccessful()
145+
->assertSee('Type: view');
146+
})->group('middleware');
147+
148+
it('handles download media type correctly', function () {
149+
// Set up test route with middleware
150+
Route::get('/test-media/{type}/{uuid}', function (Request $request, $type, $uuid) {
151+
// Check that the type is correctly passed
152+
return 'Type: '.$type;
153+
})->middleware(ValidateMediaAccess::class);
154+
155+
// Create a real user for testing
156+
$user = user();
157+
login($user);
158+
159+
// Create a real media record in the database
160+
$media = new \Spatie\MediaLibrary\MediaCollections\Models\Media([
161+
'model_type' => get_class($user),
162+
'model_id' => $user->id,
163+
'uuid' => 'test-uuid-download',
164+
'collection_name' => 'default',
165+
'name' => 'test-file',
166+
'file_name' => 'test.txt',
167+
'mime_type' => 'text/plain',
168+
'disk' => 'public',
169+
'conversions_disk' => 'public',
170+
'size' => 100,
171+
'manipulations' => '[]',
172+
'custom_properties' => '{}',
173+
'generated_conversions' => '{}',
174+
'responsive_images' => '{}',
175+
]);
176+
$media->save();
177+
178+
// Mock the Gate to allow access
179+
Gate::shouldReceive('forUser')->andReturnSelf();
180+
Gate::shouldReceive('check')->andReturn(true);
181+
182+
$this->get('/test-media/download/'.$media->uuid)
183+
->assertSuccessful()
184+
->assertSee('Type: download');
185+
})->group('middleware');
186+
187+
it('handles stream media type correctly', function () {
188+
// Set up test route with middleware
189+
Route::get('/test-media/{type}/{uuid}', function (Request $request, $type, $uuid) {
190+
// Check that the type is correctly passed
191+
return 'Type: '.$type;
192+
})->middleware(ValidateMediaAccess::class);
193+
194+
// Create a real user for testing
195+
$user = user();
196+
login($user);
197+
198+
// Create a real media record in the database
199+
$media = new \Spatie\MediaLibrary\MediaCollections\Models\Media([
200+
'model_type' => get_class($user),
201+
'model_id' => $user->id,
202+
'uuid' => 'test-uuid-stream',
203+
'collection_name' => 'default',
204+
'name' => 'test-file',
205+
'file_name' => 'test.txt',
206+
'mime_type' => 'text/plain',
207+
'disk' => 'public',
208+
'conversions_disk' => 'public',
209+
'size' => 100,
210+
'manipulations' => '[]',
211+
'custom_properties' => '{}',
212+
'generated_conversions' => '{}',
213+
'responsive_images' => '{}',
214+
]);
215+
$media->save();
216+
217+
// Mock the Gate to allow access
218+
Gate::shouldReceive('forUser')->andReturnSelf();
219+
Gate::shouldReceive('check')->andReturn(true);
220+
221+
$this->get('/test-media/stream/'.$media->uuid)
222+
->assertSuccessful()
223+
->assertSee('Type: stream');
224+
})->group('middleware');

0 commit comments

Comments
 (0)