In a recent project we needed to make use of both public and private files in Drupal 9. We want to make sure that anonymous visitors can't get access to the private files, even if they enter the URL for it directly into the browser (bypassing Drupal).
Of course, it's rare that someone might be able to guess a path/filename exactly, but what if someone with access to the file emails a link for that file to someone else? How do we make sure only logged-in authorized users have access?
I found the most helpful information here: setting up a private file directory in Drupal -- but I'm going to walk you through the process below, and share what I learned.
Tell Drupal where your Private Files directory is going to be.
This is accomplished in Drupal 8+ by setting the variable in the settings.php file:
$settings['file_private_path'] = 'some/absolute/file/path'
The instructions for this setting say "this directory must be absolute, outside of the Drupal installation directory and not accessible over the web".
The statement that the directory must be outside the web root folder (where Drupal is installed) isn't strictly true, you can secure a directory inside the public files directory too, as I'll discuss below. But it's definitely one more level of security to have it above the web root if you have access to that. You will need to create this directory, using either SSH or via FTP on the server.
I tested this in two different server environments, a server on Cloudways, and also in the Pantheon environment. There are some differences.
On the Cloudways server I can set the directory to be above the root.
Pantheon sets the private directory for you (at sites/default/files/private) and doesn't give you access to create your own above the web root -- at least not that I can see. In their documentation, they point to a "guide document" that honestly makes no sense whatsoever.
If you could edit your settings.php file, you could perhaps use a different directory inside sites/default/files to be the private one. Unfortunately, I can't do that because the settings.php file is not tracked via Git as we're using the standard drupal managed upstream package.
So it's already set in Pantheon, and there's nothing to edit in settings.php.
Once this settings variable is set, you should be able to see that Drupal is reading it at Configuration > Media > File System
Simple enough, is that it?
No, there's more. All you've done so far is tell Drupal where you would like your private directory to be. You haven't actually secured it. Now do the following:
- Run the cron -- this will trigger Drupal to generate an .htaccess file inside that private directory.
- Clear the cache.
You would think that the magic .htaccess file would restrict the files from being served over the web, but upon testing, I found that anonymous users can still get to the file. This was true on both my server environments.
What was the point of the .htaccess file then? Well, if someone tried to visit the path directly:
then the .htaccess file should prevent that. However, Drupal's private file system creates another URL for the private file that actually is now openly accessible. Drupal's system changes the URL of the file, and passes it through the routing system serving the file via a different path.
For example, let's say you created a node type called "Private Documents" and on this node type you added a new file field. Let's called it "private file" (field_private_file).
Next set the field settings to be private:
Now if you add some test content, and post up a test sample file, you'll see a link to the file:
Hovering over that link, you'll see the path is pointing to: domain/system/files/[filename]. It's not pointing to the "real" URL as the .htaccess file is going to prevent access to that.
This new path is controlled through Drupal, but is still accessible to the public. However, now that it's a Drupal path, it's also subject to Drupal's permission system. This gives you the power to control exactly who should have access (either by role, or user ID, etc.). The .htaccess file restricts the native path from all users, but Drupal itself still has read/write access to the file, and now through it's routing system you can set permissions.
You just need some way to set up permissions to restrict access.
A couple options that I tested are:
Allows you to set up edit & view permissions for content types. This would work if you have a certain content type that would only have a private field.
This is a good option if you need to be able to have both public and private fields on the same content type. You can set different permissions per field on the same content.
If either the field or the content type is restricted for the given user, then they will not be able to access the file at the private path, even if they enter that path directly into a browser.