Coverage for cas_server/utils.py: 95%

301 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-13 11:30 +0000

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 def to_unicode(data): 

253 if isinstance(data, bytes): 

254 return data.decode('utf-8') 

255 else: 

256 return data 

257 

258 def to_bytes(data): 

259 if not isinstance(data, bytes): 

260 return data.encode('utf-8') 

261 else: 

262 return data 

263 

264 if six.PY3: 

265 url = to_unicode(url) 

266 params = {to_unicode(key): to_unicode(value) for (key, value) in params.items()} 

267 else: 

268 url = to_bytes(url) 

269 params = {to_bytes(key): to_bytes(value) for (key, value) in params.items()} 

270 

271 url_parts = list(urlparse(url)) 

272 query = dict(parse_qsl(url_parts[4], keep_blank_values=True)) 

273 query.update(params) 

274 # make the params order deterministic 

275 query = list(query.items()) 

276 query.sort() 

277 url_query = urlencode(query) 

278 url_parts[4] = url_query 

279 url = urlunparse(url_parts) 

280 

281 if isinstance(url, bytes): 281 ↛ 282line 281 didn't jump to line 282, because the condition on line 281 was never true

282 url = url.decode('utf-8') 

283 return url 

284 

285 

286def unpack_nested_exception(error): 

287 """ 

288 If exception are stacked, return the first one 

289 

290 :param error: A python exception with possible exception embeded within 

291 :return: A python exception with no exception embeded within 

292 """ 

293 i = 0 

294 while True: 

295 if error.args[i:]: 

296 if isinstance(error.args[i], Exception): 

297 error = error.args[i] 

298 i = 0 

299 else: 

300 i += 1 

301 else: 

302 break 

303 return error 

304 

305 

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

307 """ 

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

309 

310 :param unicode prefix: An optional prefix (probably ST, PT, PGT or PGTIOU) 

311 :param int lg: The length of the generated ticket (with the prefix) 

312 :return: A randomlly generated ticket of length ``lg`` 

313 :rtype: unicode 

314 """ 

315 random_part = u''.join( 

316 random.choice( 

317 string.ascii_letters + string.digits 

318 ) for _ in range(lg - len(prefix or "") - 1) 

319 ) 

320 if prefix is not None: 

321 return u'%s-%s' % (prefix, random_part) 

322 else: 

323 return random_part 

324 

325 

326def gen_lt(): 

327 """ 

328 Generate a Login Ticket 

329 

330 :return: A ticket with prefix ``settings.CAS_LOGIN_TICKET_PREFIX`` and length 

331 ``settings.CAS_LT_LEN`` 

332 :rtype: unicode 

333 """ 

334 return _gen_ticket(settings.CAS_LOGIN_TICKET_PREFIX, settings.CAS_LT_LEN) 

335 

336 

337def gen_st(): 

338 """ 

339 Generate a Service Ticket 

340 

341 :return: A ticket with prefix ``settings.CAS_SERVICE_TICKET_PREFIX`` and length 

342 ``settings.CAS_ST_LEN`` 

343 :rtype: unicode 

344 """ 

345 return _gen_ticket(settings.CAS_SERVICE_TICKET_PREFIX, settings.CAS_ST_LEN) 

346 

347 

348def gen_pt(): 

349 """ 

350 Generate a Proxy Ticket 

351 

352 :return: A ticket with prefix ``settings.CAS_PROXY_TICKET_PREFIX`` and length 

353 ``settings.CAS_PT_LEN`` 

354 :rtype: unicode 

355 """ 

356 return _gen_ticket(settings.CAS_PROXY_TICKET_PREFIX, settings.CAS_PT_LEN) 

357 

358 

359def gen_pgt(): 

360 """ 

361 Generate a Proxy Granting Ticket 

362 

363 :return: A ticket with prefix ``settings.CAS_PROXY_GRANTING_TICKET_PREFIX`` and length 

364 ``settings.CAS_PGT_LEN`` 

365 :rtype: unicode 

366 """ 

367 return _gen_ticket(settings.CAS_PROXY_GRANTING_TICKET_PREFIX, settings.CAS_PGT_LEN) 

368 

369 

370def gen_pgtiou(): 

371 """ 

372 Generate a Proxy Granting Ticket IOU 

373 

374 :return: A ticket with prefix ``settings.CAS_PROXY_GRANTING_TICKET_IOU_PREFIX`` and length 

375 ``settings.CAS_PGTIOU_LEN`` 

376 :rtype: unicode 

377 """ 

