Hide keyboard shortcuts

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 

14 

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 

26 

27import re 

28import random 

29import string 

30import json 

31import hashlib 

32import crypt 

33import base64 

34import six 

35import requests 

36import time 

37import logging 

38import binascii 

39 

40from importlib import import_module 

41from datetime import datetime, timedelta 

42from six.moves.urllib.parse import urlparse, urlunparse, parse_qsl, urlencode 

43 

44from . import VERSION 

45 

46#: logger facility 

47logger = logging.getLogger(__name__) 

48 

49 

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) 

57 

58 

59def context(params): 

60 """ 

61 Function that add somes variable to the context before template rendering 

62 

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 

70 

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 

79 

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 

101 

102 

103def json_response(request, data): 

104 """ 

105 Wrapper dumping `data` to a json and sending it to the user with an HttpResponse 

106 

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") 

116 

117 

118def import_attr(path): 

119 """ 

120 transform a python dotted path to the attr 

121 

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

141 

142 

143def redirect_params(url_name, params=None): 

144 """ 

145 Redirect to ``url_name`` with ``params`` as querystring 

146 

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) 

156 

157 

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 

162 

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 

176 

177 

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

181 

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 

195 

196 

197def set_cookie(response, key, value, max_age): 

198 """ 

199 Set the cookie ``key`` on ``response`` with value ``value`` valid for ``max_age`` secondes 

200 

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 ) 

218 

219 

220def get_current_url(request, ignore_params=None): 

221 """ 

222 Giving a django request, return the current http url, possibly ignoring some GET parameters 

223 

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 

239 

240 

241def update_url(url, params): 

242 """ 

243 update parameters using ``params`` in the ``url`` query string 

244 

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') 

271 

272 

273def unpack_nested_exception(error): 

274 """ 

275 If exception are stacked, return the first one 

276 

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 

291 

292 

293def _gen_ticket(prefix=None, lg=settings.CAS_TICKET_LEN): 

294 """ 

295 Generate a ticket with prefix ``prefix`` and length ``lg`` 

296 

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 

311 

312 

313def gen_lt(): 

314 """ 

315 Generate a Login Ticket 

316 

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) 

322 

323 

324def gen_st(): 

325 """ 

326 Generate a Service Ticket 

327 

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) 

333 

334 

335def gen_pt(): 

336 """ 

337 Generate a Proxy Ticket 

338 

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) 

344 

345 

346def gen_pgt(): 

347 """ 

348 Generate a Proxy Granting Ticket 

349 

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) 

355 

356 

357def gen_pgtiou(): 

358 """ 

359 Generate a Proxy Granting Ticket IOU 

360 

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) 

366 

367 

368def gen_saml_id(): 

369 """ 

370 Generate an saml id 

371 

372 :return: A random id of length ``settings.CAS_TICKET_LEN`` 

373 :rtype: unicode 

374 """ 

375 return _gen_ticket() 

376 

377 

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 

391 

392 

393def crypt_salt_is_valid(salt): 

394 """ 

395 Validate a salt as crypt salt 

396 

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 

418 

419 

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

425 

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}"} 

430 

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 } 

444 

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 } 

453 

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 

460 

461 class BadHash(ValueError): 

462 """Error raised then the hash is too short""" 

463 pass 

464 

465 class BadSalt(ValueError): 

466 """Error raised then, with the scheme ``{CRYPT}``, the salt is invalid""" 

467 pass 

468 

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

474 

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

483 

484 @classmethod 

485 def _test_scheme(cls, scheme): 

486 """ 

487 Test if a scheme is valide or raise BadScheme 

488 

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 ) 

498 

499 @classmethod 

500 def _test_scheme_salt(cls, scheme): 

501 """ 

502 Test if the scheme need a salt or raise BadScheme 

503 

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 ) 

513 

514 @classmethod 

515 def _test_scheme_nosalt(cls, scheme): 

516 """ 

517 Test if the scheme need no salt or raise BadScheme 

518 

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 ) 

528 

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``. 

534 

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 

563 

564 @classmethod 

565 def get_scheme(cls, hashed_passord): 

566 """ 

567 Return the scheme of ``hashed_passord`` or raise :attr:`BadHash` 

568 

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 

579 

580 @classmethod 

581 def get_salt(cls, hashed_passord): 

582 """ 

583 Return the salt of ``hashed_passord`` possibly empty 

584 

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]:] 

605 

606 

607def check_password(method, password, hashed_password, charset): 

608 """ 

609 Check that ``password`` match `hashed_password` using ``method``, 

610 assuming the encoding is ``charset``. 

611 

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) 

659 

660 

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. 

665 

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('.')) 

671 

672 

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. 

677 

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) 

706 

707 

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 ] 

715 

716 

717def logout_request(ticket): 

718 """ 

719 Forge a SLO logout request 

720 

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 } 

734 

735 

736def regexpr_validator(value): 

737 """ 

738 Test that ``value`` is a valid regular expression 

739 

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 )