django-nginx-secure-links 0.0.9
Django storage based on Nginx secure links module
License: MIT License
Requires Python: >=3.6

.. image:: https://github.com/LighTechLLC/django-nginx-secure-links/actions/workflows/ci.yml/badge.svg
    :target: https://github.com/LighTechLLC/django-nginx-secure-links/actions/workflows/ci.yml
    :alt: Build Status

.. image:: https://coveralls.io/repos/LighTechLLC/django-nginx-secure-links/badge.svg?branch=master
   :target: https://coveralls.io/r/LighTechLLC/django-nginx-secure-links?branch=master
   :alt: Coverage

Django Nginx Secure Links
=========================

This module is a Django extension for using `ngx_http_secure_link_module <http://nginx.org/en/docs/http/ngx_http_secure_link_module.html>`_.
It provides private urls with expiration lifetime by implementing described logic of ngx_http_secure_link_module.
The major advantage of the extension is that Django delegates file serving on Nginx layer and does only pre-signed urls generation.

Requirements
============

Django Nginx Secure Links requires Django 3.2 or later.


Installation
============

Installing from PyPI is as easy as doing:

.. code-block:: bash

    pip install django-nginx-secure-links

If you want to install it from source, grab the git repository from GitHub and run setup.py:

.. code-block:: bash

    git clone git://github.com/lighTechLLC/django-nginx-secure-links.git
    cd django-nginx-secure-links
    python setup.py install

Nginx module set up
===================

**Option 1**

Install using apt (Ubuntu example):

.. code-block:: bash

    sudo apt install nginx-extras

**Option 2**

Build from sources:

.. code-block:: bash

    ./configure .... --with-http_secure_link_module


Quick example
=============


1. Django settings set up **settings.py**:

.. code-block:: python

    INSTALLED_APPS = (
        ...
        'nginx_secure_links',
        ...
    )
    MEDIA_ROOT = '/var/www/media/'
    MEDIA_URL = '/media/'
    SECURE_LINK_SECRET_KEY = 'KfM6aA6M7H'

- Django < 4.2

.. code-block:: python

    DEFAULT_FILE_STORAGE = 'nginx_secure_links.storages.FileStorage'


- Django >= 4.2

.. code-block:: python

    STORAGES = {
        "default": {
            "BACKEND": "nginx_secure_links.storages.FileStorage",
        },
        "staticfiles": {
            "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
        },
    }



2. Create a private file inside your ``settings.MEDIA_ROOT``:

.. code-block:: bash

    echo "I'm private text file" > /var/www/media/sample.txt

3. Let's start ``runserver`` and access the file outside of Django file storage. It works and the file is available. There is no access denied, because of ``runserver`` mode:

.. code-block:: bash

    curl http://127.0.0.1:8000/media/sample.txt

4. Set up Nginx virtual host file **site.conf**:

.. code-block:: nginx

    server 127.0.0.1;
    listen 80;

    ...

    location /media/ {
        secure_link $arg_token,$arg_expires;
        secure_link_md5 "$secure_link_expires$uri KfM6aA6M7H";

        if ($secure_link = "") {
            return 403;
        }

        if ($secure_link = "0") {
            return 410;
        }

        alias /var/www/media/;
    }

    ...

5. Let's access the file through Nginx host/port.

.. code-block:: bash

    curl http://127.0.0.1/media/sample.txt

Because of Nginx secure link module protection, the file won't be served
without ``?token=...&expires=...`` parameters. Only django users will be able
to access files which urls generated by django storage.

Usage
=====

**models.py**

.. code-block:: python

    class Report(models.Model):
        pdf_file = models.FileField(upload_to='reports')

**views.py**

.. code-block:: python

    def report_details(request, report_id)
        instance = Report.objects.get(id=report_id)
        return JsonResponse({'url': instance.pdf_file.url})

**json response**

.. code-block:: json

    {
      "url": "/media/reports/29974.pdf?expires=1599214310&token=ErLcMm96-4h2qsuj2Avo-w"
    }


