1"""tests for passlib.context
2
3this file is a clone of the 1.5 test_context.py,
4containing the tests using the legacy CryptPolicy api.
5it's being preserved here to ensure the old api doesn't break
6(until Passlib 1.8, when this and the legacy api will be removed).
7"""
8#=============================================================================
9# imports
10#=============================================================================
11from __future__ import with_statement
12# core
13from logging import getLogger
14import os
15import warnings
16# site
17try:
18    from pkg_resources import resource_filename
19except ImportError:
20    resource_filename = None
21# pkg
22from passlib import hash
23from passlib.context import CryptContext, CryptPolicy, LazyCryptContext
24from passlib.utils import to_bytes, to_unicode
25import passlib.utils.handlers as uh
26from passlib.tests.utils import TestCase, set_file
27from passlib.registry import (register_crypt_handler_path,
28                        _has_crypt_handler as has_crypt_handler,
29                        _unload_handler_name as unload_handler_name,
30                        )
31# module
32log = getLogger(__name__)
33
34#=============================================================================
35#
36#=============================================================================
37class CryptPolicyTest(TestCase):
38    """test CryptPolicy object"""
39
40    # TODO: need to test user categories w/in all this
41
42    descriptionPrefix = "CryptPolicy"
43
44    #===================================================================
45    # sample crypt policies used for testing
46    #===================================================================
47
48    #---------------------------------------------------------------
49    # sample 1 - average config file
50    #---------------------------------------------------------------
51    # NOTE: copy of this is stored in file passlib/tests/sample_config_1s.cfg
52    sample_config_1s = """\
53[passlib]
54schemes = des_crypt, md5_crypt, bsdi_crypt, sha512_crypt
55default = md5_crypt
56all.vary_rounds = 10%%
57bsdi_crypt.max_rounds = 30000
58bsdi_crypt.default_rounds = 25000
59sha512_crypt.max_rounds = 50000
60sha512_crypt.min_rounds = 40000
61"""
62    sample_config_1s_path = os.path.abspath(os.path.join(
63        os.path.dirname(__file__), "sample_config_1s.cfg"))
64    if not os.path.exists(sample_config_1s_path) and resource_filename:
65        # in case we're zipped up in an egg.
66        sample_config_1s_path = resource_filename("passlib.tests",
67                                                  "sample_config_1s.cfg")
68
69    # make sure sample_config_1s uses \n linesep - tests rely on this
70    assert sample_config_1s.startswith("[passlib]\nschemes")
71
72    sample_config_1pd = dict(
73        schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"],
74        default = "md5_crypt",
75        # NOTE: not maintaining backwards compat for rendering to "10%"
76        all__vary_rounds = 0.1,
77        bsdi_crypt__max_rounds = 30000,
78        bsdi_crypt__default_rounds = 25000,
79        sha512_crypt__max_rounds = 50000,
80        sha512_crypt__min_rounds = 40000,
81    )
82
83    sample_config_1pid = {
84        "schemes": "des_crypt, md5_crypt, bsdi_crypt, sha512_crypt",
85        "default": "md5_crypt",
86        # NOTE: not maintaining backwards compat for rendering to "10%"
87        "all.vary_rounds": 0.1,
88        "bsdi_crypt.max_rounds": 30000,
89        "bsdi_crypt.default_rounds": 25000,
90        "sha512_crypt.max_rounds": 50000,
91        "sha512_crypt.min_rounds": 40000,
92    }
93
94    sample_config_1prd = dict(
95        schemes = [ hash.des_crypt, hash.md5_crypt, hash.bsdi_crypt, hash.sha512_crypt],
96        default = "md5_crypt", # NOTE: passlib <= 1.5 was handler obj.
97        # NOTE: not maintaining backwards compat for rendering to "10%"
98        all__vary_rounds = 0.1,
99        bsdi_crypt__max_rounds = 30000,
100        bsdi_crypt__default_rounds = 25000,
101        sha512_crypt__max_rounds = 50000,
102        sha512_crypt__min_rounds = 40000,
103    )
104
105    #---------------------------------------------------------------
106    # sample 2 - partial policy & result of overlay on sample 1
107    #---------------------------------------------------------------
108    sample_config_2s = """\
109[passlib]
110bsdi_crypt.min_rounds = 29000
111bsdi_crypt.max_rounds = 35000
112bsdi_crypt.default_rounds = 31000
113sha512_crypt.min_rounds = 45000
114"""
115
116    sample_config_2pd = dict(
117        # using this to test full replacement of existing options
118        bsdi_crypt__min_rounds = 29000,
119        bsdi_crypt__max_rounds = 35000,
120        bsdi_crypt__default_rounds = 31000,
121        # using this to test partial replacement of existing options
122        sha512_crypt__min_rounds=45000,
123    )
124
125    sample_config_12pd = dict(
126        schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"],
127        default = "md5_crypt",
128        # NOTE: not maintaining backwards compat for rendering to "10%"
129        all__vary_rounds = 0.1,
130        bsdi_crypt__min_rounds = 29000,
131        bsdi_crypt__max_rounds = 35000,
132        bsdi_crypt__default_rounds = 31000,
133        sha512_crypt__max_rounds = 50000,
134        sha512_crypt__min_rounds=45000,
135    )
136
137    #---------------------------------------------------------------
138    # sample 3 - just changing default
139    #---------------------------------------------------------------
140    sample_config_3pd = dict(
141        default="sha512_crypt",
142    )
143
144    sample_config_123pd = dict(
145        schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"],
146        default = "sha512_crypt",
147        # NOTE: not maintaining backwards compat for rendering to "10%"
148        all__vary_rounds = 0.1,
149        bsdi_crypt__min_rounds = 29000,
150        bsdi_crypt__max_rounds = 35000,
151        bsdi_crypt__default_rounds = 31000,
152        sha512_crypt__max_rounds = 50000,
153        sha512_crypt__min_rounds=45000,
154    )
155
156    #---------------------------------------------------------------
157    # sample 4 - category specific
158    #---------------------------------------------------------------
159    sample_config_4s = """
160[passlib]
161schemes = sha512_crypt
162all.vary_rounds = 10%%
163default.sha512_crypt.max_rounds = 20000
164admin.all.vary_rounds = 5%%
165admin.sha512_crypt.max_rounds = 40000
166"""
167
168    sample_config_4pd = dict(
169        schemes = [ "sha512_crypt" ],
170        # NOTE: not maintaining backwards compat for rendering to "10%"
171        all__vary_rounds = 0.1,
172        sha512_crypt__max_rounds = 20000,
173        # NOTE: not maintaining backwards compat for rendering to "5%"
174        admin__all__vary_rounds = 0.05,
175        admin__sha512_crypt__max_rounds = 40000,
176        )
177
178    #---------------------------------------------------------------
179    # sample 5 - to_string & deprecation testing
180    #---------------------------------------------------------------
181    sample_config_5s = sample_config_1s + """\
182deprecated = des_crypt
183admin__context__deprecated = des_crypt, bsdi_crypt
184"""
185
186    sample_config_5pd = sample_config_1pd.copy()
187    sample_config_5pd.update(
188        deprecated = [ "des_crypt" ],
189        admin__context__deprecated = [ "des_crypt", "bsdi_crypt" ],
190    )
191
192    sample_config_5pid = sample_config_1pid.copy()
193    sample_config_5pid.update({
194        "deprecated": "des_crypt",
195        "admin.context.deprecated": "des_crypt, bsdi_crypt",
196    })
197
198    sample_config_5prd = sample_config_1prd.copy()
199    sample_config_5prd.update({
200        # XXX: should deprecated return the actual handlers in this case?
201        #      would have to modify how policy stores info, for one.
202        "deprecated": ["des_crypt"],
203        "admin__context__deprecated": ["des_crypt", "bsdi_crypt"],
204    })
205
206    #===================================================================
207    # constructors
208    #===================================================================
209    def setUp(self):
210        TestCase.setUp(self)
211        warnings.filterwarnings("ignore",
212                                r"The CryptPolicy class has been deprecated")
213        warnings.filterwarnings("ignore",
214                                r"the method.*hash_needs_update.*is deprecated")
215        warnings.filterwarnings("ignore", "The 'all' scheme is deprecated.*")
216        warnings.filterwarnings("ignore", "bsdi_crypt rounds should be odd")
217
218    def test_00_constructor(self):
219        """test CryptPolicy() constructor"""
220        policy = CryptPolicy(**self.sample_config_1pd)
221        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
222
223        policy = CryptPolicy(self.sample_config_1pd)
224        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
225
226        self.assertRaises(TypeError, CryptPolicy, {}, {})
227        self.assertRaises(TypeError, CryptPolicy, {}, dummy=1)
228
229        # check key with too many separators is rejected
230        self.assertRaises(TypeError, CryptPolicy,
231            schemes = [ "des_crypt", "md5_crypt", "bsdi_crypt", "sha512_crypt"],
232            bad__key__bsdi_crypt__max_rounds = 30000,
233            )
234
235        # check nameless handler rejected
236        class nameless(uh.StaticHandler):
237            name = None
238        self.assertRaises(ValueError, CryptPolicy, schemes=[nameless])
239
240        # check scheme must be name or crypt handler
241        self.assertRaises(TypeError, CryptPolicy, schemes=[uh.StaticHandler])
242
243        # check name conflicts are rejected
244        class dummy_1(uh.StaticHandler):
245            name = 'dummy_1'
246        self.assertRaises(KeyError, CryptPolicy, schemes=[dummy_1, dummy_1])
247
248        # with unknown deprecated value
249        self.assertRaises(KeyError, CryptPolicy,
250                          schemes=['des_crypt'],
251                          deprecated=['md5_crypt'])
252
253        # with unknown default value
254        self.assertRaises(KeyError, CryptPolicy,
255                          schemes=['des_crypt'],
256                          default='md5_crypt')
257
258    def test_01_from_path_simple(self):
259        """test CryptPolicy.from_path() constructor"""
260        # NOTE: this is separate so it can also run under GAE
261
262        # test preset stored in existing file
263        path = self.sample_config_1s_path
264        policy = CryptPolicy.from_path(path)
265        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
266
267        # test if path missing
268        self.assertRaises(EnvironmentError, CryptPolicy.from_path, path + 'xxx')
269
270    def test_01_from_path(self):
271        """test CryptPolicy.from_path() constructor with encodings"""
272        path = self.mktemp()
273
274        # test "\n" linesep
275        set_file(path, self.sample_config_1s)
276        policy = CryptPolicy.from_path(path)
277        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
278
279        # test "\r\n" linesep
280        set_file(path, self.sample_config_1s.replace("\n","\r\n"))
281        policy = CryptPolicy.from_path(path)
282        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
283
284        # test with custom encoding
285        uc2 = to_bytes(self.sample_config_1s, "utf-16", source_encoding="utf-8")
286        set_file(path, uc2)
287        policy = CryptPolicy.from_path(path, encoding="utf-16")
288        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
289
290    def test_02_from_string(self):
291        """test CryptPolicy.from_string() constructor"""
292        # test "\n" linesep
293        policy = CryptPolicy.from_string(self.sample_config_1s)
294        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
295
296        # test "\r\n" linesep
297        policy = CryptPolicy.from_string(
298            self.sample_config_1s.replace("\n","\r\n"))
299        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
300
301        # test with unicode
302        data = to_unicode(self.sample_config_1s)
303        policy = CryptPolicy.from_string(data)
304        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
305
306        # test with non-ascii-compatible encoding
307        uc2 = to_bytes(self.sample_config_1s, "utf-16", source_encoding="utf-8")
308        policy = CryptPolicy.from_string(uc2, encoding="utf-16")
309        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
310
311        # test category specific options
312        policy = CryptPolicy.from_string(self.sample_config_4s)
313        self.assertEqual(policy.to_dict(), self.sample_config_4pd)
314
315    def test_03_from_source(self):
316        """test CryptPolicy.from_source() constructor"""
317        # pass it a path
318        policy = CryptPolicy.from_source(self.sample_config_1s_path)
319        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
320
321        # pass it a string
322        policy = CryptPolicy.from_source(self.sample_config_1s)
323        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
324
325        # pass it a dict (NOTE: make a copy to detect in-place modifications)
326        policy = CryptPolicy.from_source(self.sample_config_1pd.copy())
327        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
328
329        # pass it existing policy
330        p2 = CryptPolicy.from_source(policy)
331        self.assertIs(policy, p2)
332
333        # pass it something wrong
334        self.assertRaises(TypeError, CryptPolicy.from_source, 1)
335        self.assertRaises(TypeError, CryptPolicy.from_source, [])
336
337    def test_04_from_sources(self):
338        """test CryptPolicy.from_sources() constructor"""
339
340        # pass it empty list
341        self.assertRaises(ValueError, CryptPolicy.from_sources, [])
342
343        # pass it one-element list
344        policy = CryptPolicy.from_sources([self.sample_config_1s])
345        self.assertEqual(policy.to_dict(), self.sample_config_1pd)
346
347        # pass multiple sources
348        policy = CryptPolicy.from_sources(
349            [
350            self.sample_config_1s_path,
351            self.sample_config_2s,
352            self.sample_config_3pd,
353            ])
354        self.assertEqual(policy.to_dict(), self.sample_config_123pd)
355
356    def test_05_replace(self):
357        """test CryptPolicy.replace() constructor"""
358
359        p1 = CryptPolicy(**self.sample_config_1pd)
360
361        # check overlaying sample 2
362        p2 = p1.replace(**self.sample_config_2pd)
363        self.assertEqual(p2.to_dict(), self.sample_config_12pd)
364
365        # check repeating overlay makes no change
366        p2b = p2.replace(**self.sample_config_2pd)
367        self.assertEqual(p2b.to_dict(), self.sample_config_12pd)
368
369        # check overlaying sample 3
370        p3 = p2.replace(self.sample_config_3pd)
371        self.assertEqual(p3.to_dict(), self.sample_config_123pd)
372
373    def test_06_forbidden(self):
374        """test CryptPolicy() forbidden kwds"""
375
376        # salt not allowed to be set
377        self.assertRaises(KeyError, CryptPolicy,
378            schemes=["des_crypt"],
379            des_crypt__salt="xx",
380        )
381        self.assertRaises(KeyError, CryptPolicy,
382            schemes=["des_crypt"],
383            all__salt="xx",
384        )
385
386        # schemes not allowed for category
387        self.assertRaises(KeyError, CryptPolicy,
388            schemes=["des_crypt"],
389            user__context__schemes=["md5_crypt"],
390        )
391
392    #===================================================================
393    # reading
394    #===================================================================
395    def test_10_has_schemes(self):
396        """test has_schemes() method"""
397
398        p1 = CryptPolicy(**self.sample_config_1pd)
399        self.assertTrue(p1.has_schemes())
400
401        p3 = CryptPolicy(**self.sample_config_3pd)
402        self.assertTrue(not p3.has_schemes())
403
404    def test_11_iter_handlers(self):
405        """test iter_handlers() method"""
406
407        p1 = CryptPolicy(**self.sample_config_1pd)
408        s = self.sample_config_1prd['schemes']
409        self.assertEqual(list(p1.iter_handlers()), s)
410
411        p3 = CryptPolicy(**self.sample_config_3pd)
412        self.assertEqual(list(p3.iter_handlers()), [])
413
414    def test_12_get_handler(self):
415        """test get_handler() method"""
416
417        p1 = CryptPolicy(**self.sample_config_1pd)
418
419        # check by name
420        self.assertIs(p1.get_handler("bsdi_crypt"), hash.bsdi_crypt)
421
422        # check by missing name
423        self.assertIs(p1.get_handler("sha256_crypt"), None)
424        self.assertRaises(KeyError, p1.get_handler, "sha256_crypt", required=True)
425
426        # check default
427        self.assertIs(p1.get_handler(), hash.md5_crypt)
428
429    def test_13_get_options(self):
430        """test get_options() method"""
431
432        p12 = CryptPolicy(**self.sample_config_12pd)
433
434        self.assertEqual(p12.get_options("bsdi_crypt"),dict(
435            # NOTE: not maintaining backwards compat for rendering to "10%"
436            vary_rounds = 0.1,
437            min_rounds = 29000,
438            max_rounds = 35000,
439            default_rounds = 31000,
440        ))
441
442        self.assertEqual(p12.get_options("sha512_crypt"),dict(
443            # NOTE: not maintaining backwards compat for rendering to "10%"
444            vary_rounds = 0.1,
445            min_rounds = 45000,
446            max_rounds = 50000,
447        ))
448
449        p4 = CryptPolicy.from_string(self.sample_config_4s)
450        self.assertEqual(p4.get_options("sha512_crypt"), dict(
451            # NOTE: not maintaining backwards compat for rendering to "10%"
452            vary_rounds=0.1,
453            max_rounds=20000,
454        ))
455
456        self.assertEqual(p4.get_options("sha512_crypt", "user"), dict(
457            # NOTE: not maintaining backwards compat for rendering to "10%"
458            vary_rounds=0.1,
459            max_rounds=20000,
460        ))
461
462        self.assertEqual(p4.get_options("sha512_crypt", "admin"), dict(
463            # NOTE: not maintaining backwards compat for rendering to "5%"
464            vary_rounds=0.05,
465            max_rounds=40000,
466        ))
467
468    def test_14_handler_is_deprecated(self):
469        """test handler_is_deprecated() method"""
470        pa = CryptPolicy(**self.sample_config_1pd)
471        pb = CryptPolicy(**self.sample_config_5pd)
472
473        self.assertFalse(pa.handler_is_deprecated("des_crypt"))
474        self.assertFalse(pa.handler_is_deprecated(hash.bsdi_crypt))
475        self.assertFalse(pa.handler_is_deprecated("sha512_crypt"))
476
477        self.assertTrue(pb.handler_is_deprecated("des_crypt"))
478        self.assertFalse(pb.handler_is_deprecated(hash.bsdi_crypt))
479        self.assertFalse(pb.handler_is_deprecated("sha512_crypt"))
480
481        # check categories as well
482        self.assertTrue(pb.handler_is_deprecated("des_crypt", "user"))
483        self.assertFalse(pb.handler_is_deprecated("bsdi_crypt", "user"))
484        self.assertTrue(pb.handler_is_deprecated("des_crypt", "admin"))
485        self.assertTrue(pb.handler_is_deprecated("bsdi_crypt", "admin"))
486
487        # check deprecation is overridden per category
488        pc = CryptPolicy(
489            schemes=["md5_crypt", "des_crypt"],
490            deprecated=["md5_crypt"],
491            user__context__deprecated=["des_crypt"],
492        )
493        self.assertTrue(pc.handler_is_deprecated("md5_crypt"))
494        self.assertFalse(pc.handler_is_deprecated("des_crypt"))
495        self.assertFalse(pc.handler_is_deprecated("md5_crypt", "user"))
496        self.assertTrue(pc.handler_is_deprecated("des_crypt", "user"))
497
498    def test_15_min_verify_time(self):
499        """test get_min_verify_time() method"""
500        # silence deprecation warnings for min verify time
501        warnings.filterwarnings("ignore", category=DeprecationWarning)
502
503        pa = CryptPolicy()
504        self.assertEqual(pa.get_min_verify_time(), 0)
505        self.assertEqual(pa.get_min_verify_time('admin'), 0)
506
507        pb = pa.replace(min_verify_time=.1)
508        self.assertEqual(pb.get_min_verify_time(), 0)
509        self.assertEqual(pb.get_min_verify_time('admin'), 0)
510
511    #===================================================================
512    # serialization
513    #===================================================================
514    def test_20_iter_config(self):
515        """test iter_config() method"""
516        p5 = CryptPolicy(**self.sample_config_5pd)
517        self.assertEqual(dict(p5.iter_config()), self.sample_config_5pd)
518        self.assertEqual(dict(p5.iter_config(resolve=True)), self.sample_config_5prd)
519        self.assertEqual(dict(p5.iter_config(ini=True)), self.sample_config_5pid)
520
521    def test_21_to_dict(self):
522        """test to_dict() method"""
523        p5 = CryptPolicy(**self.sample_config_5pd)
524        self.assertEqual(p5.to_dict(), self.sample_config_5pd)
525        self.assertEqual(p5.to_dict(resolve=True), self.sample_config_5prd)
526
527    def test_22_to_string(self):
528        """test to_string() method"""
529        pa = CryptPolicy(**self.sample_config_5pd)
530        s = pa.to_string() # NOTE: can't compare string directly, ordering etc may not match
531        pb = CryptPolicy.from_string(s)
532        self.assertEqual(pb.to_dict(), self.sample_config_5pd)
533
534        s = pa.to_string(encoding="latin-1")
535        self.assertIsInstance(s, bytes)
536
537    #===================================================================
538    #
539    #===================================================================
540
541#=============================================================================
542# CryptContext
543#=============================================================================
544class CryptContextTest(TestCase):
545    """test CryptContext class"""
546    descriptionPrefix = "CryptContext"
547
548    def setUp(self):
549        TestCase.setUp(self)
550        warnings.filterwarnings("ignore",
551                                r"CryptContext\(\)\.replace\(\) has been deprecated.*")
552        warnings.filterwarnings("ignore",
553                                r"The CryptContext ``policy`` keyword has been deprecated.*")
554        warnings.filterwarnings("ignore", ".*(CryptPolicy|context\.policy).*(has|have) been deprecated.*")
555        warnings.filterwarnings("ignore",
556                                r"the method.*hash_needs_update.*is deprecated")
557
558    #===================================================================
559    # constructor
560    #===================================================================
561    def test_00_constructor(self):
562        """test constructor"""
563        # create crypt context using handlers
564        cc = CryptContext([hash.md5_crypt, hash.bsdi_crypt, hash.des_crypt])
565        c,b,a = cc.policy.iter_handlers()
566        self.assertIs(a, hash.des_crypt)
567        self.assertIs(b, hash.bsdi_crypt)
568        self.assertIs(c, hash.md5_crypt)
569
570        # create context using names
571        cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"])
572        c,b,a = cc.policy.iter_handlers()
573        self.assertIs(a, hash.des_crypt)
574        self.assertIs(b, hash.bsdi_crypt)
575        self.assertIs(c, hash.md5_crypt)
576
577        # policy kwd
578        policy = cc.policy
579        cc = CryptContext(policy=policy)
580        self.assertEqual(cc.to_dict(), policy.to_dict())
581
582        cc = CryptContext(policy=policy, default="bsdi_crypt")
583        self.assertNotEqual(cc.to_dict(), policy.to_dict())
584        self.assertEqual(cc.to_dict(), dict(schemes=["md5_crypt","bsdi_crypt","des_crypt"],
585                                            default="bsdi_crypt"))
586
587        self.assertRaises(TypeError, setattr, cc, 'policy', None)
588        self.assertRaises(TypeError, CryptContext, policy='x')
589
590    def test_01_replace(self):
591        """test replace()"""
592
593        cc = CryptContext(["md5_crypt", "bsdi_crypt", "des_crypt"])
594        self.assertIs(cc.policy.get_handler(), hash.md5_crypt)
595
596        cc2 = cc.replace()
597        self.assertIsNot(cc2, cc)
598        # NOTE: was not able to maintain backward compatibility with this...
599        ##self.assertIs(cc2.policy, cc.policy)
600
601        cc3 = cc.replace(default="bsdi_crypt")
602        self.assertIsNot(cc3, cc)
603        # NOTE: was not able to maintain backward compatibility with this...
604        ##self.assertIs(cc3.policy, cc.policy)
605        self.assertIs(cc3.policy.get_handler(), hash.bsdi_crypt)
606
607    def test_02_no_handlers(self):
608        """test no handlers"""
609
610        # check constructor...
611        cc = CryptContext()
612        self.assertRaises(KeyError, cc.identify, 'hash', required=True)
613        self.assertRaises(KeyError, cc.hash, 'secret')
614        self.assertRaises(KeyError, cc.verify, 'secret', 'hash')
615
616        # check updating policy after the fact...
617        cc = CryptContext(['md5_crypt'])
618        p = CryptPolicy(schemes=[])
619        cc.policy = p
620
621        self.assertRaises(KeyError, cc.identify, 'hash', required=True)
622        self.assertRaises(KeyError, cc.hash, 'secret')
623        self.assertRaises(KeyError, cc.verify, 'secret', 'hash')
624
625    #===================================================================
626    # policy adaptation
627    #===================================================================
628    sample_policy_1 = dict(
629            schemes = [ "des_crypt", "md5_crypt", "phpass", "bsdi_crypt",
630                       "sha256_crypt"],
631            deprecated = [ "des_crypt", ],
632            default = "sha256_crypt",
633            bsdi_crypt__max_rounds = 30,
634            bsdi_crypt__default_rounds = 25,
635            bsdi_crypt__vary_rounds = 0,
636            sha256_crypt__max_rounds = 3000,
637            sha256_crypt__min_rounds = 2000,
638            sha256_crypt__default_rounds = 3000,
639            phpass__ident = "H",
640            phpass__default_rounds = 7,
641    )
642
643    def test_12_hash_needs_update(self):
644        """test hash_needs_update() method"""
645        cc = CryptContext(**self.sample_policy_1)
646
647        # check deprecated scheme
648        self.assertTrue(cc.hash_needs_update('9XXD4trGYeGJA'))
649        self.assertFalse(cc.hash_needs_update('$1$J8HC2RCr$HcmM.7NxB2weSvlw2FgzU0'))
650
651        # check min rounds
652        self.assertTrue(cc.hash_needs_update('$5$rounds=1999$jD81UCoo.zI.UETs$Y7qSTQ6mTiU9qZB4fRr43wRgQq4V.5AAf7F97Pzxey/'))
653        self.assertFalse(cc.hash_needs_update('$5$rounds=2000$228SSRje04cnNCaQ$YGV4RYu.5sNiBvorQDlO0WWQjyJVGKBcJXz3OtyQ2u8'))
654
655        # check max rounds
656        self.assertFalse(cc.hash_needs_update('$5$rounds=3000$fS9iazEwTKi7QPW4$VasgBC8FqlOvD7x2HhABaMXCTh9jwHclPA9j5YQdns.'))
657        self.assertTrue(cc.hash_needs_update('$5$rounds=3001$QlFHHifXvpFX4PLs$/0ekt7lSs/lOikSerQ0M/1porEHxYq7W/2hdFpxA3fA'))
658
659    #===================================================================
660    # border cases
661    #===================================================================
662    def test_30_nonstring_hash(self):
663        """test non-string hash values cause error"""
664        warnings.filterwarnings("ignore", ".*needs_update.*'scheme' keyword is deprecated.*")
665
666        #
667        # test hash=None or some other non-string causes TypeError
668        # and that explicit-scheme code path behaves the same.
669        #
670        cc = CryptContext(["des_crypt"])
671        for hash, kwds in [
672                (None, {}),
673                # NOTE: 'scheme' kwd is deprecated...
674                (None, {"scheme": "des_crypt"}),
675                (1, {}),
676                ((), {}),
677                ]:
678
679            self.assertRaises(TypeError, cc.hash_needs_update, hash, **kwds)
680
681        cc2 = CryptContext(["mysql323"])
682        self.assertRaises(TypeError, cc2.hash_needs_update, None)
683
684    #===================================================================
685    # eoc
686    #===================================================================
687
688#=============================================================================
689# LazyCryptContext
690#=============================================================================
691class dummy_2(uh.StaticHandler):
692    name = "dummy_2"
693
694class LazyCryptContextTest(TestCase):
695    descriptionPrefix = "LazyCryptContext"
696
697    def setUp(self):
698        TestCase.setUp(self)
699
700        # make sure this isn't registered before OR after
701        unload_handler_name("dummy_2")
702        self.addCleanup(unload_handler_name, "dummy_2")
703
704        # silence some warnings
705        warnings.filterwarnings("ignore",
706                                r"CryptContext\(\)\.replace\(\) has been deprecated")
707        warnings.filterwarnings("ignore", ".*(CryptPolicy|context\.policy).*(has|have) been deprecated.*")
708
709    def test_kwd_constructor(self):
710        """test plain kwds"""
711        self.assertFalse(has_crypt_handler("dummy_2"))
712        register_crypt_handler_path("dummy_2", "passlib.tests.test_context")
713
714        cc = LazyCryptContext(iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"])
715
716        self.assertFalse(has_crypt_handler("dummy_2", True))
717
718        self.assertTrue(cc.policy.handler_is_deprecated("des_crypt"))
719        self.assertEqual(cc.policy.schemes(), ["dummy_2", "des_crypt"])
720
721        self.assertTrue(has_crypt_handler("dummy_2", True))
722
723    def test_callable_constructor(self):
724        """test create_policy() hook, returning CryptPolicy"""
725        self.assertFalse(has_crypt_handler("dummy_2"))
726        register_crypt_handler_path("dummy_2", "passlib.tests.test_context")
727
728        def create_policy(flag=False):
729            self.assertTrue(flag)
730            return CryptPolicy(schemes=iter(["dummy_2", "des_crypt"]), deprecated=["des_crypt"])
731
732        cc = LazyCryptContext(create_policy=create_policy, flag=True)
733
734        self.assertFalse(has_crypt_handler("dummy_2", True))
735
736        self.assertTrue(cc.policy.handler_is_deprecated("des_crypt"))
737        self.assertEqual(cc.policy.schemes(), ["dummy_2", "des_crypt"])
738
739        self.assertTrue(has_crypt_handler("dummy_2", True))
740
741#=============================================================================
742# eof
743#=============================================================================
744