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 

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 _ 

27 

28import re 

29import random 

30import string 

31import json 

32import hashlib 

33import crypt 

34import base64 

35import six 

36import requests 

37import time 

38import logging 

39import binascii 

40 

41from importlib import import_module 

42from datetime import datetime, timedelta 

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

44 

45from . import VERSION 

46 

47#: logger facility 

48logger = logging.getLogger(__name__) 

49 

50 

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) 

58 

59 

60def context(params): 

61 """ 

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

63 

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 

71 

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 

80 

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 

102 

103 

104def json_response(request, data): 

105 """ 

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

107 

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

117 

118 

119def import_attr(path): 

120 """ 

121 transform a python dotted path to the attr 

122 

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

142 

143 

144def redirect_params(url_name, params=None): 

145 """ 

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

147 

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) 

157 

158 

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 

163 

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 

177 

178 

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

182 

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 

196 

197 

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

199 """ 

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

201 

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 ) 

219 

220 

221def get_current_url(request, ignore_params=None): 

222 """ 

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

224 

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 

240 

241 

242def update_url(url, params): 

243 """ 

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

245 

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

272 

273 

274def unpack_nested_exception(error): 

275 """ 

276 If exception are stacked, return the first one 

277 

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 

292 

293 

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

295 """ 

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

297 

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 

312 

313 

314def gen_lt(): 

315 """ 

316 Generate a Login Ticket 

317 

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) 

323 

324 

325def gen_st(): 

326 """ 

327 Generate a Service Ticket 

328 

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) 

334 

335 

336def gen_pt(): 

337 """ 

338 Generate a Proxy Ticket 

339 

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) 

345 

346 

347def gen_pgt(): 

348 """ 

349 Generate a Proxy Granting Ticket 

350 

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) 

356 

357 

358def gen_pgtiou(): 

359 """ 

360 Generate a Proxy Granting Ticket IOU 

361 

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) 

367 

368 

369def gen_saml_id(): 

370 """ 

371 Generate an saml id 

372 

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

374 :rtype: unicode 

375 """ 

376 return _gen_ticket() 

377 

378 

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 

392 

393 

394def crypt_salt_is_valid(salt): 

395 """ 

396 Validate a salt as crypt salt 

397 

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 

422 

423 

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

429 

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

434 

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 } 

448 

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 } 

457 

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 

464 

465 class BadHash(ValueError): 

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

467 pass 

468 

469 class BadSalt(ValueError): 

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

471 pass 

472 

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

478 

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

487 

488 @classmethod 

489 def _test_scheme(cls, scheme): 

490 """ 

491 Test if a scheme is valide or raise BadScheme 

492 

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 ) 

502 

503 @classmethod 

504 def _test_scheme_salt(cls, scheme): 

505 """ 

506 Test if the scheme need a salt or raise BadScheme 

507 

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 ) 

517 

518 @classmethod 

519 def _test_scheme_nosalt(cls, scheme): 

520 """ 

521 Test if the scheme need no salt or raise BadScheme 

522 

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 ) 

532 

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

538 

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 

567 

568 @classmethod 

569 def get_scheme(cls, hashed_passord): 

570 """ 

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

572 

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 

583 

584 @classmethod 

585 def get_salt(cls, hashed_passord): 

586 """ 

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

588 

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

611 

612 

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

614 """ 

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

616 assuming the encoding is ``charset``. 

617 

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) 

665 

666 

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. 

671 

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

677 

678 

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. 

683 

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) 

712 

713 

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 ] 

721 

722 

723def logout_request(ticket): 

724 """ 

725 Forge a SLO logout request 

726 

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 } 

740 

741 

742def regexpr_validator(value): 

743 """ 

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

745 

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 )