|
@@ -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
|