Lychee Policies Documentation
August 14, 2025 · View on GitHub
This document explains the authorization and access control system in Lychee, focusing on the distinction between regular Policies and Query Policies, and their roles in securing the application.
Overview
Lychee implements a comprehensive authorization system using Laravel's Policy classes with custom extensions. The system is designed to handle both individual model authorization and complex query-level filtering for security and performance.
Policy Types
Regular Policies (Authorization Policies)
Regular policies handle individual model authorization and define what actions a user can perform on specific resources. These policies work at the model instance level.
Files:
AlbumPolicy.php- Album-specific permissionsPhotoPolicy.php- Photo-specific permissionsUserPolicy.php- User management permissionsUserGroupPolicy.php- User group permissionsSettingsPolicy.php- Application settings permissionsMetricsPolicy.php- Metrics and analytics permissionsBasePolicy.php- Base class with admin override logic
Key Characteristics:
- Instance-based: Work with specific model instances (e.g., "Can user X edit album Y?")
- Action-oriented: Define specific actions (view, edit, delete, upload, share)
- Permission checking: Used by controllers to verify if a user can perform an action
- Gate integration: Work with Laravel's Gate system for authorization
Example Authorization Methods:
// AlbumPolicy.php
public function canEdit(User $user, AbstractAlbum|null $album): bool
{
return $this->isOwner($user, $album) ||
$this->hasEditPermission($user, $album);
}
public function canUpload(?User $user, AbstractAlbum|null $album): bool
{
return $this->isOwner($user, $album) ||
$album->current_user_permissions()?->grants_upload === true ||
$album->public_permissions()?->grants_upload === true;
}
⚠️ Important: User Type Implications
Pay careful attention to the difference between User? (nullable) and User (non-nullable) in policy method signatures:
?User $user: Allows both authenticated and guest users. The method must handlenullvalues for unauthenticated requests.User $user: Requires an authenticated user. Laravel will automatically deny access if no user is authenticated.
// Allows guests - must handle null user
public function canSee(?User $user, Album $album): bool
{
if ($user === null) {
// Handle guest user logic
return $album->is_public && !$album->requires_link;
}
// Handle authenticated user logic
return $this->isOwner($user, $album) || $album->is_public;
}
// Requires authentication - Laravel denies if user is null
public function canEdit(User $user, Album $album): bool
{
// $user is guaranteed to be non-null here
return $this->isOwner($user, $album);
}
Admin Override via before() Method:
All policies inherit from BasePolicy which implements a before() method that runs before any specific policy check:
// BasePolicy.php
public function before(?User $user, $ability)
{
if ($user?->may_administrate === true) {
return true; // Admin bypass - skips all other checks
}
// Returns null/void - continues to specific policy method
}
This means admin users automatically pass all policy checks without executing the specific permission logic, providing a global override for administrative access.
Admin-Only Actions:
As a consequence of the admin override, a policy method can return false for all regular users while still being valid due to the admin bypass. This pattern effectively creates admin-only actions:
// app/Policies/SettingsPolicy.php
public function canEdit(User $user): bool
{
// Another admin-only action
// Regular users: false, Admins: true (via before() override)
return false;
}
This design pattern allows for clean separation between actions available to regular users and those restricted to administrators only.
Query Policies (Query Filtering Policies)
Query policies handle database query filtering and define what data a user can see when querying collections. These policies work at the query builder level.
Files:
AlbumQueryPolicy.php- Album query filtering and visibilityPhotoQueryPolicy.php- Photo query filtering and visibility
Key Characteristics:
- Query-based: Work with query builders to filter results before execution
- Collection filtering: Define what records a user can see in listings
- Performance-oriented: Apply filters at database level for efficiency
- Visibility control: Implement complex visibility rules (public, shared, private)
- Hierarchical awareness: Handle nested album structures and inheritance
Key Concepts:
- Visibility: What albums/photos appear in listings
- Accessibility: What albums/photos a user can access directly
- Reachability: What albums can be reached through navigation
- Browsability: What albums can be found by "clicking around"
- Searchability: What photos appear in search results
Regular Policies Deep Dive
AlbumPolicy
The AlbumPolicy class manages permissions for album operations:
Core Permission Methods:
const IS_OWNER = 'isOwner';
const CAN_SEE = 'canSee';
const CAN_ACCESS = 'canAccess';
const CAN_ACCESS_FULL_PHOTO = 'canAccessFullPhoto';
const CAN_DOWNLOAD = 'canDownload';
const CAN_DELETE = 'canDelete';
const CAN_UPLOAD = 'canUpload';
const CAN_EDIT = 'canEdit';
const CAN_SHARE = 'canShare';
Permission Hierarchy:
- Admin Override: Admins have full access to everything
- Ownership: Album owners have full control
- Shared Permissions: Based on
AccessPermissionrecords - Password Protection: Albums can require passwords
- Public Access: Based on
AccessPermissionrecords with a nulluser_id- Direct Link Only: When
is_link_requiredis true, the album is public but not visible in listings - only accessible via direct link - Full Public: When
is_link_requiredis false, the album is fully public, visible in listings, and browsable
- Direct Link Only: When
Session Management:
- Unlocked albums are tracked in user sessions
- Password-protected albums remain unlocked during session
- Session key:
AlbumPolicy::UNLOCKED_ALBUMS_SESSION_KEY
PhotoPolicy
The PhotoPolicy class manages permissions for individual photos:
Core Permission Methods:
const CAN_SEE = 'canSee';
const CAN_DOWNLOAD = 'canDownload';
const CAN_EDIT = 'canEdit';
const CAN_ACCESS_FULL_PHOTO = 'canAccessFullPhoto';
const CAN_DELETE_BY_ID = 'canDeleteById';
Permission Logic:
- Album-based Inheritance: Photo permissions often inherit from parent album
- Direct Ownership: Photo owners have control regardless of album
- Shared Album Access: Photos in shared albums follow album permissions
- Public Photos: Photos in public albums are publicly accessible
BasePolicy
The BasePolicy provides common functionality:
Admin Override Logic:
public function before(?User $user, $ability)
{
if ($user?->may_administrate === true) {
return true;
}
}
All policies extend BasePolicy to inherit admin privilege override.
Query Policies Deep Dive
AlbumQueryPolicy
The AlbumQueryPolicy manages album query filtering with sophisticated visibility rules:
Core Filtering Methods
1. Visibility Filtering (applyVisibilityFilter)
- Determines which albums appear in listings
- Considers: ownership, sharing, public access, link requirements
- Used for: album trees, navigation menus
public function applyVisibilityFilter(AlbumBuilder $query): AlbumBuilder
{
// Admin users see everything
if (Auth::user()?->may_administrate === true) {
return $query;
}
// Apply visibility conditions based on:
// - User ownership
// - Shared permissions
// - Public access settings
// - Link requirements
}
2. Reachability Filtering (applyReachabilityFilter)
- Determines which albums can be directly accessed
- Combines visibility and accessibility rules
- Used for: direct album access validation
3. Browsability Filtering (applyBrowsabilityFilter)
- Determines which albums can be reached through navigation
- Ensures entire path from root to album is accessible
- Used for: preventing access to orphaned albums
Key Concepts Explained
The Three Levels of Album Access:
- Visible: Album appears in listings and trees (basic visibility), but might be protected by a password.
- Reachable: Album can be accessed via direct link, but requires parent albums to be accessible (not private or blocked by is_link_required=true).
- Browsable: Album can be navigated to through click-through from parent albums (requires entire path to be accessible and visible).
Important Distinction:
- Reachability depends on the album's own permissions AND its parent chain accessibility
- Browsability depends on the entire path from origin being both accessible AND visible (not blocked by private albums or is_link_required=true)
┌─ Album A (Private) ───────────────────────────────────────┐
│ ┌─ Album B (Public, is_link_required = false) ───────────┐│
│ │ ┌─ Album C (Public, is_link_required = false ) ───────┐││
│ │ │ ┌─ Album D (Public, is_link_required = false ) ────┐│││
│ │ │ │ ┌─ Album E (Public, is_link_required = true ) ──┐││││
│ │ │ │ └───────────────────────────────────────────────┘││││
│ │ │ └──────────────────────────────────────────────────┘│││
│ │ └─────────────────────────────────────────────────────┘││
│ └────────────────────────────────────────────────────────┘│
└───────────────────────────────────────────────────────────┘
Access Analysis:
Via Direct Link (Anonymous User with specific URLs):
- Album A: ❌ Not accessible (private album, user is not owner)
- Album B: ✅ Accessible (public album, direct link bypasses private parent A)
- Album C: ✅ Accessible (public album, direct link bypasses private parent A)
- Album D: ✅ Accessible (public album, direct link bypasses private parent A)
- Album E: ✅ Accessible (public album with is_link_required=true, accessible only via direct link)
From Origin (Anonymous User Perspective):
- Album A: ❌ Not visible, ❌ not reachable, ❌ not browsable (private)
- Album B: ❌ Not visible, ❌ not reachable, ❌ not browsable (parent A is private, blocks all access)
- Album C: ❌ Not visible, ❌ not reachable, ❌ not browsable (path blocked by private A)
- Album D: ❌ Not visible, ❌ not reachable, ❌ not browsable (path blocked by private A)
- Album E: ❌ Not visible, ❌ not reachable, ❌ not browsable (path blocked by private A)
From Album B Perspective (if user has been given direct link to B):
- Album A: ❌ Not visible, ❌ not reachable, ❌ not browsable (private, user is not owner)
- Album B: ✅ Visible (from A), ✅ reachable, ❌ not browsable (current location, accessible via direct link)
- Album C: ✅ Visible (from B), ✅ reachable (from B), ❌ not browsable (public child, parent B is accessible)
- Album D: ✅ Visible (from C), ✅ reachable (from B), ❌ not browsable (public child, parent B is accessible)
- Album E: ❌ Not visible (from D), ❌ not reachable, ❌ not browsable (is_link_required=true makes parent C not accessible, breaking the chain)
Key Insight: Parent Chain Dependency
Reachability requires the entire parent chain to be accessible. If any parent has is_link_required=true or is private (and user lacks access), it breaks reachability for all descendants, even if those descendants are public.
Access Level Hierarchy:
Browsable ⊆ Reachable ⊆ Visible
- If an album is browsable, it must also be reachable and visible
- If an album is reachable, it must also be visible
- An album can be visible but not reachable (e.g., link-required albums)
- An album can be reachable but not browsable (e.g., child of private parent)
Computed Access Permissions:
- Dynamic JOIN with
access_permissionstable - Aggregates user, user group, and public permissions
- Optimizes permission checking at database level
private function getComputedAccessPermissionSubQuery(): BaseBuilder
{
// Creates subquery that computes effective permissions
// by combining user permissions, group permissions, and public settings
}
PhotoQueryPolicy
The PhotoQueryPolicy manages photo query filtering with album-aware logic:
Core Filtering Methods
1. Visibility Filtering (applyVisibilityFilter)
- Determines which photos appear in listings
- Considers: photo ownership, album accessibility, public access
- Used for: photo galleries, recent photos
public function applyVisibilityFilter(FixedQueryBuilder $query): FixedQueryBuilder
{
// Admin users see everything
if (Auth::user()?->may_administrate === true) {
return $query;
}
// Apply visibility based on:
// - Direct photo ownership
// - Album accessibility (via AlbumQueryPolicy)
// - Public photo settings
}
2. Searchability Filtering (applySearchabilityFilter)
- Determines which photos appear in search results
- Restricts search scope to accessible albums
- Handles album hierarchy constraints
public function applySearchabilityFilter(
FixedQueryBuilder $query,
?Album $origin = null,
bool $include_nsfw = true
): FixedQueryBuilder
3. Sensitivity Filtering (applySensitivityFilter)
- Filters out photos in sensitive albums
- Respects user preferences and album settings
- Handles recursive sensitivity (sensitive parent albums)
Advanced Features
Album Hierarchy Integration:
// Photos must be in albums that form an unbroken accessible path
$query->whereNotExists(function (BaseBuilder $q) {
$this->album_query_policy->appendUnreachableAlbumsCondition($q);
});
Root Level Photos:
- Special handling for photos not in albums (root level)
- Different permission rules apply
- Owner-only or public access based on configuration
Performance Optimizations:
private function prepareModelQueryOrFail(
FixedQueryBuilder $query,
bool $add_albums,
bool $add_base_albums
): void
{
// Automatically joins necessary tables
// - photo_albums (for album relationships)
// - albums (for hierarchy data)
// - base_albums (for album metadata)
// - computed_access_permissions (for permission data)
}
Usage Patterns
In Controllers
Regular Policies:
// Check if user can edit specific album
Gate::authorize(AlbumPolicy::CAN_EDIT, $album);
// Check if user can upload to album
if (Gate::denies(AlbumPolicy::CAN_UPLOAD, $album)) {
throw new UnauthorizedException();
}
Query Policies:
// Get albums visible to current user
$albums = Album::query()
->when(true, fn($q) => $album_query_policy->applyVisibilityFilter($q))
->get();
// Search photos with restrictions
$photos = Photo::query()
->when(true, fn($q) => $photo_query_policy->applySearchabilityFilter($q, $origin))
->where('title', 'like', "%{$search}%")
->get();
In Relationships
Query policies are automatically applied in custom relationship classes:
// HasManyChildAlbums.php
public function addConstraints()
{
if (static::$constraints) {
parent::addConstraints();
$this->album_query_policy->applyVisibilityFilter($this->getRelationQuery());
}
}
Security Considerations
Defense in Depth
- Controller Level: Regular policies check specific actions
- Query Level: Query policies filter data at source
- Relationship Level: Automatic filtering in model relationships
- Frontend Level: UI hides unauthorized options
Performance Impact
Query Policies Benefits:
- Filter data at database level (efficient)
- Reduce memory usage by not loading unauthorized data
- Enable complex permission logic in single queries
Potential Concerns:
- Complex JOIN operations for permission checking
- Nested subqueries for hierarchy validation
- Can impact query performance with large datasets
Last updated: August 14, 2025