Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2# This program is distributed in the hope that it will be useful, but WITHOUT
3# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
4# FOR A PARTICULAR PURPOSE. See the GNU General Public License version 3 for
5# more details.
6#
7# You should have received a copy of the GNU General Public License version 3
8# along with this program; if not, write to the Free Software Foundation, Inc., 51
9# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
10#
11# (c) 2015-2016 Valentin Samir
12"""Some util function for the app"""
13from .default_settings import settings
15from django.http import HttpResponseRedirect, HttpResponse
16from django.contrib import messages
17from django.contrib.messages import constants as DEFAULT_MESSAGE_LEVELS
18from django.core.serializers.json import DjangoJSONEncoder
19from django.utils import timezone
20from django.core.exceptions import ValidationError
21try:
22 from django.urls import reverse
23 from django.utils.translation import gettext_lazy as _
24except ImportError:
25 from django.core.urlresolvers import reverse
26 from django.utils.translation import ugettext_lazy as _
28import re
29import random
30import string
31import json
32import hashlib
33import crypt
34import base64
35import six
36import requests
37import time
38import logging
39import binascii
41from importlib import import_module
42from datetime import datetime, timedelta
43from six.moves.urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
45from . import VERSION
47#: logger facility
48logger = logging.getLogger(__name__)
51def json_encode(obj):
52 """Encode a python object to json"""
53 try:
54 return json_encode.encoder.encode(obj)
55 except AttributeError:
56 json_encode.encoder = DjangoJSONEncoder(default=six.text_type)
57 return json_encode(obj)
60def context(params):
61 """
62 Function that add somes variable to the context before template rendering
64 :param dict params: The context dictionary used to render templates.
65 :return: The ``params`` dictionary with the key ``settings`` set to
66 :obj:`django.conf.settings`.
67 :rtype: dict
68 """
69 params["settings"] = settings
70 params["message_levels"] = DEFAULT_MESSAGE_LEVELS
72 if settings.CAS_NEW_VERSION_HTML_WARNING:
73 LAST_VERSION = last_version()
74 params["VERSION"] = VERSION
75 params["LAST_VERSION"] = LAST_VERSION
76 if LAST_VERSION is not None:
77 params["upgrade_available"] = decode_version(VERSION) < decode_version(LAST_VERSION)
78 else:
79 params["upgrade_available"] = False
81 if settings.CAS_INFO_MESSAGES_ORDER:
82 params["CAS_INFO_RENDER"] = []
83 for msg_name in settings.CAS_INFO_MESSAGES_ORDER:
84 if msg_name in settings.CAS_INFO_MESSAGES:
85 if not isinstance(settings.CAS_INFO_MESSAGES[msg_name], dict):
86 continue
87 msg = settings.CAS_INFO_MESSAGES[msg_name].copy()
88 if "message" in msg:
89 msg["name"] = msg_name
90 # use info as default infox type
91 msg["type"] = msg.get("type", "info")
92 # make box discardable by default
93 msg["discardable"] = msg.get("discardable", True)
94 msg_hash = (
95 six.text_type(msg["message"]).encode("utf-8") +
96 msg["type"].encode("utf-8")
97 )
98 # hash depend of the rendering language
99 msg["hash"] = hashlib.md5(msg_hash).hexdigest()
100 params["CAS_INFO_RENDER"].append(msg)
101 return params
104def json_response(request, data):
105 """
106 Wrapper dumping `data` to a json and sending it to the user with an HttpResponse
108 :param django.http.HttpRequest request: The request object used to generate this response.
109 :param dict data: The python dictionnary to return as a json
110 :return: The content of ``data`` serialized in json
111 :rtype: django.http.HttpResponse
112 """
113 data["messages"] = []
114 for msg in messages.get_messages(request):
115 data["messages"].append({'message': msg.message, 'level': msg.level_tag})
116 return HttpResponse(json.dumps(data), content_type="application/json")
119def import_attr(path):
120 """
121 transform a python dotted path to the attr
123 :param path: A dotted path to a python object or a python object
124 :type path: :obj:`unicode` or :obj:`str` or anything
125 :return: The python object pointed by the dotted path or the python object unchanged
126 """
127 # if we got a str, decode it to unicode (normally it should only contain ascii)
128 if isinstance(path, six.binary_type): 128 ↛ 129line 128 didn't jump to line 129, because the condition on line 128 was never true
129 path = path.decode("utf-8")
130 # if path is not an unicode, return it unchanged (may be it is already the attribute to import)
131 if not isinstance(path, six.text_type):
132 return path
133 if u"." not in path:
134 ValueError("%r should be of the form `module.attr` and we just got `attr`" % path)
135 module, attr = path.rsplit(u'.', 1)
136 try:
137 return getattr(import_module(module), attr)
138 except ImportError:
139 raise ImportError("Module %r not found" % module)
140 except AttributeError:
141 raise AttributeError("Module %r has not attribut %r" % (module, attr))
144def redirect_params(url_name, params=None):
145 """
146 Redirect to ``url_name`` with ``params`` as querystring
148 :param unicode url_name: a URL pattern name
149 :param params: Some parameter to append to the reversed URL
150 :type params: :obj:`dict` or :obj:`NoneType<types.NoneType>`
151 :return: A redirection to the URL with name ``url_name`` with ``params`` as querystring.
152 :rtype: django.http.HttpResponseRedirect
153 """
154 url = reverse(url_name)
155 params = urlencode(params if params else {})
156 return HttpResponseRedirect(url + "?%s" % params)
159def reverse_params(url_name, params=None, **kwargs):
160 """
161 compute the reverse url of ``url_name`` and add to it parameters from ``params``
162 as querystring
164 :param unicode url_name: a URL pattern name
165 :param params: Some parameter to append to the reversed URL
166 :type params: :obj:`dict` or :obj:`NoneType<types.NoneType>`
167 :param **kwargs: additional parameters needed to compure the reverse URL
168 :return: The computed reverse URL of ``url_name`` with possible querystring from ``params``
169 :rtype: unicode
170 """
171 url = reverse(url_name, **kwargs)
172 params = urlencode(params if params else {})
173 if params:
174 return u"%s?%s" % (url, params)
175 else:
176 return url
179def copy_params(get_or_post_params, ignore=None):
180 """
181 copy a :class:`django.http.QueryDict` in a :obj:`dict` ignoring keys in the set ``ignore``
183 :param django.http.QueryDict get_or_post_params: A GET or POST
184 :class:`QueryDict<django.http.QueryDict>`
185 :param set ignore: An optinal set of keys to ignore during the copy
186 :return: A copy of get_or_post_params
187 :rtype: dict
188 """
189 if ignore is None:
190 ignore = set()
191 params = {}
192 for key in get_or_post_params:
193 if key not in ignore and get_or_post_params[key]:
194 params[key] = get_or_post_params[key]
195 return params
198def set_cookie(response, key, value, max_age):
199 """
200 Set the cookie ``key`` on ``response`` with value ``value`` valid for ``max_age`` secondes
202 :param django.http.HttpResponse response: a django response where to set the cookie
203 :param unicode key: the cookie key
204 :param unicode value: the cookie value
205 :param int max_age: the maximum validity age of the cookie
206 """
207 expires = datetime.strftime(
208 datetime.utcnow() + timedelta(seconds=max_age),
209 "%a, %d-%b-%Y %H:%M:%S GMT"
210 )
211 response.set_cookie(
212 key,
213 value,
214 max_age=max_age,
215 expires=expires,
216 domain=settings.SESSION_COOKIE_DOMAIN,
217 secure=settings.SESSION_COOKIE_SECURE or None
218 )
221def get_current_url(request, ignore_params=None):
222 """
223 Giving a django request, return the current http url, possibly ignoring some GET parameters
225 :param django.http.HttpRequest request: The current request object.
226 :param set ignore_params: An optional set of GET parameters to ignore
227 :return: The URL of the current page, possibly omitting some parameters from
228 ``ignore_params`` in the querystring.
229 :rtype: unicode
230 """
231 if ignore_params is None:
232 ignore_params = set()
233 protocol = u'https' if request.is_secure() else u"http"
234 service_url = u"%s://%s%s" % (protocol, request.get_host(), request.path)
235 if request.GET:
236 params = copy_params(request.GET, ignore_params)
237 if params:
238 service_url += u"?%s" % urlencode(params)
239 return service_url
242def update_url(url, params):
243 """
244 update parameters using ``params`` in the ``url`` query string
246 :param url: An URL possibily with a querystring
247 :type url: :obj:`unicode` or :obj:`str`
248 :param dict params: A dictionary of parameters for updating the url querystring
249 :return: The URL with an updated querystring
250 :rtype: unicode
251 """
252 if not isinstance(url, bytes):
253 url = url.encode('utf-8')
254 for key, value in list(params.items()):
255 if not isinstance(key, bytes):
256 del params[key]
257 key = key.encode('utf-8')
258 if not isinstance(value, bytes):
259 value = value.encode('utf-8')
260 params[key] = value
261 url_parts = list(urlparse(url))
262 query = dict(parse_qsl(url_parts[4], keep_blank_values=True))
263 query.update(params)
264 # make the params order deterministic
265 query = list(query.items())
266 query.sort()
267 url_query = urlencode(query)
268 if not isinstance(url_query, bytes): # pragma: no cover in python3 urlencode return an unicode
269 url_query = url_query.encode("utf-8")
270 url_parts[4] = url_query
271 return urlunparse(url_parts).decode('utf-8')
274def unpack_nested_exception(error):
275 """
276 If exception are stacked, return the first one
278 :param error: A python exception with possible exception embeded within
279 :return: A python exception with no exception embeded within
280 """
281 i = 0
282 while True:
283 if error.args[i:]: 283 ↛ 290line 283 didn't jump to line 290, because the condition on line 283 was never false
284 if isinstance(error.args[i], Exception):
285 error = error.args[i]
286 i = 0
287 else:
288 i += 1
289 else:
290 break
291 return error
294def _gen_ticket(prefix=None, lg=settings.CAS_TICKET_LEN):
295 """
296 Generate a ticket with prefix ``prefix`` and length ``lg``
298 :param unicode prefix: An optional prefix (probably ST, PT, PGT or PGTIOU)
299 :param int lg: The length of the generated ticket (with the prefix)
300 :return: A randomlly generated ticket of length ``lg``
301 :rtype: unicode
302 """
303 random_part = u''.join(
304 random.choice(
305 string.ascii_letters + string.digits
306 ) for _ in range(lg - len(prefix or "") - 1)
307 )
308 if prefix is not None:
309 return u'%s-%s' % (prefix, random_part)
310 else:
311 return random_part
314def gen_lt():
315 """
316 Generate a Login Ticket
318 :return: A ticket with prefix ``settings.CAS_LOGIN_TICKET_PREFIX`` and length
319 ``settings.CAS_LT_LEN``
320 :rtype: unicode
321 """
322 return _gen_ticket(settings.CAS_LOGIN_TICKET_PREFIX, settings.CAS_LT_LEN)
325def gen_st():
326 """
327 Generate a Service Ticket
329 :return: A ticket with prefix ``settings.CAS_SERVICE_TICKET_PREFIX`` and length
330 ``settings.CAS_ST_LEN``
331 :rtype: unicode
332 """
333 return _gen_ticket(settings.CAS_SERVICE_TICKET_PREFIX, settings.CAS_ST_LEN)
336def gen_pt():
337 """
338 Generate a Proxy Ticket
340 :return: A ticket with prefix ``settings.CAS_PROXY_TICKET_PREFIX`` and length
341 ``settings.CAS_PT_LEN``
342 :rtype: unicode
343 """
344 return _gen_ticket(settings.CAS_PROXY_TICKET_PREFIX, settings.CAS_PT_LEN)
347def gen_pgt():
348 """
349 Generate a Proxy Granting Ticket
351 :return: A ticket with prefix ``settings.CAS_PROXY_GRANTING_TICKET_PREFIX`` and length
352 ``settings.CAS_PGT_LEN``
353 :rtype: unicode
354 """
355 return _gen_ticket(settings.CAS_PROXY_GRANTING_TICKET_PREFIX, settings.CAS_PGT_LEN)
358def gen_pgtiou():
359 """
360 Generate a Proxy Granting Ticket IOU
362 :return: A ticket with prefix ``settings.CAS_PROXY_GRANTING_TICKET_IOU_PREFIX`` and length
363 ``settings.CAS_PGTIOU_LEN``
364 :rtype: unicode
365 """
366 return _gen_ticket(settings.CAS_PROXY_GRANTING_TICKET_IOU_PREFIX, settings.CAS_PGTIOU_LEN)
369def gen_saml_id():
370 """
371 Generate an saml id
373 :return: A random id of length ``settings.CAS_TICKET_LEN``
374 :rtype: unicode
375 """
376 return _gen_ticket()
379def get_tuple(nuplet, index, default=None):
380 """
381 :param tuple nuplet: A tuple
382 :param int index: An index
383 :param default: An optional default value
384 :return: ``nuplet[index]`` if defined, else ``default`` (possibly ``None``)
385 """
386 if nuplet is None:
387 return default
388 try:
389 return nuplet[index]
390 except IndexError:
391 return default
394def crypt_salt_is_valid(salt):
395 """
396 Validate a salt as crypt salt
398 :param str salt: a password salt
399 :return: ``True`` if ``salt`` is a valid crypt salt on this system, ``False`` otherwise
400 :rtype: bool
401 """
402 if len(salt) < 2:
403 return False
404 else:
405 if salt[0] == '$':
406 if salt[1] == '$':
407 return False
408 else:
409 if '$' not in salt[1:]:
410 return False
411 else:
412 hashed = crypt.crypt("", salt)
413 if not hashed or '$' not in hashed[1:]:
414 return False
415 else:
416 return True
417 else:
418 return True
421class LdapHashUserPassword(object):
422 """
423 Class to deal with hashed password as defined at
424 https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html
425 """
427 #: valide schemes that require a salt
428 schemes_salt = {b"{SMD5}", b"{SSHA}", b"{SSHA256}", b"{SSHA384}", b"{SSHA512}", b"{CRYPT}"}
429 #: valide sschemes that require no slat
430 schemes_nosalt = {b"{MD5}", b"{SHA}", b"{SHA256}", b"{SHA384}", b"{SHA512}"}
432 #: map beetween scheme and hash function
433 _schemes_to_hash = {
434 b"{SMD5}": hashlib.md5,
435 b"{MD5}": hashlib.md5,
436 b"{SSHA}": hashlib.sha1,
437 b"{SHA}": hashlib.sha1,
438 b"{SSHA256}": hashlib.sha256,
439 b"{SHA256}": hashlib.sha256,
440 b"{SSHA384}": hashlib.sha384,
441 b"{SHA384}": hashlib.sha384,
442 b"{SSHA512}": hashlib.sha512,
443 b"{SHA512}": hashlib.sha512
444 }
446 #: map between scheme and hash length
447 _schemes_to_len = {
448 b"{SMD5}": 16,
449 b"{SSHA}": 20,
450 b"{SSHA256}": 32,
451 b"{SSHA384}": 48,
452 b"{SSHA512}": 64,
453 }
455 class BadScheme(ValueError):
456 """
457 Error raised then the hash scheme is not in
458 :attr:`LdapHashUserPassword.schemes_salt` + :attr:`LdapHashUserPassword.schemes_nosalt`
459 """
460 pass
462 class BadHash(ValueError):
463 """Error raised then the hash is too short"""
464 pass
466 class BadSalt(ValueError):
467 """Error raised then, with the scheme ``{CRYPT}``, the salt is invalid"""
468 pass
470 @classmethod
471 def _raise_bad_scheme(cls, scheme, valid, msg):
472 """
473 Raise :attr:`BadScheme` error for ``scheme``, possible valid scheme are
474 in ``valid``, the error message is ``msg``
476 :param bytes scheme: A bad scheme
477 :param list valid: A list a valid scheme
478 :param str msg: The error template message
479 :raises LdapHashUserPassword.BadScheme: always
480 """
481 valid_schemes = [s.decode() for s in valid]
482 valid_schemes.sort()
483 raise cls.BadScheme(msg % (scheme, u", ".join(valid_schemes)))
485 @classmethod
486 def _test_scheme(cls, scheme):
487 """
488 Test if a scheme is valide or raise BadScheme
490 :param bytes scheme: A scheme
491 :raises BadScheme: if ``scheme`` is not a valid scheme
492 """
493 if scheme not in cls.schemes_salt and scheme not in cls.schemes_nosalt:
494 cls._raise_bad_scheme(
495 scheme,
496 cls.schemes_salt | cls.schemes_nosalt,
497 "The scheme %r is not valid. Valide schemes are %s."
498 )
500 @classmethod
501 def _test_scheme_salt(cls, scheme):
502 """
503 Test if the scheme need a salt or raise BadScheme
505 :param bytes scheme: A scheme
506 :raises BadScheme: if ``scheme` require no salt
507 """
508 if scheme not in cls.schemes_salt:
509 cls._raise_bad_scheme(
510 scheme,
511 cls.schemes_salt,
512 "The scheme %r is only valid without a salt. Valide schemes with salt are %s."
513 )
515 @classmethod
516 def _test_scheme_nosalt(cls, scheme):
517 """
518 Test if the scheme need no salt or raise BadScheme
520 :param bytes scheme: A scheme
521 :raises BadScheme: if ``scheme` require a salt
522 """
523 if scheme not in cls.schemes_nosalt:
524 cls._raise_bad_scheme(
525 scheme,
526 cls.schemes_nosalt,
527 "The scheme %r is only valid with a salt. Valide schemes without salt are %s."
528 )
530 @classmethod
531 def hash(cls, scheme, password, salt=None, charset="utf8"):
532 """
533 Hash ``password`` with ``scheme`` using ``salt``.
534 This three variable beeing encoded in ``charset``.
536 :param bytes scheme: A valid scheme
537 :param bytes password: A byte string to hash using ``scheme``
538 :param bytes salt: An optional salt to use if ``scheme`` requires any
539 :param str charset: The encoding of ``scheme``, ``password`` and ``salt``
540 :return: The hashed password encoded with ``charset``
541 :rtype: bytes
542 """
543 scheme = scheme.upper()
544 cls._test_scheme(scheme)
545 if salt is None or salt == b"":
546 salt = b""
547 cls._test_scheme_nosalt(scheme)
548 else:
549 cls._test_scheme_salt(scheme)
550 try:
551 return scheme + base64.b64encode(
552 cls._schemes_to_hash[scheme](password + salt).digest() + salt
553 )
554 except KeyError:
555 if six.PY3:
556 password = password.decode(charset)
557 salt = salt.decode(charset)
558 if not crypt_salt_is_valid(salt):
559 raise cls.BadSalt("System crypt implementation do not support the salt %r" % salt)
560 hashed_password = crypt.crypt(password, salt)
561 if six.PY3:
562 hashed_password = hashed_password.encode(charset)
563 return scheme + hashed_password
565 @classmethod
566 def get_scheme(cls, hashed_passord):
567 """
568 Return the scheme of ``hashed_passord`` or raise :attr:`BadHash`
570 :param bytes hashed_passord: A hashed password
571 :return: The scheme used by the hashed password
572 :rtype: bytes
573 :raises BadHash: if no valid scheme is found within ``hashed_passord``
574 """
575 if not hashed_passord[0] == b'{'[0] or b'}' not in hashed_passord:
576 raise cls.BadHash("%r should start with the scheme enclosed with { }" % hashed_passord)
577 scheme = hashed_passord.split(b'}', 1)[0]
578 scheme = scheme.upper() + b"}"
579 return scheme
581 @classmethod
582 def get_salt(cls, hashed_passord):
583 """
584 Return the salt of ``hashed_passord`` possibly empty
586 :param bytes hashed_passord: A hashed password
587 :return: The salt used by the hashed password (empty if no salt is used)
588 :rtype: bytes
589 :raises BadHash: if no valid scheme is found within ``hashed_passord`` or if the
590 hashed password is too short for the scheme found.
591 """
592 scheme = cls.get_scheme(hashed_passord)
593 cls._test_scheme(scheme)
594 if scheme in cls.schemes_nosalt:
595 return b""
596 elif scheme == b'{CRYPT}':
597 if b'$' in hashed_passord: 597 ↛ 599line 597 didn't jump to line 599, because the condition on line 597 was never false
598 return b'$'.join(hashed_passord.split(b'$', 3)[:-1])[len(scheme):]
599 return hashed_passord.split(b'}', 1)[-1]
600 else:
601 try:
602 hashed_passord = base64.b64decode(hashed_passord[len(scheme):])
603 except (TypeError, binascii.Error) as error:
604 raise cls.BadHash("Bad base64: %s" % error)
605 if len(hashed_passord) < cls._schemes_to_len[scheme]:
606 raise cls.BadHash("Hash too short for the scheme %s" % scheme)
607 return hashed_passord[cls._schemes_to_len[scheme]:]
610def check_password(method, password, hashed_password, charset):
611 """
612 Check that ``password`` match `hashed_password` using ``method``,
613 assuming the encoding is ``charset``.
615 :param str method: on of ``"crypt"``, ``"ldap"``, ``"hex_md5"``, ``"hex_sha1"``,
616 ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``, ``"hex_sha512"``, ``"plain"``
617 :param password: The user inputed password
618 :type password: :obj:`str` or :obj:`unicode`
619 :param hashed_password: The hashed password as stored in the database
620 :type hashed_password: :obj:`str` or :obj:`unicode`
621 :param str charset: The used char encoding (also used internally, so it must be valid for
622 the charset used by ``password`` when it was initially )
623 :return: True if ``password`` match ``hashed_password`` using ``method``,
624 ``False`` otherwise
625 :rtype: bool
626 """
627 if not isinstance(password, six.binary_type):
628 password = password.encode(charset)
629 if not isinstance(hashed_password, six.binary_type):
630 hashed_password = hashed_password.encode(charset)
631 if method == "plain":
632 return password == hashed_password
633 elif method == "crypt":
634 if hashed_password.startswith(b'$'):
635 salt = b'$'.join(hashed_password.split(b'$', 3)[:-1])
636 elif hashed_password.startswith(b'_'): # pragma: no cover old BSD format not supported
637 salt = hashed_password[:9]
638 else:
639 salt = hashed_password[:2]
640 if six.PY3:
641 password = password.decode(charset)
642 salt = salt.decode(charset)
643 hashed_password = hashed_password.decode(charset)
644 if not crypt_salt_is_valid(salt):
645 raise ValueError("System crypt implementation do not support the salt %r" % salt)
646 crypted_password = crypt.crypt(password, salt)
647 return crypted_password == hashed_password
648 elif method == "ldap":
649 scheme = LdapHashUserPassword.get_scheme(hashed_password)
650 salt = LdapHashUserPassword.get_salt(hashed_password)
651 return LdapHashUserPassword.hash(scheme, password, salt, charset=charset) == hashed_password
652 elif (
653 method.startswith("hex_") and
654 method[4:] in {"md5", "sha1", "sha224", "sha256", "sha384", "sha512"}
655 ):
656 return getattr(
657 hashlib,
658 method[4:]
659 )(password).hexdigest().encode("ascii") == hashed_password.lower()
660 else:
661 raise ValueError("Unknown password method check %r" % method)
664def decode_version(version):
665 """
666 decode a version string following version semantic http://semver.org/ input a tuple of int.
667 It will work as long as we do not use pre release versions.
669 :param unicode version: A dotted version
670 :return: A tuple a int
671 :rtype: tuple
672 """
673 return tuple(int(sub_version) for sub_version in version.split('.'))
676def last_version():
677 """
678 Fetch the last version from pypi and return it. On successful fetch from pypi, the response
679 is cached 24h, on error, it is cached 10 min.
681 :return: the last django-cas-server version
682 :rtype: unicode
683 """
684 try:
685 last_update, version, success = last_version._cache
686 except AttributeError:
687 last_update = 0
688 version = None
689 success = False
690 cache_delta = 24 * 3600 if success else 600
691 if (time.time() - last_update) < cache_delta:
692 return version
693 else:
694 try:
695 req = requests.get(settings.CAS_NEW_VERSION_JSON_URL)
696 data = json.loads(req.text)
697 version = data["info"]["version"]
698 last_version._cache = (time.time(), version, True)
699 return version
700 except (
701 KeyError,
702 ValueError,
703 requests.exceptions.RequestException
704 ) as error: # pragma: no cover (should not happen unless pypi is not available)
705 logger.error(
706 "Unable to fetch %s: %s" % (settings.CAS_NEW_VERSION_JSON_URL, error)
707 )
708 last_version._cache = (time.time(), version, False)
711def dictfetchall(cursor):
712 "Return all rows from a django cursor as a dict"
713 columns = [col[0] for col in cursor.description]
714 return [
715 dict(zip(columns, row))
716 for row in cursor.fetchall()
717 ]
720def logout_request(ticket):
721 """
722 Forge a SLO logout request
724 :param unicode ticket: A ticket value
725 :return: A SLO XML body request
726 :rtype: unicode
727 """
728 return u"""<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
729 ID="%(id)s" Version="2.0" IssueInstant="%(datetime)s">
730<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"></saml:NameID>
731<samlp:SessionIndex>%(ticket)s</samlp:SessionIndex>
732</samlp:LogoutRequest>""" % {
733 'id': gen_saml_id(),
734 'datetime': timezone.now().isoformat(),
735 'ticket': ticket
736 }
739def regexpr_validator(value):
740 """
741 Test that ``value`` is a valid regular expression
743 :param unicode value: A regular expression to test
744 :raises ValidationError: if ``value`` is not a valid regular expression
745 """
746 try:
747 re.compile(value)
748 except re.error:
749 raise ValidationError(
750 _('"%(value)s" is not a valid regular expression'),
751 params={'value': value}
752 )