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 hashed = crypt.crypt("", salt) 

413 if not hashed or '$' not in hashed[1:]: 

414 return False 

415 else: 

416 return True 

417 else: 

418 return True 

419 

420 

421class LdapHashUserPassword(object): 

422 """ 

423 Class to deal with hashed password as defined at 

424 https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html 

425 """ 

426 

427 #: valide schemes that require a salt 

428 schemes_salt = {b"{SMD5}", b"{SSHA}", b"{SSHA256}", b"{SSHA384}", b"{SSHA512}", b"{CRYPT}"} 

429 #: valide sschemes that require no slat 

430 schemes_nosalt = {b"{MD5}", b"{SHA}", b"{SHA256}", b"{SHA384}", b"{SHA512}"} 

431 

432 #: map beetween scheme and hash function 

433 _schemes_to_hash = { 

434 b"{SMD5}": hashlib.md5, 

435 b"{MD5}": hashlib.md5, 

436 b"{SSHA}": hashlib.sha1, 

437 b"{SHA}": hashlib.sha1, 

438 b"{SSHA256}": hashlib.sha256, 

439 b"{SHA256}": hashlib.sha256, 

440 b"{SSHA384}": hashlib.sha384, 

441 b"{SHA384}": hashlib.sha384, 

442 b"{SSHA512}": hashlib.sha512, 

443 b"{SHA512}": hashlib.sha512 

444 } 

445 

446 #: map between scheme and hash length 

447 _schemes_to_len = { 

448 b"{SMD5}": 16, 

449 b"{SSHA}": 20, 

450 b"{SSHA256}": 32, 

451 b"{SSHA384}": 48, 

452 b"{SSHA512}": 64, 

453 } 

454 

455 class BadScheme(ValueError): 

456 """ 

457 Error raised then the hash scheme is not in 

458 :attr:`LdapHashUserPassword.schemes_salt` + :attr:`LdapHashUserPassword.schemes_nosalt` 

459 """ 

460 pass 

461 

462 class BadHash(ValueError): 

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

464 pass 

465 

466 class BadSalt(ValueError): 

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

468 pass 

469 

470 @classmethod 

471 def _raise_bad_scheme(cls, scheme, valid, msg): 

472 """ 

473 Raise :attr:`BadScheme` error for ``scheme``, possible valid scheme are 

474 in ``valid``, the error message is ``msg`` 

475 

476 :param bytes scheme: A bad scheme 

477 :param list valid: A list a valid scheme 

478 :param str msg: The error template message 

479 :raises LdapHashUserPassword.BadScheme: always 

480 """ 

481 valid_schemes = [s.decode() for s in valid] 

482 valid_schemes.sort() 

483 raise cls.BadScheme(msg % (scheme, u", ".join(valid_schemes))) 

484 

485 @classmethod 

486 def _test_scheme(cls, scheme): 

487 """ 

488 Test if a scheme is valide or raise BadScheme 

489 

490 :param bytes scheme: A scheme 

491 :raises BadScheme: if ``scheme`` is not a valid scheme 

492 """ 

493 if scheme not in cls.schemes_salt and scheme not in cls.schemes_nosalt: 

494 cls._raise_bad_scheme( 

495 scheme, 

496 cls.schemes_salt | cls.schemes_nosalt, 

497 "The scheme %r is not valid. Valide schemes are %s." 

498 ) 

499 

500 @classmethod 

501 def _test_scheme_salt(cls, scheme): 

502 """ 

503 Test if the scheme need a salt or raise BadScheme 

504 

505 :param bytes scheme: A scheme 

506 :raises BadScheme: if ``scheme` require no salt 

507 """ 

508 if scheme not in cls.schemes_salt: 

509 cls._raise_bad_scheme( 

510 scheme, 

511 cls.schemes_salt, 

512 "The scheme %r is only valid without a salt. Valide schemes with salt are %s." 

513 ) 

514 

515 @classmethod 

516 def _test_scheme_nosalt(cls, scheme): 

517 """ 

518 Test if the scheme need no salt or raise BadScheme 

519 

520 :param bytes scheme: A scheme 

521 :raises BadScheme: if ``scheme` require a salt 

522 """ 

523 if scheme not in cls.schemes_nosalt: 

524 cls._raise_bad_scheme( 

525 scheme, 

526 cls.schemes_nosalt, 

527 "The scheme %r is only valid with a salt. Valide schemes without salt are %s." 

528 ) 

529 

530 @classmethod 

