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 permissions
  • PhotoPolicy.php - Photo-specific permissions
  • UserPolicy.php - User management permissions
  • UserGroupPolicy.php - User group permissions
  • SettingsPolicy.php - Application settings permissions
  • MetricsPolicy.php - Metrics and analytics permissions
  • BasePolicy.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 handle null values 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 visibility
  • PhotoQueryPolicy.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:

  1. Admin Override: Admins have full access to everything
  2. Ownership: Album owners have full control
  3. Shared Permissions: Based on AccessPermission records
  4. Password Protection: Albums can require passwords
  5. Public Access: Based on AccessPermission records with a null user_id
    • Direct Link Only: When is_link_required is true, the album is public but not visible in listings - only accessible via direct link
    • Full Public: When is_link_required is false, the album is fully public, visible in listings, and browsable

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:

  1. Album-based Inheritance: Photo permissions often inherit from parent album
  2. Direct Ownership: Photo owners have control regardless of album
  3. Shared Album Access: Photos in shared albums follow album permissions
  4. 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:

  1. Visible: Album appears in listings and trees (basic visibility), but might be protected by a password.
  2. Reachable: Album can be accessed via direct link, but requires parent albums to be accessible (not private or blocked by is_link_required=true).
  3. 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_permissions table
  • 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

  1. Controller Level: Regular policies check specific actions
  2. Query Level: Query policies filter data at source
  3. Relationship Level: Automatic filtering in model relationships
  4. 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