make session serializer extensible

support serializing 1-item dicts with tag as key
refactor serializer into flask.json.tag module
continues #1452, closes #1438, closes #1908
This commit is contained in:
David Lord 2017-06-01 22:47:23 -07:00
parent ea2e9609bc
commit 5e1ced3c05
No known key found for this signature in database
GPG key ID: 7A1C87E3F5BC42A8
4 changed files with 201 additions and 140 deletions

View file

@ -9,18 +9,14 @@
:license: BSD, see LICENSE for more details.
"""
import hashlib
import uuid
import warnings
from base64 import b64decode, b64encode
from datetime import datetime
from itsdangerous import BadSignature, URLSafeTimedSerializer
from werkzeug.datastructures import CallbackDict
from werkzeug.http import http_date, parse_date
from . import Markup, json
from ._compat import iteritems, text_type
from .helpers import is_ip, total_seconds
from flask.helpers import is_ip, total_seconds
from flask.json.tag import TaggedJSONSerializer
class SessionMixin(object):
@ -57,126 +53,6 @@ class SessionMixin(object):
#: from being served the same cache.
accessed = True
class TaggedJSONSerializer(object):
"""A customized JSON serializer that supports a few extra types that
we take for granted when serializing (tuples, markup objects, datetime).
"""
def __init__(self):
self.conversions = [
{
'check': lambda value: self._is_dict_with_used_key(value),
'tag': lambda value: self._tag_dict_used_with_key(value),
'untag': lambda value: self._untag_dict_used_with_key(value),
'key': ' di',
},
{
'check': lambda value: isinstance(value, tuple),
'tag': lambda value: [self._tag(x) for x in value],
'untag': lambda value: tuple(value),
'key': ' t',
},
{
'check': lambda value: isinstance(value, uuid.UUID),
'tag': lambda value: value.hex,
'untag': lambda value: uuid.UUID(value),
'key': ' u',
},
{
'check': lambda value: isinstance(value, bytes),
'tag': lambda value: b64encode(value).decode('ascii'),
'untag': lambda value: b64decode(value),
'key': ' b',
},
{
'check': lambda value: callable(getattr(value, '__html__',
None)),
'tag': lambda value: text_type(value.__html__()),
'untag': lambda value: Markup(value),
'key': ' m',
},
{
'check': lambda value: isinstance(value, list),
'tag': lambda value: [self._tag(x) for x in value],
},
{
'check': lambda value: isinstance(value, datetime),
'tag': lambda value: http_date(value),
'untag': lambda value: parse_date(value),
'key': ' d',
},
{
'check': lambda value: isinstance(value, dict),
'tag': lambda value: dict((k, self._tag(v)) for k, v in
iteritems(value)),
},
{
'check': lambda value: isinstance(value, str),
'tag': lambda value: self._tag_string(value),
}
]
@property
def keys(self):
return [c['key'] for c in self.conversions if c.get('key')]
def _get_conversion_untag(self, key):
return next(
(c['untag'] for c in self.conversions if c.get('key') == key),
lambda v: None
)
def _is_dict_with_used_key(self, v):
return isinstance(v, dict) and len(v) == 1 and list(v)[0] in self.keys
def _was_dict_with_used_key(self, k):
return k.endswith('__') and k[:-2] in self.keys
def _tag_string(self, value):
try:
return text_type(value)
except UnicodeError:
from flask.debughelpers import UnexpectedUnicodeError
raise UnexpectedUnicodeError(u'A byte string with '
u'non-ASCII data was passed to the session system '
u'which can only store unicode strings. Consider '
u'base64 encoding your string (String was %r)' % value)
def _tag_dict_used_with_key(self, value):
k, v = next(iteritems(value))
return {'%s__' % k: v}
def _tag(self, value):
for tag_ops in self.conversions:
if tag_ops['check'](value):
tag = tag_ops.get('key')
if tag:
return {tag: tag_ops['tag'](value)}
return tag_ops['tag'](value)
return value
def _untag_dict_used_with_key(self, the_value):
k, v = next(iteritems(the_value))
if self._was_dict_with_used_key(k):
return {k[:-2]: self._untag(v)}
def _untag(self, obj):
if len(obj) != 1:
return obj
the_key, the_value = next(iteritems(obj))
untag = self._get_conversion_untag(the_key)
new_value = untag(the_value)
return new_value if new_value else obj
def dumps(self, value):
return json.dumps(self._tag(value), separators=(',', ':'))
def loads(self, value):
return json.loads(value, object_hook=self._untag)
session_json_serializer = TaggedJSONSerializer()
class SecureCookieSession(CallbackDict, SessionMixin):
"""Base class for sessions based on signed cookies."""
@ -284,10 +160,10 @@ class SessionInterface(object):
def get_cookie_domain(self, app):
"""Returns the domain that should be set for the session cookie.
Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise
falls back to detecting the domain based on ``SERVER_NAME``.
Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is
updated to avoid re-running the logic.
"""
@ -377,7 +253,7 @@ class SessionInterface(object):
has been modified, the cookie is set. If the session is permanent and
the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
always set.
This check is usually skipped if the session was deleted.
.. versionadded:: 0.11
@ -404,6 +280,9 @@ class SessionInterface(object):
raise NotImplementedError()
session_json_serializer = TaggedJSONSerializer()
class SecureCookieSessionInterface(SessionInterface):
"""The default session interface that stores sessions in signed cookies
through the :mod:`itsdangerous` module.