jazzband / django-downloadview

Serve files with Django.
https://django-downloadview.readthedocs.io
Other
374 stars 57 forks source link

Implement signatures for file download URLs #138

Closed aleksihakli closed 4 years ago

aleksihakli commented 7 years ago

Hi,

tl; dr: Would this project benefit from the ability to sign download URLs (cryptographically and with expiration)?

I thought I would open a discussion on adding download URL signatures.

I recently implemented cryptographic authorization on top of URLs that were generated directly by the storage backend, much like with S3.

These were served with nginx by using X-Accel and a custom view that generated serve requests to the proxy server, offloading the file serving from Django.

The idea is fairly simple and I think many people could benefit from it. Implementation just requires

The best thing is that download views will work with or without the signature.

Following a naive example of the idea of the implementation. Please bear in mind that these examples are untested and would, of course, need to be further adapted for django_downloadview.

# django_downloadview/storage.py

from django.conf import settings
from django.core.files.storage import FileSystemStorage
from django.core.signing import BadSignature, SignatureExpired, TimestampSigner

class SignedURLMixin(Storage):
    """ Mixin for generating signed file URLs with storage backends. Adds X-Signature query parameter to the normal URLs generated by the storage backend."""

    def url(self, name):
        signer = TimestampSigner()
        expiration = getattr(settings, "DOWNLOADVIEW_URL_EXPIRATION", None)

        path = super(SignedURLMixin, self).url(name)
        signature = signer.sign(path)
        return '{}?X-Signature={}'.format(path, signature)

class SignedFileSystemStorage(SignedURLMixin, FileSystemStorage):
    pass
# django_downloadview/decorators.py

from functools import wraps

from django.core.exceptions import PermissionDenied

def signature_required(function):
    """ Decorator that checks for X-Signature query parameter to authorize specific user access. """

    @wraps
    def decorator(request, *args, **kwargs):
        signer = TimestampSigner()
        signature = request.GET.get("X-Signature")
        expiration = getattr(settings, "DOWNLOADVIEW_URL_EXPIRATION", None)

        try:
            signature_path = signer.unsign(signature, max_age=expiration)
        except SignatureExpired as e:
            raise PermissionDenied("Signature expired") from e
        except BadSignature as e:
            raise PermissionDenied("Signature invalid") from e
        except Exception as e:
            raise PermissionDenied("Signature error") from e

        if request.path != signature_path:
            raise PermissionDenied("Signature mismatch")

        return function(request, *args, **kwargs)

    return decorator

Then the usage can simply be:

# demoproject/urls.py

# Django is set up with
# DEFAULT_FILE_STORAGE='example.storage.SignedFileSystemStorage'

from django.conf.urls import url, url_patterns
from django_downloadview import ObjectDownloadView
from django_downloadview.decorators import signature_required

from demoproject.download.models import Document  # A model with a FileField

# ObjectDownloadView inherits from django.views.generic.BaseDetailView.
download = ObjectDownloadView.as_view(model=Document, file_field='file')

url_patterns = ('',
    url('^download/(?P<slug>[A-Za-z0-9_-]+)/$', signature_required(download), name='download'),
)

{# demoproject/download/template.html #}
{# URLs in templates are generated with the storage class URL implementation #}

<a href="{{ object.file.url  }}">Click here to download.</a>

The S3 Boto storage backend uses a similar approach and makes it possible to generate URLs in user templates and then authorize S3 access with those URLs. This vanilla Django approach makes it very easy to emulate that behaviour.

Additional hardening can then be achieved with:

This approach only lacks in that it introduces non-cacheable URLs that require slight computation to decrypt.

Inspiration was received from Grok. You can find more information on generic URL signatures in his weblog:

If signatures are appended to URLs with existing query parameters, a more sophisticated solution has to be used. For example:

aleksihakli commented 7 years ago

Hi @benoitbryon and @Natim, would this be something that is needed in this project? I could implement this in a PR but would like to know if there has been any previous discussion or opinions on the subject.

Natim commented 4 years ago

@aleksihakli feel free to do so if you think it is still interesting yes.

aleksihakli commented 4 years ago

I'll try and find time for making a PR. The example code should work as-is. I've tested it locally in the past. It could be added to the codebase with a few unit tests.

aleksihakli commented 4 years ago

@Natim the docs seem to be of the old version by the way, is it possible to update the RTD site with the new contents which also describe using the new configuration options?

Natim commented 4 years ago

Yes I asked @benoitbryon about that. But we might consider asking someone from RTD to give us access.

benoitbryon commented 4 years ago

Hi there :) @Natim, I just granted you maintainer of django-downloadview on RTD. Is it ok for you? Feel free to make the changes you think are necessary.

Natim commented 4 years ago

Thank you Benoît I will have a look

Le mar. 14 janv. 2020 à 23:05, Benoît Bryon notifications@github.com a écrit :

Hi there :) Natim, I just granted you maintainer of django-downloadview on RTD. Is it ok for you? Feel free to make the changes you think are necessary.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/benoitbryon/django-downloadview/issues/138?email_source=notifications&email_token=AABYATPICSWOUFY2GLB6E5DQ5YZLTA5CNFSM4DFCGB3KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEI6JP6A#issuecomment-574396408, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABYATINNY3NFUTZNZU6SR3Q5YZLTANCNFSM4DFCGB3A .