• Mahmoud Aglan's avatar
    fix(cache): stop refusing to restore the objects we cache on purpose · 905ffefc
    Mahmoud Aglan authored
    Every branded page on the live tenant answered 500 with
    "BrandingService::for(): Return value must be of type BrandProfile,
    __PHP_Incomplete_Class returned".
    
    config/cache.php shipped Laravel's default serializable_classes => false,
    which unserializes cache values with allowed_classes: false. That is safe for
    an app that caches only scalars and arrays, and fatal for one that does not.
    We cache whole value objects deliberately: BrandProfile is the entire tenant
    brand, resolved once and held until branding changes, and read by every admin,
    portal and print layout. With classes refused it came back as
    __PHP_Incomplete_Class, the return type threw, and the admin went dark.
    
    The setting exists to stop a gadget chain in a cache an attacker can already
    write to. Ours is the tenant's own Postgres, reachable only by the app;
    anyone who can write there can do worse directly. CACHE_SERIALIZABLE_CLASSES
    lets a deployment pass its own allowlist without a code change.
    
    BrandingService now also checks what the cache handed back before trusting it,
    and rebuilds when it is not a profile. A cache that cannot return this class
    should cost a rebuild per request, never a 500 — the failure has to degrade,
    not detonate.
    
    Verified: every admin screen and the group roster render against a restored
    copy of the live tenant with CACHE_STORE=database.
    Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
    905ffefc