Parcourir la source

Add URL-token authorization tools

Jocelyn Delalande il y a 7 ans
Parent
commit
aed60757ed

+ 1 - 0
requirements/base.txt

@@ -1,4 +1,5 @@
 Django>=1.9.3,<1.10
 PyYAML>=3.11,<4.0
+django-request-token>=0.6,<0.7
 pytz
 sqlparse

+ 36 - 0
wifiwithme/apps/contribmap/tests.py

@@ -2,11 +2,13 @@ import json
 import warnings
 
 from django.core import mail
+from django.core.signing import BadSignature
 from django.contrib.auth.models import User
 from django.test import TestCase, Client, override_settings
 
 from contribmap.models import Contrib
 from contribmap.forms import PublicContribForm
+from contribmap.tokens import ContribTokenManager, URLTokenManager
 
 
 class APITestClient(Client):
@@ -246,3 +248,37 @@ class TestDataImport(TestCase):
         for contrib in Contrib.objects.all():
             contrib.full_clean()
             contrib.save()
+
+
+class URLTokenManagerTests(TestCase):
+    def test_sign_unsign_ok(self):
+        input_data = {'foo': 12}
+        at = URLTokenManager().sign(input_data)
+        output_data = URLTokenManager().unsign(at)
+        self.assertEqual(input_data, output_data)
+
+    def test_sign_unsign_wrong_sig(self):
+        with self.assertRaises(BadSignature):
+            URLTokenManager().unsign(
+                b'eyJmb28iOiAxfTpvUFZ1Q3FsSldtQ2htMXJBMmx5VFV0ZWxDLWM')
+
+
+class ContribTokenManagerTests(TestCase):
+    def test_sign_unsign_ok(self):
+        Contrib.objects.create(
+            name='John2',
+            phone='010101020101',
+            contrib_type=Contrib.CONTRIB_CONNECT,
+            privacy_coordinates=True,
+            latitude=0.1,
+            longitude=0.12,
+        )
+
+        contrib = Contrib.objects.all().first()
+
+        manager = ContribTokenManager()
+        token = manager.mk_token(contrib)
+
+        self.assertEqual(
+            manager.get_instance_if_allowed(token, contrib.pk),
+            contrib)

+ 81 - 0
wifiwithme/apps/contribmap/tokens.py

@@ -0,0 +1,81 @@
+import json
+
+from django.core.signing import TimestampSigner, BadSignature
+from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
+from django.utils.encoding import DjangoUnicodeDecodeError
+
+from .models import Contrib
+
+
+class URLTokenManager(TimestampSigner):
+    """ Handle signed json data as URL-safe strings
+
+    This class has two responsibilities:
+    - sign/unsign
+    - pack/unpack JSON to base64
+    """
+
+    def sign(self, payload, *args, **kwargs):
+        """
+        :param payload: the data to be embeded into the token
+        :type data: dict
+        """
+        return urlsafe_base64_encode(
+            super().sign(
+                json.dumps(payload), *args, **kwargs
+            ).encode('utf-8')).decode('ascii')
+
+    def unsign(self, encoded_payload, *args, **kwargs):
+        decoded_payload = urlsafe_base64_decode(encoded_payload)
+        unsigned = super().unsign(decoded_payload)
+        return json.loads(unsigned)
+
+
+class TokenError(Exception):
+    pass
+
+
+class ContribTokenManager:
+    """Produce and use signed URL tokens for account-less contrib management
+    """
+    SCOPE = 'contrib/manage'
+
+    def __init__(self):
+        self.manager = URLTokenManager()
+
+    def mk_token(self, contrib):
+        """ Generate a signed contrib management token
+
+        Valid for a given contrib, and for a limited time.
+
+        :type contrib: Contrib
+        :rtype str:
+        """
+        return self.manager.sign({'scope': self.SCOPE, 'pk': contrib.pk})
+
+    def get_instance_if_allowed(self, encoded_token, pk=None):
+        """Return a contrib if the token grants authz for that Contrib pk
+
+        Raise a TokenError if not authorized.
+
+        :param pk: the contrib pk (optional, if you want to check that the
+          instance is the right one)
+        :param encoded_token: the encoded token, from ``mk_token()``:
+        :return: a Contrib instance or None, if the object does not exist
+        :rtype Contrib:
+        """
+        try:
+            data = self.manager.unsign(encoded_token)
+        except BadSignature:
+            raise TokenError('Invalid token signature')
+
+        except (DjangoUnicodeDecodeError, UnicodeDecodeError):
+            raise TokenError('This is not a well-formed token')
+
+        if (pk is not None) and (data['pk'] != pk):
+            raise TokenError('Token is not valid for id {}'.format(pk))
+        else:
+            try:
+                return Contrib.objects.get(pk=data['pk'])
+            except Contrib.DoesNotExist:
+                return None