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