If a file is marked as having a private ACL, we should optionally generate a presigned URL with a configurable expiry.
While we reasonably limit ACL configurations to be at the wrapper level, S3 does support per-file ACLs. Instead of just relying on the wrapper configuration, we should emit presigned URLs based on the key specific ACLs, assuming presigned URLs are configured.
| Comment | File | Size | Author |
|---|---|---|---|
| #59 | interdiff-58-59.patch | 1.4 KB | leon kessler |
| #59 | flysystem_2772847-59-presigned-urls.patch | 14.28 KB | leon kessler |
| #56 | 2772847-56-private-files-image-styles.patch | 31.22 KB | leon kessler |
| #50 | 2772847-50-private-files-image-styles.patch | 30.46 KB | hkirsman |
| #49 | interdiff-1.1dH63D.rej_.txt | 3.84 KB | hkirsman |
Issue fork flysystem_s3-2772847
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
deviantintegral commentedHere's a first pass. This ended up being more complex than just supporting presigned URLs in getExternalUrl(), due to core chmod'ing files to public-read. If this looks good, I can write a unit test around chmod().
Comment #3
deviantintegral commentedWe need to add cache tags to the response based on the external URL expiry. Otherwise, core caches it 'forever'. It's easy to see if you set a short expiry and then reload after the url has expired.
Comment #4
twistor commentedWow this is quite a patch! Seems like Drupal is really fighting us here.
I think we're commenting out optional configuration.
I did not know about decorates. Nifty.
Are we supposed to inject Settings? I've done it in the past, but I don't think there's any value. Also, it seems like an easy way to leak keys during exceptions. (That's a separate problem, but sightly related.)
Looks like it :) We might want to re-examine
Drupal\flysystem_s3\Flysystem\Adapter\S3Adapter::getMetadata()and how I've hacked on directory support.Comment #5
deviantintegral commentedOptional config is now commented, and I removed the if() I was questioning.
\Drupal\Core\File\FileSystem actually injects Settings, so I was following it's lead. What have you seen re key disclosure?
What are you envisioning for getMetadata()? Or do you see that as a new issue?
Comment #6
deviantintegral commentedOh, and I totally didn't mention this ran into #2352009: Bubbling of elements' max-age to the page's headers and the page cache (comments in the code though). We could enable smarting caching, but I don't think it's worth the core patch given that I'd see most presigned usage as being managed by calling code or for authenticated traffic that skips the page cache anyways.
Comment #7
deviantintegral commentedThis PR adds test coverage, and fixes a bug if the page cache module was disabled. As well, this includes the smaller patch from #2774877: testCreate() should only test create() and not getExternalUrl() which I filed as a separate patch for easier review. If there's anything holding that one up we should mark this issue as postponed.
Comment #9
deviantintegral commentedJust checking to make sure this isn't transient.
Comment #11
deviantintegral commented@group annotations attack again!
Comment #12
deviantintegral commentedComment #13
deviantintegral commentedThis patch includes #2695695: Return a generated URL instead of uploading an image style for projects that need both and can be ignored. Note that none of these patches support image styles on a private bucket.
Comment #14
deviantintegral commentedHere's a reroll against dev.
Comment #15
twistor commentedWhat's up with this?
Would it make sense to move this to the Flysystem module? Can we make it generic? Probably not.
This is looking pretty good. I've taken a break from D.O stuffs for a bit, but I'm back now. I will consider this shortly.
Comment #16
deviantintegral commentedHere's a reroll that:
I suppose it's possible to move much of the FileSystem class to the Flysystem module, but I'd worry about abstracting the wrong bits. How about we wait until another adapter needs something similar?
Comment #17
deviantintegral commentedAlso, I've put up sandboxes for both Flysystem and Flysystem S3 with a bunch of in-review patches merged in. There's a bunch of ugly tags from when I was figuring out what Satis supports for indexing, but you might find these useful for any sites needing S3 support and the patches are becoming a pain to maintain.
https://www.drupal.org/sandbox/deviantintegral/2793055
https://www.drupal.org/sandbox/deviantintegral/2793177
Comment #19
deviantintegral commentedForgot to pull my 8.x-1.x branch when making the diff.
Comment #20
tjferre commentedPatch in #18 works great... one issue I ran into was the itok token being added for image styles... it interferes with access to the image when using a private ACL. Addressed the issue by adding the following to my settings.php file, $config['image.settings']['suppress_itok_output'] = TRUE;
Comment #21
fathershawnI've applied the patch and am successfully managing files in a restricted bucket from Drupal! Based on the base flysystem README
I expected the url exposed to be a drupal private file type url but instead I see an http://s3.amazonaws.com/ - if that's not in the scope of this issue I'll open a new issue, but is this possible?
Comment #22
deviantintegral commentedGlad to hear it’s working for you!
The URLs generated should be signed so they can be downloaded by anyone who has them, but they aren’t “private” in the Drupal sense.
I suppose there could be a way to route the files through Drupal, so normal Drupal access controls could be applied. Or, check Drupal access controls, but then issue a very short expiry URL forcing subsequent requests back through Drupal.
What this is really telling me is that we should refrain from using “private” anywhere in this patch. “Signed” is probably a more accurate description.
Supporting private files in the Drupal sense is probably out of scope for this issue, just so we have a chance of actually finishing and committing this one!
Comment #23
matthew.perry commentedI've been using this patch successfully for a few days now; However, I've run into an issue while using this patch with jsonapi_file module.
While trying to post the files via jsonapi routes I am receiving a 500 - Internal Server Error
This is resolved if I update the render call in S3.php from
$this->renderer->render($build);to this:$this->renderer->renderPlain($build);Comment #24
jody lynn@matthew.perry take a look at #2883086: [PP-1] Port RequestHandler + ResourceResponseSubscriber improvements from REST module to JSON API
Comment #25
jody lynnThe use of $this->renderer->render($build) was causing a fatal error from \Drupal\Core\Render\Renderer.php:
"Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead."
I got this error by using hook_page_attachments to add a Facebook image metatag using file_create_url which then calls Drupal\flysystem_s3\Flysystem\S3->getExternalUrl(). I'm thinking the render system does not like having render called during hook_page_attachments.
I don't know what renderPlain and renderRoot are, but I switched the patch to use renderRoot like the error suggests and that resolves the problem.
Comment #26
fathershawnI was seeing a similar error:
So I applied the patch in #25, which resolved that error, but when attempting to render image content in a modal, a new error appeared:
Investigating an approach that fixes both.
Comment #27
jody lynnI've been having performance issues with this patch on pages with many images.
I tracked it down to the addition of $this->getAdapter()->getVisibility($target)['visibility'] added to getExternalUrl. This check causes an s3 request to run, which typically takes several tenths of a second. If you are showing 100 images on a page and each one has a responsive srcset of 3 images, that really adds up.
As I'm mainly using public images I was able to work around it by first checking
$this->options['ACL'] != 'public-read'
before doing the getVisibility check.
Comment #28
fathershawn@jody-lynn we hit the same performance issue. To get our site launched in time, we switch from responsive image to a single optimized image for the image field on our view page, which with breakpoints and pixel density alternatives needed 180 image files for responsive. We also added code elsewhere to pre-populate the derivatives in S3 on save - which decreased our load time dramatically - by a factor of 15.
Our testing via blackfire showed over 6,800 curl calls to S3 on this page all tracing back to
. This seems like it's crying out for a caching strategy - which I'm happy to collaborate on - or any other ideas to speed up the performance for interaction with Drupal's image system. Until then we are limiting responsive images for S3 stored field.
Comment #29
fathershawnI adjusted this patch to use a call to
to avoid an actual request through
. See if this improves the performance:
In my own implementation I've completely commented out the code here to use render to add a max-age of 0. Instead I'm adding a max-age that matches my expire value in a
call.
Comment #30
jody lynnThanks Father Shawn! I had just found that file_exists in getExternalUrl as another place calling out to S3 as well so that patch will help a lot.
I added caching on my site around the responsive image style url generation, but every time the caches get cleared we'd be back to 15s page loads. At one point I added really aggressive 'caching' that just stored the generated responsive image URLs to a field to stay there forever, but this proved problematic each time we actually did want to adjust file sizes.
Comment #31
fathershawnWhich doesn't work for the same reason that is noted in the patch. I could even see caching the url as you say, but it needs an age limit for the private aspect to work. I don't see a way around the caching issue here until #2352009: Bubbling of elements' max-age to the page's headers and the page cache is fixed because the whole page gets cached.
My only other idea is that we build a facade path similar to private files and use that to reduce the calls to S3 with cache_tags dependent on the file entity.
Comment #32
fathershawnComment #33
dave reidI've updated #29 with some fixes that were in a fork were were using to change from calling
$this->generateImageStyle($target);and not returning (and affecting performance), to returning$this->generateImageUrl($target);Comment #34
dave reidI think it's also silly because if you miss setting the 'expires' config, that you will not get pre-signed URLs even if the bucket/files are private, which lead to URLS that will 403. So I've moved to making the expires optional, and default it to 1 hour which is the default of the
aws s3 presigncli command.Comment #35
dmouseHi @deviantintegral how to can I help you to test and solve this issue?
Comment #36
dmouseThis is a patch for the latest beta1 version
Comment #37
dmouseSorry the latest patch are wrong :P
Comment #38
misc commentedThanks for the patch @dmouse ! Settings this to Needs review.
Comment #39
dmouseAlso, I added these parameters in my configuration:
Comment #40
dave reidThis version leaves the new 'public' config in the documentation above our new expires config.
Comment #41
tame4tex commentedI have extracted the FileSystem decorator portion of this patch to a new patch on the issue https://www.drupal.org/project/flysystem_s3/issues/3058063.
I need to fix a file permissions error I was getting by did not want to implement signed urls at this stage.
Is there a reason why the decorator class does not extend FileSystem? In my patch I extended it as my thinking is it would be less vulnerable to breaking if FileSystem is modified.
Comment #42
sheise commentedHere's an updated patch to account for the new abstract methods added to FileSystemInterface in 8.7.
Comment #43
dave reidUsing the new interface constants will prevent this from working on existing Drupal 8.6 installs, so if we replace them with the global constants in file.inc, and do a method_exists() check on $this->fileSystem, we should make it compatible with both.
Comment #44
tame4tex commentedWhy are you not extending the original FileSystem class? Wouldn't that prevent having to add these methods when the interface changes?
Comment #45
dave reidExtending is a safe option only if we know 100% for sure that no other modules would want to extend/decorate the FileSystem class. Using decorators is a much safer way to ensure that modules trying to modify a core service will work together instead of conflict with each other.
More resources:
https://www.lullabot.com/articles/get-a-decorator-for-your-drupal-home
https://symfony.com/doc/current/service_container/service_decoration.html
https://stackoverflow.com/questions/4842978/decorator-pattern-versus-sub...
Comment #46
paulusmoj commentedHas anyone done a fix to this patch so it'll apply?
Comment #47
paulusmoj commentedUnfortunately patching against 2.0.0rc1 gives the following error re renderRoot:
LogicException: A stray renderRoot() invocation is causing bubbling of attached assets to break. in Drupal\Core\Render\Renderer->renderRoot() (line 138 of /var/www/html/core/lib/Drupal/Core/Render/Renderer.php).
Does this affect assets themselves or just their display in the Drupal editing page? If it's the latter we may not care for our purposes.
Comment #48
hkirsman commentedI'm trying to migrate files to s3 private file system and getting errors because it tries to create folders. I think it shouldn't do any folder creation on S3, right?
So here it goes to the core class.