That's it, all uploaded media files through Django will be pre-signed.
If you work locally and do not want to install Nginx, let's skip it for
local development- django will generate pre-signed urls, but all files will be
available because of ``runserver`` command serves files and does not provide
3rd-party ``nginx-secure-link`` module functionality.

Settings
========

- ``SECURE_LINK_SECRET_KEY``

Your specific secret string which Nginx is going to use in ``secure_link_md5`` directive.

- ``SECURE_LINK_TOKEN_FIELD`` (optional, default: ``token``)

Your custom name of the hash GET-parameter (?token=xyz)

- ``SECURE_LINK_EXPIRES_FIELD`` (optional, default: ``expires``)

Your custom name of expiration timestamp GET-parameter  (?expires=1599215210)

- ``SECURE_LINK_EXPIRATION_SECONDS`` (optional, default: ``86400``- 1 day)

Your custom value of expiration seconds. Any pre-signed link will be expired after ``SECURE_LINK_EXPIRATION_SECONDS``.

- ``SECURE_LINK_PRIVATE_PREFIXES`` (optional, default: ``[]``)

List of private paths without ``MEDIA_URL`` prefix. Just leave it empty for making all media urls private. Example:

.. code-block:: python

    MEDIA_URL = '/media/'
    SECURE_LINK_PRIVATE_PREFIXES = [
        'documents/',
        'reports/',
    ]

In such case all ``/media/documents/`` and ``/media/reports/`` urls will be private and pre-signed by using token and expiration time. If any of existing prefixes on the project are not listed in ``SECURE_LINK_PRIVATE_PREFIXES``, so the url will be public.

- ``SECURE_LINK_PUBLIC_PREFIXES`` (optional, default: ``[]``)

List of private paths without ``MEDIA_URL`` prefix. Example:

.. code-block:: python

    MEDIA_URL = '/media/'
    SECURE_LINK_PUBLIC_PREFIXES = [
        'avatars/',
        'shared/',
    ]

In such case only ``/media/avatars/`` and ``/media/shared/`` urls will be public and generated without pre-signed urls. All other urls, will be private and pre-signed by using token and expiration time.

**Important** If you want to keep all media files privately, ``SECURE_LINK_PRIVATE_PREFIXES`` and ``SECURE_LINK_PUBLIC_PREFIXES`` should be ``[]``.

Custom storage for non-media files
==================================

**Example 1:** We are going to use our own server directory and url prefix instead
of ``settings.MEDIA_ROOT`` / ``settings.MEDIA_URL``.
The example is going to use all default ``settings.SECURE_LINK_*``

.. code-block:: python

    from nginx_secure_links.storages import FileStorage

    storage = FileStorage(location='/var/www/personal_data/', base_url='/personal/')
    storage.url('profile.pdf')
    storage.url('profile.pdf', lifetime=60)
    storage.url('profile.pdf', lifetime=0)

**Example 2**: We are going to use custom storage with all overridden settings.

.. code-block:: python

  from nginx_secure_links.storages import FileStorage

  storage = FileStorage(
        location='/var/www/personal_data/',
        base_url='/personal/'
        nginx_secret_key='91rdywY7d4494X',
        expires_field_name='expires_timestamp',
        token_field_name='hash',
        private_prefixes=[],
        public_prefixes=[],
        expires_seconds=60 * 60,  # 60min
    ) # all private
    storage.url('profile.pdf')  # /personal/profile.pdf?hash=mlkiuhbhu83d&expires_timestamp=2147483647

Using It
========

Generate pre-signed url by passing public url::

    python manage.py secure_links_gen_signed /media/reports/sample.pdf


Generates a sample of Nginx location basing on the settings::

    python manage.py secure_links_nginx_location


Found a Bug?
============
Issues are tracked via GitHub issues at the `project issue page
<https://github.com/LighTechLLC/django-nginx-secure-links/issues>`_.