531 def hash(cls, scheme, password, salt=None, charset="utf8"): 

532 """ 

533 Hash ``password`` with ``scheme`` using ``salt``. 

534 This three variable beeing encoded in ``charset``. 

535 

536 :param bytes scheme: A valid scheme 

537 :param bytes password: A byte string to hash using ``scheme`` 

538 :param bytes salt: An optional salt to use if ``scheme`` requires any 

539 :param str charset: The encoding of ``scheme``, ``password`` and ``salt`` 

540 :return: The hashed password encoded with ``charset`` 

541 :rtype: bytes 

542 """ 

543 scheme = scheme.upper() 

544 cls._test_scheme(scheme) 

545 if salt is None or salt == b"": 

546 salt = b"" 

547 cls._test_scheme_nosalt(scheme) 

548 else: 

549 cls._test_scheme_salt(scheme) 

550 try: 

551 return scheme + base64.b64encode( 

552 cls._schemes_to_hash[scheme](password + salt).digest() + salt 

553 ) 

554 except KeyError: 

555 if six.PY3: 

556 password = password.decode(charset) 

557 salt = salt.decode(charset) 

558 if not crypt_salt_is_valid(salt): 

559 raise cls.BadSalt("System crypt implementation do not support the salt %r" % salt) 

560 hashed_password = crypt.crypt(password, salt) 

561 if six.PY3: 

562 hashed_password = hashed_password.encode(charset) 

563 return scheme + hashed_password 

564 

565 @classmethod 

566 def get_scheme(cls, hashed_passord): 

567 """ 

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

569 

570 :param bytes hashed_passord: A hashed password 

571 :return: The scheme used by the hashed password 

572 :rtype: bytes 

573 :raises BadHash: if no valid scheme is found within ``hashed_passord`` 

574 """ 

575 if not hashed_passord[0] == b'{'[0] or b'}' not in hashed_passord: 

576 raise cls.BadHash("%r should start with the scheme enclosed with { }" % hashed_passord) 

577 scheme = hashed_passord.split(b'}', 1)[0] 

578 scheme = scheme.upper() + b"}" 

579 return scheme 

580 

581 @classmethod 

582 def get_salt(cls, hashed_passord): 

583 """ 

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

585 

586 :param bytes hashed_passord: A hashed password 

587 :return: The salt used by the hashed password (empty if no salt is used) 

588 :rtype: bytes 

589 :raises BadHash: if no valid scheme is found within ``hashed_passord`` or if the 

590 hashed password is too short for the scheme found. 

591 """ 

592 scheme = cls.get_scheme(hashed_passord) 

593 cls._test_scheme(scheme) 

594 if scheme in cls.schemes_nosalt: 

595 return b"" 

596 elif scheme == b'{CRYPT}': 

597 if b'$' in hashed_passord: 597 ↛ 599line 597 didn't jump to line 599, because the condition on line 597 was never false

598 return b'$'.join(hashed_passord.split(b'$', 3)[:-1])[len(scheme):] 

599 return hashed_passord.split(b'}', 1)[-1] 

600 else: 

601 try: 

602 hashed_passord = base64.b64decode(hashed_passord[len(scheme):]) 

603 except (TypeError, binascii.Error) as error: 

604 raise cls.BadHash("Bad base64: %s" % error) 

605 if len(hashed_passord) < cls._schemes_to_len[scheme]: 

606 raise cls.BadHash("Hash too short for the scheme %s" % scheme) 

607 return hashed_passord[cls._schemes_to_len[scheme]:] 

608 

609 

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

611 """ 

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

613 assuming the encoding is ``charset``. 

614 

615 :param str method: on of ``"crypt"``, ``"ldap"``, ``"hex_md5"``, ``"hex_sha1"``, 

616 ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``, ``"hex_sha512"``, ``"plain"`` 

617 :param password: The user inputed password 

618 :type password: :obj:`str` or :obj:`unicode` 

619 :param hashed_password: The hashed password as stored in the database 

620 :type hashed_password: :obj:`str` or :obj:`unicode` 

621 :param str charset: The used char encoding (also used internally, so it must be valid for 

622 the charset used by ``password`` when it was initially ) 

623 :return: True if ``password`` match ``hashed_password`` using ``method``, 

624 ``False`` otherwise 

625 :rtype: bool 

626 """ 

627 if not isinstance(password, six.binary_type): 

628 password = password.encode(charset) 

629 if not isinstance(hashed_password, six.binary_type): 