In the core class there's the prepareDestination().
And in that function this fails:
I thought I'd do something like this in the decorator class:
but as you saw from the first screenshot, it executes the original method.
What to do with this?
Comment #49
hkirsman commentedGiving it another try - adjusting against latest dev
1 out of 10 hunks of Interdiff fails but I'll add it here anyway.
Comment #50
hkirsman commentedAdding new patch.
1. I was still getting "The file permissions could not be set on sxf://2020-Downloads-Icon-TS-128x128.svg. " during migration so I copy pasted the original copy() method and removed chmod. Also could somebody explain why try to chmod files on s3 in the first place? Is that even possible?
2. Secondly I was starting to get this:
Error: Call to protected method Drupal\Core\File\FileSystem::prepareDestination() from context 'Drupal\flysystem_s3\File\FlysystemS3FileSystem' in /app/web/modules/contrib/flysystem_s3/src/File/FlysystemS3FileSystem.php on line 272 #So I changed the code to extend FileSystem. Probably related to this https://gonzalo123.com/tag/programming/
3. I updated realpath() to get the path on S3. Flysystem itself decorates the original class and the method just returns false. Maybe we should decorate StreamWrapperManager and update the method in that way?
PS. Why are we extending all the functions and then just calling the original?
Comment #51
leon kessler commentedJust tried latest patch from #50
I get the following fatal error when I edit a node that already has an existing file upload:
So I've gone searching fro this
generateImageUrl()function. But can't seem to find it anywhere.It's not in:
Can anyone help me out?
Comment #52
leon kessler commentedOh I think I found it in #2661588: Support delivering local image styles until remote upload is complete
I guess this patch depends on that one?
Comment #53
deviantintegral commentedGlad to see this is still active. I just realized this was still assigned to me years later!
The client we originally wrote this for eventually migrated to Amazon EFS (it wasn't available when we did this work). As far as I know, none of our clients are currently using S3 for assets, or images in particular. They're all on hosting providers that offer shared storage of some kind. Long term, I think a better solution would be for core to support pluggable image style rendering, so we don't have to mask latency with terminate events and the like. That would likely simplify this patch too. Otherwise, for now I'll be unsubscribing from this issue.
Comment #54
leon kessler commentedUnfortunately for us, we are tied to S3. So I have continued to work with this patch.
Here's a new patch that fixes the following updates:
$this->renderer->renderRoot($build)to$this->renderer->renderPlain($build). This fixes fatal errors reported in comments #26 #47. I was able to reproduce those errors simply by uploading an image (I would get a fatal error on the Ajax request to upload the image). So this change is crucial to actually allow image uploads (at least in my instance).Please note I haven't fully understood the consequences of switching to renderPlain(). But it appears to work fine for me right now.
$config['image.settings']['suppress_itok_output'] = TRUE;as per comment from #20Comment #55
leon kessler commentedPrevious patch wasn't setting max-age, so Drupal's render caching would eventually return expired urls.
I've added a simple
hook_file_load()to add the correct max-age. However, this depends on the$settings['flysystem']['s3']['config']['expires']being implicitly set in settings.php (when actually it is optional, with a default of 3600 set in S3.php). I didn't have time to work out a way to get the default value from the plugin.Also, if page caching is enabled you will most likely get expired urls anyway.
I'm setting this to needs work, I don't think this is ready to be merged into the module as it currently stands. But it looks as though many are using the patch (or various versions of it) in production. So it's a case of use-at-your-own-risk for now :-)
Comment #56
leon kessler commentedNew patch incoming.
I've been running into more max-age issues, and have changed the way max-age is set.
This was the code from the previous patch:
Firstly, yes page cache is still an issue, and will need to be disabled for any response that has a s3 signature. (Big caveat there for anyone wanting to use this patch).
Secondly, that
renderRoot()call doesn't work at all for me (for reasons mentioned in my previous comment). But I've since realised that changing this torenderPlain()was pointless. As the whole reason of this code is to set the max-age to 0 on the current request, whereas using renderPlain() means you just set it within that isolated rendering process.Instead, I've moved all caching logic to
hook_entity_load()(including the page cache kill switch). There may be a better place for this, but this is working for me right now.Also, a sidenote for anyone using jsonapi. I found that the dynamic_page_cache module is incompatible with this approach, and this must be disabled. This is due to the issue described in #2449749: Add #cache['downstream-ttl'] to force expiration after a certain time and fix #cache['max-age'] logic by adding #cache['age'].
Jsonapi's own
cache_jsonapi_normalizationsare essentially re-used in the dynamic_page_cache, so if the former is set before the latter, you will end up with expired urls being outputted to the user.Appears to be specific to jsonapi, as outside of this the dynamic_page_cache module still works ok.
This patch still "needs work". The tests need updating, and I am certain there are situations where max-age bubbling will be an issue.
Comment #57
leon kessler commentedNew stripped down patch:
$config['image.settings']['suppress_itok_output'] = TRUE;. As this is is a global setting (so may break for sites running a local + s3 filesystem together). Also it looks like it might the setting may be getting removed in Drupal core. Instead we allow the itok parameter to be added to the url, but strip it out using a preprocess function. I looked at how the s3fs module was handling the itok parameter for this.However, despite me continually working on this, I do not believe we can currently merge this patch in, due to Drupal's internal caching not supporting definitive max-ages.
From my experience, even after using the core patch to fix the internal page cache issue, we still found that expired urls would persist in the cache. It was difficult to debug, as it not clear what generated the cache entry (whether there was any re-use of existing cache's, which therefore weren't passing on the previously set max-age). We've ended up resorting to a nightly cron job to run
drush cr, to ensure all our signatures are valid.Therefore, I'm marking this now as "postponed". Which means essentially, use as-is, and be aware of the potential caching issues that can occur. If the Drupal core page cache issue is fixed, we can look at merging this in.
I've also added a related issue for the s3fs module.
Comment #58
leon kessler commentedSmall update to pass the tests.
Comment #59
leon kessler commentedSo it turns out that using
hook_preprocess_image_stylefor removing the itok param isn't a great solution, as there as instances where this isn't run (we've already encountered this through JSON:API).However, the security concern I had around using
suppress_itok_outputis actually incorrect. This setting just handles the output of image style urls, whereasallow_insecure_derivativesis responsible for allowing images to be generated without the token.Therefore it's safe to use
suppress_itok_output. The only issues are:Comment #60
effulgentsia commentedThat's a good reason not to use the global setting for this. You can implement hook_file_url_alter() instead, so that you only remove the token from the URLs in the scheme and/or bucket that you want to target.
Comment #61
leon kessler commentedUnfortunately, you cannot use hook_file_url_alter() to remove the token, as it’s run before the token is added.
The only way I could see doing it, would be to override the core ImageStyle entity, which is not ideal.
See my comment on https://www.drupal.org/project/drupal/issues/2568517
Comment #62
eli-tHere is a reroll of the patch in [2772847-59] against 2.0.x HEAD.Comment #63
eli-tDo not use the patch in 62, it contains errors. Reroll of the reroll incoming.
Comment #65
eli-tNote I've discussed this with @deviantintegral and they no longer have a use for this patch. This issue may be closed once I have no further need for it unless anyone else comments that they still require it.
Comment #66
eli-tNeeds reroll to apply against 2.1.x following #3409513: webblog 10.0.0
Comment #67
eli-tApologies, my PHPStorm settings have mangled the formatting in that push. Will rectify forthwith...
Comment #68
eli-tFor some reason* the patch generated on gitlab for the current state of this merge request won't apply against current 2.1.x, but a manually generated diff between the merge request branch and 2.1.x will. This is v odd especially as the MR will merge without conflicts.
So here's a patch.
*probably because I somehow did something wrong merging in the changes commit in #3409514: Fix PHPCS and PHPStan issues reported by Gitlab CI 🤷♀️
Comment #69
eli-t