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 try:
413 hashed = crypt.crypt("", salt)
414 except OSError:
415 return False
416 if not hashed or '$' not in hashed[1:]: 416 ↛ 417line 416 didn't jump to line 417, because the condition on line 416 was never true
417 return False
418 else:
419 return True
420 else:
421 return True
424class LdapHashUserPassword(object):
425 """
426 Class to deal with hashed password as defined at
427 https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html
428 """
430 #: valide schemes that require a salt
431 schemes_salt = {b"{SMD5}", b"{SSHA}", b"{SSHA256}", b"{SSHA384}", b"{SSHA512}", b"{CRYPT}"}
432 #: valide sschemes that require no slat
433 schemes_nosalt = {b"{MD5}", b"{SHA}", b"{SHA256}", b"{SHA384}", b"{SHA512}"}
435 #: map beetween scheme and hash function
436 _schemes_to_hash = {
437 b"{SMD5}": hashlib.md5,
438 b"{MD5}": hashlib.md5,
439 b"{SSHA}": hashlib.sha1,
440 b"{SHA}": hashlib.sha1,
441 b"{SSHA256}": hashlib.sha256,
442 b"{SHA256}": hashlib.sha256,
443 b"{SSHA384}": hashlib.sha384,
444 b"{SHA384}": hashlib.sha384,
445 b"{SSHA512}": hashlib.sha512,
446 b"{SHA512}": hashlib.sha512
447 }
449 #: map between scheme and hash length
450 _schemes_to_len = {
451 b"{SMD5}": 16,
452 b"{SSHA}": 20,
453 b"{SSHA256}": 32,
454 b"{SSHA384}": 48,
455 b"{SSHA512}": 64,
456 }
458 class BadScheme(ValueError):
459 """
460 Error raised then the hash scheme is not in
461 :attr:`LdapHashUserPassword.schemes_salt` + :attr:`LdapHashUserPassword.schemes_nosalt`
462 """
463 pass
465 class BadHash(ValueError):
466 """Error raised then the hash is too short"""
467 pass
469 class BadSalt(ValueError):
470 """Error raised then, with the scheme ``{CRYPT}``, the salt is invalid"""
471 pass
473 @classmethod
474 def _raise_bad_scheme(cls, scheme, valid, msg):
475 """
476 Raise :attr:`BadScheme` error for ``scheme``, possible valid scheme are
477 in ``valid``, the error message is ``msg``
479 :param bytes scheme: A bad scheme
480 :param list valid: A list a valid scheme
481 :param str msg: The error template message
482 :raises LdapHashUserPassword.BadScheme: always
483 """
484 valid_schemes = [s.decode() for s in valid]
485 valid_schemes.sort()
486 raise cls.BadScheme(msg % (scheme, u", ".join(valid_schemes)))
488 @classmethod
489 def _test_scheme(cls, scheme):
490 """
491 Test if a scheme is valide or raise BadScheme
493 :param bytes scheme: A scheme
494 :raises BadScheme: if ``scheme`` is not a valid scheme
495 """
496 if scheme not in cls.schemes_salt and scheme not in cls.schemes_nosalt:
497 cls._raise_bad_scheme(
498 scheme,
499 cls.schemes_salt | cls.schemes_nosalt,
500 "The scheme %r is not valid. Valide schemes are %s."
501 )
503 @classmethod
504 def _test_scheme_salt(cls, scheme):
505 """
506 Test if the scheme need a salt or raise BadScheme
508 :param bytes scheme: A scheme
509 :raises BadScheme: if ``scheme` require no salt
510 """
511 if scheme not in cls.schemes_salt:
512 cls._raise_bad_scheme(
513 scheme,
514 cls.schemes_salt,
515 "The scheme %r is only valid without a salt. Valide schemes with salt are %s."
516 )
518 @classmethod
519 def _test_scheme_nosalt(cls, scheme):
520 """
521 Test if the scheme need no salt or raise BadScheme
523 :param bytes scheme: A scheme
524 :raises BadScheme: if ``scheme` require a salt
525 """
526 if scheme not in cls.schemes_nosalt:
527 cls._raise_bad_scheme(
528 scheme,
529 cls.schemes_nosalt,
530 "The scheme %r is only valid with a salt. Valide schemes without salt are %s."
531 )
533 @classmethod
534 def hash(cls, scheme, password, salt=None, charset="utf8"):
535 """
536 Hash ``password`` with ``scheme`` using ``salt``.
537 This three variable beeing encoded in ``charset``.
539 :param bytes scheme: A valid scheme
540 :param bytes password: A byte string to hash using ``scheme``
541 :param bytes salt: An optional salt to use if ``scheme`` requires any
542 :param str charset: The encoding of ``scheme``, ``password`` and ``salt``
543 :return: The hashed password encoded with ``charset``
544 :rtype: bytes
545 """
546 scheme = scheme.upper()
547 cls._test_scheme(scheme)
548 if salt is None or salt == b"":
549 salt = b""
550 cls._test_scheme_nosalt(scheme)
551 else:
552 cls._test_scheme_salt(scheme)
553 try:
554 return scheme + base64.b64encode(
555 cls._schemes_to_hash[scheme](password + salt).digest() + salt
556 )
557 except KeyError:
558 if six.PY3:
559 password = password.decode(charset)
560 salt = salt.decode(charset)
561 if not crypt_salt_is_valid(salt):
562 raise cls.BadSalt("System crypt implementation do not support the salt %r" % salt)
563 hashed_password = crypt.crypt(password, salt)
564 if six.PY3:
565 hashed_password = hashed_password.encode(charset)
566 return scheme + hashed_password
568 @classmethod
569 def get_scheme(cls, hashed_passord):
570 """
571 Return the scheme of ``hashed_passord`` or raise :attr:`BadHash`
573 :param bytes hashed_passord: A hashed password
574 :return: The scheme used by the hashed password
575 :rtype: bytes
576 :raises BadHash: if no valid scheme is found within ``hashed_passord``
577 """
578 if not hashed_passord[0] == b'{'[0] or b'}' not in hashed_passord:
579 raise cls.BadHash("%r should start with the scheme enclosed with { }" % hashed_passord)
580 scheme = hashed_passord.split(b'}', 1)[0]
581 scheme = scheme.upper() + b"}"
582 return scheme
584 @classmethod
585 def get_salt(cls, hashed_passord):
586 """
587 Return the salt of ``hashed_passord`` possibly empty
589 :param bytes hashed_passord: A hashed password
590 :return: The salt used by the hashed password (empty if no salt is used)
591 :rtype: bytes
592 :raises BadHash: if no valid scheme is found within ``hashed_passord`` or if the
593 hashed password is too short for the scheme found.
594 """
595 scheme = cls.get_scheme(hashed_passord)
596 cls._test_scheme(scheme)
597 if scheme in cls.schemes_nosalt:
598 return b""
599 elif scheme == b'{CRYPT}':
600 if b'$' in hashed_passord: 600 ↛ 602line 600 didn't jump to line 602, because the condition on line 600 was never false
601 return b'$'.join(hashed_passord.split(b'$', 3)[:-1])[len(scheme):]
602 return hashed_passord.split(b'}', 1)[-1]
603 else:
604 try:
605 hashed_passord = base64.b64decode(hashed_passord[len(scheme):])
606 except (TypeError, binascii.Error) as error:
607 raise cls.BadHash("Bad base64: %s" % error)
608 if len(hashed_passord) < cls._schemes_to_len[scheme]:
609 raise cls.BadHash("Hash too short for the scheme %s" % scheme)
610 return hashed_passord[cls._schemes_to_len[scheme]:]
613def check_password(method, password, hashed_password, charset):
614 """
615 Check that ``password`` match `hashed_password` using ``method``,
616 assuming the encoding is ``charset``.
618 :param str method: on of ``"crypt"``, ``"ldap"``, ``"hex_md5"``, ``"hex_sha1"``,
619 ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``, ``"hex_sha512"``, ``"plain"``
620 :param password: The user inputed password
621 :type password: :obj:`str` or :obj:`unicode`
622 :param hashed_password: The hashed password as stored in the database
623 :type hashed_password: :obj:`str` or :obj:`unicode`
624 :param str charset: The used char encoding (also used internally, so it must be valid for
625 the charset used by ``password`` when it was initially )
626 :return: True if ``password`` match ``hashed_password`` using ``method``,
627 ``False`` otherwise
628 :rtype: bool
629 """
630 if not isinstance(password, six.binary_type):
631 password = password.encode(charset)
632 if not isinstance(hashed_password, six.binary_type):
633 hashed_password = hashed_password.encode(charset)
634 if method == "plain":
635 return password == hashed_password
636 elif method == "crypt":
637 if hashed_password.startswith(b'$'):
638 salt = b'$'.join(hashed_password.split(b'$', 3)[:-1])
639 elif hashed_password.startswith(b'_'): # pragma: no cover old BSD format not supported
640 salt = hashed_password[:9]
641 else:
642 salt = hashed_password[:2]
643 if six.PY3:
644 password = password.decode(charset)
645 salt = salt.decode(charset)
646 hashed_password = hashed_password.decode(charset)
647 if not crypt_salt_is_valid(salt):
648 raise ValueError("System crypt implementation do not support the salt %r" % salt)
649 crypted_password = crypt.crypt(password, salt)
650 return crypted_password == hashed_password
651 elif method == "ldap":
652 scheme = LdapHashUserPassword.get_scheme(hashed_password)
653 salt = LdapHashUserPassword.get_salt(hashed_password)
654 return LdapHashUserPassword.hash(scheme, password, salt, charset=charset) == hashed_password
655 elif (
656 method.startswith("hex_") and
657 method[4:] in {"md5", "sha1", "sha224", "sha256", "sha384", "sha512"}
658 ):
659 return getattr(
660 hashlib,
661 method[4:]
662 )(password).hexdigest().encode("ascii") == hashed_password.lower()
663 else:
664 raise ValueError("Unknown password method check %r" % method)
667def decode_version(version):
668 """
669 decode a version string following version semantic http://semver.org/ input a tuple of int.
670 It will work as long as we do not use pre release versions.
672 :param unicode version: A dotted version
673 :return: A tuple a int
674 :rtype: tuple
675 """
676 return tuple(int(sub_version) for sub_version in version.split('.'))
679def last_version():
680 """
681 Fetch the last version from pypi and return it. On successful fetch from pypi, the response
682 is cached 24h, on error, it is cached 10 min.
684 :return: the last django-cas-server version
685 :rtype: unicode
686 """
687 try:
688 last_update, version, success = last_version._cache
689 except AttributeError:
690 last_update = 0
691 version = None
692 success = False
693 cache_delta = 24 * 3600 if success else 600
694 if (time.time() - last_update) < cache_delta:
695 return version
696 else:
697 try:
698 req = requests.get(settings.CAS_NEW_VERSION_JSON_URL)
699 data = json.loads(req.text)
700 version = data["info"]["version"]
701 last_version._cache = (time.time(), version, True)
702 return version
703 except (
704 KeyError,
705 ValueError,
706 requests.exceptions.RequestException
707 ) as error: # pragma: no cover (should not happen unless pypi is not available)
708 logger.error(
709 "Unable to fetch %s: %s" % (settings.CAS_NEW_VERSION_JSON_URL, error)
710 )
711 last_version._cache = (time.time(), version, False)
714def dictfetchall(cursor):
715 "Return all rows from a django cursor as a dict"
716 columns = [col[0] for col in cursor.description]
717 return [
718 dict(zip(columns, row))
719 for row in cursor.fetchall()
720 ]
723def logout_request(ticket):
724 """
725 Forge a SLO logout request
727 :param unicode ticket: A ticket value
728 :return: A SLO XML body request
729 :rtype: unicode
730 """
731 return u"""<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
732 ID="%(id)s" Version="2.0" IssueInstant="%(datetime)s">
733<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"></saml:NameID>
734<samlp:SessionIndex>%(ticket)s</samlp:SessionIndex>
735</samlp:LogoutRequest>""" % {
736 'id': gen_saml_id(),
737 'datetime': timezone.now().isoformat(),
738 'ticket': ticket
739 }
742def regexpr_validator(value):
743 """
744 Test that ``value`` is a valid regular expression
746 :param unicode value: A regular expression to test
747 :raises ValidationError: if ``value`` is not a valid regular expression
748 """
749 try:
750 re.compile(value)
751 except re.error:
752 raise ValidationError(
753 _('"%(value)s" is not a valid regular expression'),
754 params={'value': value}
755 )