378 return _gen_ticket(settings.CAS_PROXY_GRANTING_TICKET_IOU_PREFIX, settings.CAS_PGTIOU_LEN) 

379 

380 

381def gen_saml_id(): 

382 """ 

383 Generate an saml id 

384 

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

386 :rtype: unicode 

387 """ 

388 return _gen_ticket() 

389 

390 

391def get_tuple(nuplet, index, default=None): 

392 """ 

393 :param tuple nuplet: A tuple 

394 :param int index: An index 

395 :param default: An optional default value 

396 :return: ``nuplet[index]`` if defined, else ``default`` (possibly ``None``) 

397 """ 

398 if nuplet is None: 

399 return default 

400 try: 

401 return nuplet[index] 

402 except IndexError: 

403 return default 

404 

405 

406def crypt_salt_is_valid(salt): 

407 """ 

408 Validate a salt as crypt salt 

409 

410 :param str salt: a password salt 

411 :return: ``True`` if ``salt`` is a valid crypt salt on this system, ``False`` otherwise 

412 :rtype: bool 

413 """ 

414 if len(salt) < 2: 

415 return False 

416 else: 

417 if salt[0] == '$': 

418 if salt[1] == '$': 

419 return False 

420 else: 

421 if '$' not in salt[1:]: 

422 return False 

423 else: 

424 try: 

425 hashed = crypt.crypt("", salt) 

426 except OSError: 

427 return False 

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

429 return False 

430 else: 

431 return True 

432 else: 

433 return True 

434 

435 

436class LdapHashUserPassword(object): 

437 """ 

438 Class to deal with hashed password as defined at 

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

440 """ 

441 

442 #: valide schemes that require a salt 

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

444 #: valide sschemes that require no slat 

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

446 

447 #: map beetween scheme and hash function 

448 _schemes_to_hash = { 

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

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

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

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

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

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

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

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

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

458 b"{SHA512}": hashlib.sha512 

459 } 

460 

461 #: map between scheme and hash length 

462 _schemes_to_len = { 

463 b"{SMD5}": 16, 

464 b"{SSHA}": 20, 

465 b"{SSHA256}": 32, 

466 b"{SSHA384}": 48, 

467 b"{SSHA512}": 64, 

468 } 

469 

470 class BadScheme(ValueError): 

471 """ 

472 Error raised then the hash scheme is not in 

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

474 """ 

475 pass 

476 

477 class BadHash(ValueError): 

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

479 pass 

480 

481 class BadSalt(ValueError): 

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

483 pass 

484 

485 @classmethod 

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

487 """ 

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

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

490 

491 :param bytes scheme: A bad scheme 

492 :param list valid: A list a valid scheme 

493 :param str msg: The error template message 

494 :raises LdapHashUserPassword.BadScheme: always 

495 """ 

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

497 valid_schemes.sort() 

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

499 

500 @classmethod 

501 def _test_scheme(cls, scheme): 

502 """ 

503 Test if a scheme is valide or raise BadScheme 

504 

505 :param bytes scheme: A scheme 

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

507 """ 

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

509 cls._raise_bad_scheme( 

510 scheme, 

511 cls.schemes_salt | cls.schemes_nosalt, 

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

513 ) 

514 

515 @classmethod 

516 def _test_scheme_salt(cls, scheme): 

517 """ 

518 Test if the scheme need a salt or raise BadScheme 

519 

520 :param bytes scheme: A scheme 

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

522 """ 

523 if scheme not in cls.schemes_salt: 

524 cls._raise_bad_scheme( 

525 scheme, 

526 cls.schemes_salt, 

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

528 ) 

529 

530 @classmethod 

531 def _test_scheme_nosalt(cls, scheme): 

532 """ 

533 Test if the scheme need no salt or raise BadScheme 

534 

535 :param bytes scheme: A scheme 

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

537 """ 

538 if scheme not in cls.schemes_nosalt: 

539 cls._raise_bad_scheme( 

540 scheme, 

541 cls.schemes_nosalt, 

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

543 ) 

544 

545 @classmethod 

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

547 """ 

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

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

550 

551 :param bytes scheme: A valid scheme 

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

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

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

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

556 :rtype: bytes 

557 """ 

558 scheme = scheme.upper() 

