1

My symfony 5.4 project uses aws_s3 + flysystem + liip_imagine.

In aws_s3, I have a PRIVATE bucket: "myBucket" with 3 subfolders :

  • documents
  • photos
  • media

And IAM PERMISSION

    "Effect": "Allow",
        "Action": [
            "s3:ListBucket",
            "s3:GetObject",
            "s3:DeleteObject",
            "s3:GetObjectAcl",
            "s3:PutObjectAcl",
            "s3:PutObject"
        ],
        "Resource": [
            "arn:aws:s3:::myBucket",
            "arn:aws:s3:::mybucket/*"
        ]

My setups : see : https://github.com/liip/LiipImagineBundle/issues/823

Service.yaml

...
parameters:
    uploads_base_url: '%env(AWS_S3_UPLOAD_BASE_URL)%'   

services : 
    Aws\S3\S3Client:
        arguments:
            -
                version: '2006-03-01'
                region: '%env(AWS_S3_ACCESS_REGION)%' 
                credentials:
                    key: '%env(AWS_S3_ACCESS_ID)%'
                    secret: '%env(AWS_S3_ACCESS_SECRET)%'

oneup_flysystem.yaml

...
adapters:
    aws_s3_adapter:                    
        awss3v3:
            client : Aws\S3\S3Client
            bucket: '%env(AWS_S3_BUCKET_NAME)%'
            options:
                ACL: bucket-owner-full-control

filesystems:
    aws_s3_system:
        adapter: aws_s3_adapter

lipp_imagine.yaml

# https://symfony.com/bundles/LiipImagineBundle/current/cache-resolver/aws_s3.html
# I do not understand everything
...
    driver: "gd"

    loaders:
        aws_s3_loader:  
            flysystem:
                filesystem_service: oneup_flysystem.aws_s3_system_filesystem

    data_loader: aws_s3_loader

    resolvers:
        aws_s3_resolver:
            flysystem:
                filesystem_service: oneup_flysystem.aws_s3_system_filesystem
                root_url: '%uploads_base_url%'
                cache_prefix: media/cache
     
    cache: aws_s3_resolver 

    filter_sets:
        squared_thumbnail_small:
            quality: 70
            filters:
                thumbnail:
                    size:          [50, 50]
                    mode:          outbound

twig

# call twig_function assetPresigned
<a href="{{ assetPresigned('photos', player.photoFilename) }}" target="_blank">
    <img src="{{ assetPresigned('photos', player.photoFilename ) | imagine_filter('squared_thumbnail_small') }}" alt="photo">
</a>

FileUploadService.php


    public function assetPresigned(string $folder, string $filename): string
    {
        $command = $this->s3Client->getCommand('GetObject', [
            'Bucket' => $this->awsS3BucketName,
            'Key'    => $folder.'/'.$filename,

        ]);

        // RETURN PRESIGNED URL
        $urlPresigned = $this->s3Client->createPresignedRequest($command, '+5 minutes');
        return ((string) $urlPresigned->getUri());
    }

pb 1 : My problem is that the "squared_thumbnail_small" filter rewrites the url removing the pre-signed signature

results in twig :

  • href: the image appears on the click because url is pre-signed
  • img: url loses its signature and therefore is not displayed

nb: it is liip_imagine via "imagine_filter('squared_thumbnail_small) which creates the thumbnail in mybucket/media. At this stage, the thumbnail does not yet appear in mybucket/media because because it has not yet been displayed

question : How to properly configure my code so that the filter does not remove the presigned signature ?

Here is what I tried

FileUploadService.php -

namespace App\Service;
...

 public function assetPresigned(string $folder, string $filename): string
    {
       # call ImagineFilterService.php 

       $this->imagineFilter->filter($folder.'/'.$filename);

        # ...
        $command....
    }

ImagineFilterService.php

namespace App\Service;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use Liip\ImagineBundle\Imagine\Data\DataManager;
use Liip\ImagineBundle\Imagine\Filter\FilterManager;

use App\Service\FileUploadService;

# https://symfony.com/bundles/LiipImagineBundle/current/filters.html#dynamic-filters
# I tried to create a filter dynamically based on this documentation

Class ImagineFilterService

public function filter($path) 
{
    $filter = 'squared_thumbnail_small';                 

    # if photo is not stored in myBucket/media then you save it by applying the filter?
    if (!$this->cacheManager->isStored($path, $filter)) {
        $binary = $this->dataManager->find($filter, $path);
        $filteredBinary = $this->filterManager->applyFilter($binary, $filter);

        # In this method, I pass in parameter the resolver
        $this->cacheManager->store($filteredBinary, $path, $filter, 'aws_s3_resolver');           
    }
    // ERROR 403!!!

    return $this->cacheManager->resolve($path, $filter);
}


I now recover the thumbnails that were already stored in myBucket/media (ok)

But for saving new thumbnails in myBucket/media I got a 403 error.