630 hashed_password = hashed_password.encode(charset) 

631 if method == "plain": 

632 return password == hashed_password 

633 elif method == "crypt": 

634 if hashed_password.startswith(b'$'): 

635 salt = b'$'.join(hashed_password.split(b'$', 3)[:-1]) 

636 elif hashed_password.startswith(b'_'): # pragma: no cover old BSD format not supported 

637 salt = hashed_password[:9] 

638 else: 

639 salt = hashed_password[:2] 

640 if six.PY3: 

641 password = password.decode(charset) 

642 salt = salt.decode(charset) 

643 hashed_password = hashed_password.decode(charset) 

644 if not crypt_salt_is_valid(salt): 

645 raise ValueError("System crypt implementation do not support the salt %r" % salt) 

646 crypted_password = crypt.crypt(password, salt) 

647 return crypted_password == hashed_password 

648 elif method == "ldap": 

649 scheme = LdapHashUserPassword.get_scheme(hashed_password) 

650 salt = LdapHashUserPassword.get_salt(hashed_password) 

651 return LdapHashUserPassword.hash(scheme, password, salt, charset=charset) == hashed_password 

652 elif ( 

653 method.startswith("hex_") and 

654 method[4:] in {"md5", "sha1", "sha224", "sha256", "sha384", "sha512"} 

655 ): 

656 return getattr( 

657 hashlib, 

658 method[4:] 

659 )(password).hexdigest().encode("ascii") == hashed_password.lower() 

660 else: 

661 raise ValueError("Unknown password method check %r" % method) 

662 

663 

664def decode_version(version): 

665 """ 

666 decode a version string following version semantic http://semver.org/ input a tuple of int. 

667 It will work as long as we do not use pre release versions. 

668 

669 :param unicode version: A dotted version 

670 :return: A tuple a int 

671 :rtype: tuple 

672 """ 

673 return tuple(int(sub_version) for sub_version in version.split('.')) 

674 

675 

676def last_version(): 

677 """ 

678 Fetch the last version from pypi and return it. On successful fetch from pypi, the response 

679 is cached 24h, on error, it is cached 10 min. 

680 

681 :return: the last django-cas-server version 

682 :rtype: unicode 

683 """ 

684 try: 

685 last_update, version, success = last_version._cache 

686 except AttributeError: 

687 last_update = 0 

688 version = None 

689 success = False 

690 cache_delta = 24 * 3600 if success else 600 

691 if (time.time() - last_update) < cache_delta: 

692 return version 

693 else: 

694 try: 

695 req = requests.get(settings.CAS_NEW_VERSION_JSON_URL) 

696 data = json.loads(req.text) 

697 version = data["info"]["version"] 

698 last_version._cache = (time.time(), version, True) 

699 return version 

700 except ( 

701 KeyError, 

702 ValueError, 

703 requests.exceptions.RequestException 

704 ) as error: # pragma: no cover (should not happen unless pypi is not available) 

705 logger.error( 

706 "Unable to fetch %s: %s" % (settings.CAS_NEW_VERSION_JSON_URL, error) 

707 ) 

708 last_version._cache = (time.time(), version, False) 

709 

710 

711def dictfetchall(cursor): 

712 "Return all rows from a django cursor as a dict" 

713 columns = [col[0] for col in cursor.description] 

714 return [ 

715 dict(zip(columns, row)) 

716 for row in cursor.fetchall() 

717 ] 

718 

719 

720def logout_request(ticket): 

721 """ 

722 Forge a SLO logout request 

723 

724 :param unicode ticket: A ticket value 

725 :return: A SLO XML body request 

726 :rtype: unicode 

727 """ 

728 return u"""<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" 

729 ID="%(id)s" Version="2.0" IssueInstant="%(datetime)s"> 

730<saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"></saml:NameID> 

731<samlp:SessionIndex>%(ticket)s</samlp:SessionIndex> 

732</samlp:LogoutRequest>""" % { 

733 'id': gen_saml_id(), 

734 'datetime': timezone.now().isoformat(), 

735 'ticket': ticket 

736 } 

737 

738 

739def regexpr_validator(value): 

740 """ 

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

742 

743 :param unicode value: A regular expression to test 

744 :raises ValidationError: if ``value`` is not a valid regular expression 

745 """ 

746 try: 

747 re.compile(value) 

748 except re.error: 

749 raise ValidationError( 

750 _('"%(value)s" is not a valid regular expression'), 

751 params={'value': value} 

752 )