← Laracrate 13 / 17

Access control

Access control

Access control and authorization

Every collection declares an access mode that controls how its files reach the browser. The mode lives in the collection config (see the Configuration section) and is stored on each file as the access column, backed by the EduLazaro\Laracrate\Enums\FileAccess enum. When you call $file->url(), Laracrate resolves the right strategy for you, you never build storage URLs by hand.

The three modes are:

Mode Enum case How url() resolves it Audit Use for
public FileAccess::PUBLIC Storage::disk()->url(), direct CDN link, no signature No Avatars, logos, public marketing assets
signed FileAccess::SIGNED Storage::disk()->temporaryUrl(), short-lived presigned GET, cached server-side No Private-ish files where a temporary direct link is fine
stream FileAccess::STREAM Signed Laravel route to the package stream controller, the binary is proxied through your app Yes Sensitive content, anything that needs per-request checks

$file->url() delegates to StorageManager::urlFor(), which switches on the access value: public calls GeneratePublicUrlAction, stream calls GenerateSensitiveStreamUrlAction, and everything else falls back to GenerateSignedUrlAction. Signed URLs are cached server-side (config laracrate.urls.signed_cache_ttl, default 4 minutes) so a page rendering many files does not issue one presign per file. If a disk is misconfigured or unreachable, the signed action logs a warning and returns null instead of throwing, so one broken file does not break the whole page.

Declaring authorization rules

Authorization is declared in the PolicyRegistry (EduLazaro\Laracrate\Support\PolicyRegistry), keyed by the fileable morph alias (user, case, property, and so on). Register your closures from a service provider boot() method:

use EduLazaro\Laracrate\Support\PolicyRegistry;
use EduLazaro\Laracrate\Models\File;
use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    app(PolicyRegistry::class)
        ->viewable('case', fn (File $file, ?Model $user) => $user?->canAccessCase($file->fileable))
        ->editable('case', fn (File $file, ?Model $user) => $user?->isLawyer())
        ->deletable('case', fn (File $file, ?Model $user) => $user?->isAdmin());
}

Each registrar (viewable, editable, deletable) returns $this, so calls chain. The closure receives the File and the current user (which may be null for guests) and returns a boolean.

At decision time, three methods evaluate the rules: canView(), canEdit(), and canDelete(). They are exposed on the registry and mirrored on the model, so $file->canView($user), $file->canEdit($user), and $file->canDelete($user) all work in blade and controllers.

Default policy: deny with exceptions

If no closure is registered for a fileable type, Laracrate applies safe defaults driven by the registry logic:

  • The human creator can always view, edit, and delete their own file. A file counts as creator-owned when creator_type is user and creator_id matches the current user's key.
  • Public files are always viewable by anyone (canView returns true when access is public), regardless of registered closures.
  • For everything else, when no closure exists the answer is false. The system is closed by default: you opt files in, you never accidentally leak them.

So a registered viewable closure only runs for non-creators viewing a non-public file. Edit and delete have no public exception, only the creator shortcut and your closures.

The Gate bridge

By default the package binds EduLazaro\Laracrate\Policies\FilePolicy to Laravel's Gate, so you get native ergonomics on top of the registry. This is controlled by laracrate.policies.register_gate (default true):

@can('view', $file)
    <a href="{{ $file->url() }}">Open</a>
@endcan
// In a controller
$this->authorize('update', $file);

// As route middleware
Route::get('/files/{file}', ...)->middleware('can:view,file');

The bridge maps Laravel's canonical abilities to the registry methods: Gate view calls canView, update calls canEdit, and delete calls canDelete. If your app already registers its own File policy, or you do not want the bridge, set register_gate to false and call $file->canView() (and friends) directly.

Sensitive content and encryption

For files that must never be served by a direct storage URL, set the collection's access mode to stream and mark it sensitive. Streamed files are proxied through EduLazaro\Laracrate\Http\Controllers\StreamFileController, which re-checks permissions on every request, audits the access, and (when the collection is encrypted) decrypts the binary in memory before sending it. The storage backend URL is never exposed to the client.

The per-request stream flow

When access is stream, $file->url() returns a temporary signed route built by GenerateSensitiveStreamUrlAction. The controller exposes stream, preview, download, and link actions, each routed under the laracrate.files.* names (laracrate.files.stream, laracrate.files.preview, laracrate.files.download). Every request runs through validateAccess() before a single byte is sent:

  1. Signature check. The route must carry a valid Laravel signature (hasValidSignature()), otherwise the controller aborts 403. URLs are signed with a TTL from laracrate.urls.route_signed_ttl (default 15 minutes).
  2. Viewer bind (sensitive only). If $file->isSensitive() and laracrate.urls.bind_to_user is on (default true), the request must be authenticated, and the u query parameter (the user id baked into the URL when it was generated) must match the current Auth::id(). A leaked URL pasted into another session aborts 403. Generation only adds u when a user is logged in, see GenerateSensitiveStreamUrlAction.
  3. Policy check. $file->canView($request->user()) runs the same PolicyRegistry logic described in the Access control and authorization section. Failure aborts 403.

Only after all three pass does the controller serve the file. The signed route in your HTML is what makes sensitive links safe to render: even if the page is cached, the link expires on its own and is bound to the viewer.

Audit and download tracking

sendFile() audits before streaming. When the request is a stream or download (not preview) and laracrate.stream.increment_downloads is on (default true), the file's downloads_count is incremented and last_downloaded_at is stamped with a quiet save (no events fired). When laracrate.stream.log_access is on (default true), an info log records the file id, collection, user id, IP, and method. Streamed responses are sent with Cache-Control: private, no-store, no-cache, must-revalidate so sensitive bytes are never cached downstream.

Encryption at rest

Set encrypt => true on a collection to encrypt the binary before it is written to the backend. CreateFileAction reads the flag, runs EncryptFileAction on the binary, and persists is_encrypted = true on the file. Encryption uses Laravel's app key:

  • EncryptFileAction does Crypt::encryptString(base64_encode($binary)).
  • DecryptFileAction reads the cipher from the backend and returns base64_decode(Crypt::decryptString($cipher)).

Encryption requires the binary to be present server-side. If a collection has encrypt => true but the upload skipped your server (a direct presigned upload, for example), CreateFileAction throws, because there is nothing for PHP to encrypt. Encrypted collections must route the bytes through your app on the way in.

On the way out, the stream controller transparently decrypts: in sendFile(), when $file->is_encrypted is true it calls DecryptFileAction::create()->run(['file' => $file]), otherwise it reads the raw object. Encryption pairs naturally with access => stream, since the binary has to pass through your app to be decrypted anyway.

Watermarks are baked in, not applied at stream time

Watermarking is not part of the stream flow. The watermark is rendered into the binary of the relevant variant when that variant is generated by the processing pipeline (see the Images, variants and watermarks section), not on each request. The stream controller only validates, decrypts, and serves: it never re-renders pixels. This keeps streaming cheap and makes the watermarked bytes the only bytes that exist for that variant.