559 cls._test_scheme(scheme) 

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

561 salt = b"" 

562 cls._test_scheme_nosalt(scheme) 

563 else: 

564 cls._test_scheme_salt(scheme) 

565 try: 

566 return scheme + base64.b64encode( 

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

568 ) 

569 except KeyError: 

570 if six.PY3: 

571 password = password.decode(charset) 

572 salt = salt.decode(charset) 

573 if not crypt_salt_is_valid(salt): 

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

575 hashed_password = crypt.crypt(password, salt) 

576 if six.PY3: 

577 hashed_password = hashed_password.encode(charset) 

578 return scheme + hashed_password 

579 

580 @classmethod 

581 def get_scheme(cls, hashed_passord): 

582 """ 

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

584 

585 :param bytes hashed_passord: A hashed password 

586 :return: The scheme used by the hashed password 

587 :rtype: bytes 

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

589 """ 

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

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

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

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

594 return scheme 

595 

596 @classmethod 

597 def get_salt(cls, hashed_passord): 

598 """ 

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

600 

601 :param bytes hashed_passord: A hashed password 

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

603 :rtype: bytes 

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

605 hashed password is too short for the scheme found. 

606 """ 

607 scheme = cls.get_scheme(hashed_passord) 

608 cls._test_scheme(scheme) 

609 if scheme in cls.schemes_nosalt: 

610 return b"" 

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

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

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

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

615 else: 

616 try: 

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

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

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

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

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

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

623 

624 

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

626 """ 

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

628 assuming the encoding is ``charset``. 

629 

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

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

632 :param password: The user inputed password 

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

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

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

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

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

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

639 ``False`` otherwise 

640 :rtype: bool 

641 """ 

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

643 password = password.encode(charset) 

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

645 hashed_password = hashed_password.encode(charset) 

646 if method == "plain": 

647 return password == hashed_password 

648 elif method == "crypt": 

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

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

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

652 salt = hashed_password[:9] 

653 else: 

654 salt = hashed_password[:2] 

655 if six.PY3: 

656 password = password.decode(charset) 

657 salt = salt.decode(charset) 

658 hashed_password = hashed_password.decode(charset) 

659 if not crypt_salt_is_valid(salt): 

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

661 crypted_password = crypt.crypt(password, salt) 

662 return crypted_password == hashed_password 

663 elif method == "ldap": 

664 scheme = LdapHashUserPassword.get_scheme(hashed_password) 

665 salt = LdapHashUserPassword.get_salt(hashed_password) 

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

667 elif ( 

668 method.startswith("hex_") and 

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

670 ): 

671 return getattr( 

672 hashlib, 

673 method[4:] 

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

675 else: 

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

677 

678 

679def decode_version(version): 

680 """ 

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

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

683 

684 :param unicode version: A dotted version 

685 :return: A tuple a int 

686 :rtype: tuple 

687 """ 

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

689 

690 

691def last_version(): 

692 """ 

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

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

695 

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

697 :rtype: unicode 

698 """ 

699 try: 

700 last_update, version, success = last_version._cache 

701 except AttributeError: 

702 last_update = 0 

703 version = None 

704 success = False 

705 cache_delta = 24 * 3600 if success else 600 

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

707 return version 

708 else: 

709 try: 

710 req = requests.get(settings.CAS_NEW_VERSION_JSON_URL) 

711 data = json.loads(req.text) 

712 version = data["info"]["version"] 

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

714 return version 

715 except ( 

716 KeyError, 

717 ValueError, 

718 requests.exceptions.RequestException 

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

720 logger.error( 

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

722 ) 

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

724 

725 

726def dictfetchall(cursor): 

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

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

729 return [ 

730 dict(zip(columns, row)) 

731 for row in cursor.fetchall() 

732 ] 

733 

734 

735def logout_request(ticket): 

736 """ 

737 Forge a SLO logout request 

738 

739 :param unicode ticket: A ticket value 

740 :return: A SLO XML body request 

741 :rtype: unicode 

742 """ 

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

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

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

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

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

748 'id': gen_saml_id(), 

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

750 'ticket': ticket 

751 } 

752 

753 

754def regexpr_validator(value): 

755 """ 

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

757 

758 :param unicode value: A regular expression to test 

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

760 """ 

761 try: 

762 re.compile(value) 

763 except re.error: 

764 raise ValidationError( 

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

766 params={'value': value} 

767 )