AWS HTTP error: Client error: PUT resulted in a 403 Forbidden` response: Access Denied

So I lose the credentials.

This is complex for me and a precise answer (see a piece of code) would help me a lot. I don't know where I'm wrong. I've been stuck for several days

Thanks for your help.

brico
  • 61
  • 6

1 Answers1

0

Hey I found a solution to keep the signed url. I don't know if we could do it more simply but it works.

Packages

  • "liip/imagine-bundle": "^2.10"
  • "league/flysystem-aws-s3-v3": "^3.13",
  • "league/flysystem-bundle": "^3.1",
  • "oneup/flysystem-bundle": "^4.6"

example of S3Client

//service.yaml

Aws\S3\S3Client:
    arguments:
        -   endpoint: '%env(IONOS_S3_ENDPOINT)%'
            version: '%env(IONOS_S3_VERSION)%'
            region: '%env(IONOS_S3_REGION)%'
            credentials:
                key: '%env(IONOS_S3_ACCESS_ID)%'
                secret: '%env(IONOS_S3_ACCESS_SECRET)%'

I have an endpoint argument because i'm using ionos as s3.

Configure your adapter and his filesystem where will be load your images.

    //oneup_flysystem.yaml

oneup_flysystem:
    adapters:
        user_adapter:
            awss3v3:
                client: Aws\S3\S3Client
                bucket: '%env(IONOS_S3_BUCKET_NAME)%'
                prefix: "users"
        user_thumbnail_adapter:
            awss3v3:
                client: Aws\S3\S3Client
                bucket: '%env(IONOS_S3_BUCKET_NAME)%'
                prefix: "cache/user_thumbnail"
        user_medium_adapter:
            awss3v3:
                client: Aws\S3\S3Client
                bucket: '%env(IONOS_S3_BUCKET_NAME)%'
                prefix: "cache/user_medium"

    filesystems:
        user:
            adapter: user_adapter
            mount: user
        userThumbnail:
            adapter: user_thumbnail_adapter
            mount: userThumbnail
        userMedium:
            adapter: user_medium_adapter
            mount: userMedium

I have my user filesystem link with user_adapter to load my images in this folder of my bucket.

The others (as userThumbnail and userMedium) will be for the caches images generated automatically by the filters of liip_imagine. I only configured them for the next step when I will need to get the cropped images.

Configure liip_imagine

liip_imagine:
driver: "gd"
loaders:
    user_loader:
        flysystem:
            filesystem_service: oneup_flysystem.user_filesystem

resolvers:
    aws_s3_resolver:
        aws_s3:
            bucket: '%env(IONOS_S3_BUCKET_NAME)%'
            client_config:
                credentials:
                    key: '%env(IONOS_S3_ACCESS_ID)%'
                    secret: '%env(IONOS_S3_ACCESS_SECRET)%'
                endpoint: '%env(IONOS_S3_ENDPOINT)%'
                region: '%env(IONOS_S3_REGION)%'
                version: '%env(IONOS_S3_VERSION)%'
            acl: private
            cache_prefix: cache

            get_options:
                Scheme: 'https'
            put_options:
                CacheControl: 'max-age=86400'
cache: aws_s3_resolver
filter_sets:
    cache: ~
    user_thumbnail:
        cache: aws_s3_resolver
        quality: 75
        filters:
            thumbnail: { size: [ 130, 130 ], mode: outbound }
        data_loader: user_loader
    user_medium:
        cache: aws_s3_resolver
        quality: 75
        filters:
            thumbnail: { size: [ 302, 180 ], mode: outbound }
        data_loader: user_loader

To get my files as private in the cached folders, I configure the acl at private for the aws_s3_resolver

Now, when I want to go to the url generated by liip_imagine I get a deny access. To finally get the resource, I need to change the redirection made by liip_imagine with a signed url. So I created a subscriber to catch the post resolve event.

Configure a subscriber with event liip_imagine.post_resolve

namespace App\EventSubscriber;

use App\Entity\Media;
use App\Enum\MediaFilterEnum;
use App\Repository\MediaRepository;
use League\Flysystem\FilesystemOperator;
use Liip\ImagineBundle\Events\CacheResolveEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class LiipImagineFilterSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private readonly MediaRepository $mediaRepository,
        private readonly FilesystemOperator $userThumbnailFilesystem,
        private readonly FilesystemOperator $userMediumFilesystem
    )
    {
    }

    public function onPostResolve(CacheResolveEvent $event): void
    {
        $media = $this->mediaRepository->findOneBy(['photo' => $event->getPath()]);
        $filter = $event->getFilter();

        if ($media instanceof Media) {

            $date = new \DateTime();
            $date = $date->add(new \DateInterval('PT10M'));

            if ($filter === MediaFilterEnum::USER_THUMBNAIL->value) {
                    $path = $this->userThumbnailFilesystem->temporaryUrl($media->getPhoto(), $date);
            }
            else if ($filter === MediaFilterEnum::USER_MEDIUM->value) {
                    $path = $this->userMediumFilesystem->temporaryUrl($media->getPhoto(), $date);
            }

            if (isset($path)) {
                $event->setUrl($path);
            }
        }

    }

    public static function getSubscribedEvents(): array
    {
        return [
            'liip_imagine.post_resolve' => 'onPostResolve'
        ];
    }
}