Changed passwords.py to use the correct account as acl checks now pass.
[metze/samba/wip.git] / source4 / dsdb / tests / python / passwords.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 # This tests the password changes over LDAP for AD implementations
4 #
5 # Copyright Matthias Dieter Wallnoefer 2010
6 #
7 # Notice: This tests will also work against Windows Server if the connection is
8 # secured enough (SASL with a minimum of 128 Bit encryption) - consider
9 # MS-ADTS 3.1.1.3.1.5
10
11 import optparse
12 import sys
13 import base64
14 import os
15
16 sys.path.append("bin/python")
17 import samba
18 samba.ensure_external_module("subunit", "subunit/python")
19 samba.ensure_external_module("testtools", "testtools")
20
21 import samba.getopt as options
22
23 from samba.auth import system_session
24 from samba.credentials import Credentials
25 from ldb import SCOPE_BASE, LdbError
26 from ldb import ERR_NO_SUCH_OBJECT, ERR_ATTRIBUTE_OR_VALUE_EXISTS
27 from ldb import ERR_UNWILLING_TO_PERFORM, ERR_INSUFFICIENT_ACCESS_RIGHTS
28 from ldb import ERR_NO_SUCH_ATTRIBUTE
29 from ldb import ERR_CONSTRAINT_VIOLATION
30 from ldb import Message, MessageElement, Dn
31 from ldb import FLAG_MOD_REPLACE, FLAG_MOD_DELETE
32 from samba import gensec
33 from samba.samdb import SamDB
34 import samba.tests
35 from subunit.run import SubunitTestRunner
36 import unittest
37
38 parser = optparse.OptionParser("passwords [options] <host>")
39 sambaopts = options.SambaOptions(parser)
40 parser.add_option_group(sambaopts)
41 parser.add_option_group(options.VersionOptions(parser))
42 # use command line creds if available
43 credopts = options.CredentialsOptions(parser)
44 parser.add_option_group(credopts)
45 opts, args = parser.parse_args()
46
47 if len(args) < 1:
48     parser.print_usage()
49     sys.exit(1)
50
51 host = args[0]
52
53 lp = sambaopts.get_loadparm()
54 creds = credopts.get_credentials(lp)
55
56 # Force an encrypted connection
57 creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
58
59 #
60 # Tests start here
61 #
62
63 class PasswordTests(samba.tests.TestCase):
64
65     def delete_force(self, ldb, dn):
66         try:
67             ldb.delete(dn)
68         except LdbError, (num, _):
69             self.assertEquals(num, ERR_NO_SUCH_OBJECT)
70
71     def find_basedn(self, ldb):
72         res = ldb.search(base="", expression="", scope=SCOPE_BASE,
73                          attrs=["defaultNamingContext"])
74         self.assertEquals(len(res), 1)
75         return res[0]["defaultNamingContext"][0]
76
77     def setUp(self):
78         super(PasswordTests, self).setUp()
79         self.ldb = ldb
80         self.base_dn = self.find_basedn(ldb)
81
82         # (Re)adds the test user "testuser" with the inital password
83         # "thatsAcomplPASS1"
84         self.delete_force(self.ldb, "cn=testuser,cn=users," + self.base_dn)
85         self.ldb.add({
86              "dn": "cn=testuser,cn=users," + self.base_dn,
87              "objectclass": ["user", "person"],
88              "sAMAccountName": "testuser",
89              "userPassword": "thatsAcomplPASS1" })
90         self.ldb.enable_account("(sAMAccountName=testuser)")
91
92         # Open a second LDB connection with the user credentials. Use the
93         # command line credentials for informations like the domain, the realm
94         # and the workstation.
95         creds2 = Credentials()
96         creds2.set_username("testuser")
97         creds2.set_password("thatsAcomplPASS1")
98         creds2.set_domain(creds.get_domain())
99         creds2.set_realm(creds.get_realm())
100         creds2.set_workstation(creds.get_workstation())
101         creds2.set_gensec_features(creds2.get_gensec_features()
102                                                           | gensec.FEATURE_SEAL)
103         self.ldb2 = SamDB(url=host, credentials=creds2, lp=lp)
104
105     def test_unicodePwd_hash_set(self):
106         print "Performs a password hash set operation on 'unicodePwd' which should be prevented"
107         # Notice: Direct hash password sets should never work
108
109         m = Message()
110         m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
111         m["unicodePwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
112           "unicodePwd")
113         try:
114             ldb.modify(m)
115             self.fail()
116         except LdbError, (num, _):
117             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
118
119     def test_unicodePwd_hash_change(self):
120         print "Performs a password hash change operation on 'unicodePwd' which should be prevented"
121         # Notice: Direct hash password changes should never work
122
123         # Hash password changes should never work
124         try:
125             self.ldb2.modify_ldif("""
126 dn: cn=testuser,cn=users,""" + self.base_dn + """
127 changetype: modify
128 delete: unicodePwd
129 unicodePwd: XXXXXXXXXXXXXXXX
130 add: unicodePwd
131 unicodePwd: YYYYYYYYYYYYYYYY
132 """)
133             self.fail()
134         except LdbError, (num, _):
135             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
136
137     def test_unicodePwd_clear_set(self):
138         print "Performs a password cleartext set operation on 'unicodePwd'"
139
140         m = Message()
141         m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
142         m["unicodePwd"] = MessageElement("\"thatsAcomplPASS2\"".encode('utf-16-le'),
143           FLAG_MOD_REPLACE, "unicodePwd")
144         ldb.modify(m)
145
146     def test_unicodePwd_clear_change(self):
147         print "Performs a password cleartext change operation on 'unicodePwd'"
148
149         self.ldb2.modify_ldif("""
150 dn: cn=testuser,cn=users,""" + self.base_dn + """
151 changetype: modify
152 delete: unicodePwd
153 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')) + """
154 add: unicodePwd
155 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
156 """)
157
158         # A change to the same password again will not work (password history)
159         try:
160             self.ldb2.modify_ldif("""
161 dn: cn=testuser,cn=users,""" + self.base_dn + """
162 changetype: modify
163 delete: unicodePwd
164 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
165 add: unicodePwd
166 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')) + """
167 """)
168             self.fail()
169         except LdbError, (num, _):
170             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
171
172     def test_dBCSPwd_hash_set(self):
173         print "Performs a password hash set operation on 'dBCSPwd' which should be prevented"
174         # Notice: Direct hash password sets should never work
175
176         m = Message()
177         m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
178         m["dBCSPwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
179           "dBCSPwd")
180         try:
181             ldb.modify(m)
182             self.fail()
183         except LdbError, (num, _):
184             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
185
186     def test_dBCSPwd_hash_change(self):
187         print "Performs a password hash change operation on 'dBCSPwd' which should be prevented"
188         # Notice: Direct hash password changes should never work
189
190         try:
191             self.ldb2.modify_ldif("""
192 dn: cn=testuser,cn=users,""" + self.base_dn + """
193 changetype: modify
194 delete: dBCSPwd
195 dBCSPwd: XXXXXXXXXXXXXXXX
196 add: dBCSPwd
197 dBCSPwd: YYYYYYYYYYYYYYYY
198 """)
199             self.fail()
200         except LdbError, (num, _):
201             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
202
203     def test_userPassword_clear_set(self):
204         print "Performs a password cleartext set operation on 'userPassword'"
205         # Notice: This works only against Windows if "dSHeuristics" has been set
206         # properly
207
208         m = Message()
209         m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
210         m["userPassword"] = MessageElement("thatsAcomplPASS2", FLAG_MOD_REPLACE,
211           "userPassword")
212         ldb.modify(m)
213
214     def test_userPassword_clear_change(self):
215         print "Performs a password cleartext change operation on 'userPassword'"
216         # Notice: This works only against Windows if "dSHeuristics" has been set
217         # properly
218
219         self.ldb2.modify_ldif("""
220 dn: cn=testuser,cn=users,""" + self.base_dn + """
221 changetype: modify
222 delete: userPassword
223 userPassword: thatsAcomplPASS1
224 add: userPassword
225 userPassword: thatsAcomplPASS2
226 """)
227
228         # A change to the same password again will not work (password history)
229         try:
230             self.ldb2.modify_ldif("""
231 dn: cn=testuser,cn=users,""" + self.base_dn + """
232 changetype: modify
233 delete: userPassword
234 userPassword: thatsAcomplPASS2
235 add: userPassword
236 userPassword: thatsAcomplPASS2
237 """)
238             self.fail()
239         except LdbError, (num, _):
240             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
241
242     def test_clearTextPassword_clear_set(self):
243         print "Performs a password cleartext set operation on 'clearTextPassword'"
244         # Notice: This never works against Windows - only supported by us
245
246         try:
247             m = Message()
248             m.dn = Dn(ldb, "cn=testuser,cn=users," + self.base_dn)
249             m["clearTextPassword"] = MessageElement("thatsAcomplPASS2".encode('utf-16-le'),
250               FLAG_MOD_REPLACE, "clearTextPassword")
251             ldb.modify(m)
252             # this passes against s4
253         except LdbError, (num, msg):
254             # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
255             if num != ERR_NO_SUCH_ATTRIBUTE:
256                 raise LdbError(num, msg)
257
258     def test_clearTextPassword_clear_change(self):
259         print "Performs a password cleartext change operation on 'clearTextPassword'"
260         # Notice: This never works against Windows - only supported by us
261
262         try:
263             self.ldb2.modify_ldif("""
264 dn: cn=testuser,cn=users,""" + self.base_dn + """
265 changetype: modify
266 delete: clearTextPassword
267 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS1".encode('utf-16-le')) + """
268 add: clearTextPassword
269 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
270 """)
271             # this passes against s4
272         except LdbError, (num, msg):
273             # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
274             if num != ERR_NO_SUCH_ATTRIBUTE:
275                 raise LdbError(num, msg)
276
277         # A change to the same password again will not work (password history)
278         try:
279             self.ldb2.modify_ldif("""
280 dn: cn=testuser,cn=users,""" + self.base_dn + """
281 changetype: modify
282 delete: clearTextPassword
283 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
284 add: clearTextPassword
285 clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')) + """
286 """)
287             self.fail()
288         except LdbError, (num, _):
289             # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
290             if num != ERR_NO_SUCH_ATTRIBUTE:
291                 self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
292
293     def test_failures(self):
294         print "Performs some failure testing"
295
296         try:
297             ldb.modify_ldif("""
298 dn: cn=testuser,cn=users,""" + self.base_dn + """
299 changetype: modify
300 delete: userPassword
301 userPassword: thatsAcomplPASS1
302 """)
303             self.fail()
304         except LdbError, (num, _):
305             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
306
307         try:
308             self.ldb2.modify_ldif("""
309 dn: cn=testuser,cn=users,""" + self.base_dn + """
310 changetype: modify
311 delete: userPassword
312 """)
313             self.fail()
314         except LdbError, (num, _):
315             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
316
317         try:
318             ldb.modify_ldif("""
319 dn: cn=testuser,cn=users,""" + self.base_dn + """
320 changetype: modify
321 add: userPassword
322 userPassword: thatsAcomplPASS1
323 """)
324             self.fail()
325         except LdbError, (num, _):
326             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
327
328         try:
329             self.ldb2.modify_ldif("""
330 dn: cn=testuser,cn=users,""" + self.base_dn + """
331 changetype: modify
332 add: userPassword
333 userPassword: thatsAcomplPASS1
334 """)
335             self.fail()
336         except LdbError, (num, _):
337             self.assertEquals(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
338
339         try:
340             ldb.modify_ldif("""
341 dn: cn=testuser,cn=users,""" + self.base_dn + """
342 changetype: modify
343 delete: userPassword
344 userPassword: thatsAcomplPASS1
345 add: userPassword
346 userPassword: thatsAcomplPASS2
347 userPassword: thatsAcomplPASS2
348 """)
349             self.fail()
350         except LdbError, (num, _):
351             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
352
353         try:
354             self.ldb2.modify_ldif("""
355 dn: cn=testuser,cn=users,""" + self.base_dn + """
356 changetype: modify
357 delete: userPassword
358 userPassword: thatsAcomplPASS1
359 add: userPassword
360 userPassword: thatsAcomplPASS2
361 userPassword: thatsAcomplPASS2
362 """)
363             self.fail()
364         except LdbError, (num, _):
365             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
366
367         try:
368             ldb.modify_ldif("""
369 dn: cn=testuser,cn=users,""" + self.base_dn + """
370 changetype: modify
371 delete: userPassword
372 userPassword: thatsAcomplPASS1
373 userPassword: thatsAcomplPASS1
374 add: userPassword
375 userPassword: thatsAcomplPASS2
376 """)
377             self.fail()
378         except LdbError, (num, _):
379             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
380
381         try:
382             self.ldb2.modify_ldif("""
383 dn: cn=testuser,cn=users,""" + self.base_dn + """
384 changetype: modify
385 delete: userPassword
386 userPassword: thatsAcomplPASS1
387 userPassword: thatsAcomplPASS1
388 add: userPassword
389 userPassword: thatsAcomplPASS2
390 """)
391             self.fail()
392         except LdbError, (num, _):
393             self.assertEquals(num, ERR_CONSTRAINT_VIOLATION)
394
395         try:
396             ldb.modify_ldif("""
397 dn: cn=testuser,cn=users,""" + self.base_dn + """
398 changetype: modify
399 delete: userPassword
400 userPassword: thatsAcomplPASS1
401 add: userPassword
402 userPassword: thatsAcomplPASS2
403 add: userPassword
404 userPassword: thatsAcomplPASS2
405 """)
406             self.fail()
407         except LdbError, (num, _):
408             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
409
410         try:
411             self.ldb2.modify_ldif("""
412 dn: cn=testuser,cn=users,""" + self.base_dn + """
413 changetype: modify
414 delete: userPassword
415 userPassword: thatsAcomplPASS1
416 add: userPassword
417 userPassword: thatsAcomplPASS2
418 add: userPassword
419 userPassword: thatsAcomplPASS2
420 """)
421             self.fail()
422         except LdbError, (num, _):
423             self.assertEquals(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
424
425         try:
426             ldb.modify_ldif("""
427 dn: cn=testuser,cn=users,""" + self.base_dn + """
428 changetype: modify
429 delete: userPassword
430 userPassword: thatsAcomplPASS1
431 delete: userPassword
432 userPassword: thatsAcomplPASS1
433 add: userPassword
434 userPassword: thatsAcomplPASS2
435 """)
436             self.fail()
437         except LdbError, (num, _):
438             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
439
440         try:
441             self.ldb2.modify_ldif("""
442 dn: cn=testuser,cn=users,""" + self.base_dn + """
443 changetype: modify
444 delete: userPassword
445 userPassword: thatsAcomplPASS1
446 delete: userPassword
447 userPassword: thatsAcomplPASS1
448 add: userPassword
449 userPassword: thatsAcomplPASS2
450 """)
451             self.fail()
452         except LdbError, (num, _):
453             self.assertEquals(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
454
455         try:
456             ldb.modify_ldif("""
457 dn: cn=testuser,cn=users,""" + self.base_dn + """
458 changetype: modify
459 delete: userPassword
460 userPassword: thatsAcomplPASS1
461 add: userPassword
462 userPassword: thatsAcomplPASS2
463 replace: userPassword
464 userPassword: thatsAcomplPASS3
465 """)
466             self.fail()
467         except LdbError, (num, _):
468             self.assertEquals(num, ERR_UNWILLING_TO_PERFORM)
469
470         try:
471             self.ldb2.modify_ldif("""
472 dn: cn=testuser,cn=users,""" + self.base_dn + """
473 changetype: modify
474 delete: userPassword
475 userPassword: thatsAcomplPASS1
476 add: userPassword
477 userPassword: thatsAcomplPASS2
478 replace: userPassword
479 userPassword: thatsAcomplPASS3
480 """)
481             self.fail()
482         except LdbError, (num, _):
483             self.assertEquals(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
484
485         # Reverse order does work
486         self.ldb2.modify_ldif("""
487 dn: cn=testuser,cn=users,""" + self.base_dn + """
488 changetype: modify
489 add: userPassword
490 userPassword: thatsAcomplPASS2
491 delete: userPassword
492 userPassword: thatsAcomplPASS1
493 """)
494
495         try:
496             self.ldb2.modify_ldif("""
497 dn: cn=testuser,cn=users,""" + self.base_dn + """
498 changetype: modify
499 delete: userPassword
500 userPassword: thatsAcomplPASS2
501 add: unicodePwd
502 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')) + """
503 """)
504              # this passes against s4
505         except LdbError, (num, _):
506             self.assertEquals(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
507
508         try:
509             self.ldb2.modify_ldif("""
510 dn: cn=testuser,cn=users,""" + self.base_dn + """
511 changetype: modify
512 delete: unicodePwd
513 unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')) + """
514 add: userPassword
515 userPassword: thatsAcomplPASS4
516 """)
517              # this passes against s4
518         except LdbError, (num, _):
519             self.assertEquals(num, ERR_NO_SUCH_ATTRIBUTE)
520
521         # Several password changes at once are allowed
522         ldb.modify_ldif("""
523 dn: cn=testuser,cn=users,""" + self.base_dn + """
524 changetype: modify
525 replace: userPassword
526 userPassword: thatsAcomplPASS1
527 userPassword: thatsAcomplPASS2
528 """)
529
530         # Several password changes at once are allowed
531         ldb.modify_ldif("""
532 dn: cn=testuser,cn=users,""" + self.base_dn + """
533 changetype: modify
534 replace: userPassword
535 userPassword: thatsAcomplPASS1
536 userPassword: thatsAcomplPASS2
537 replace: userPassword
538 userPassword: thatsAcomplPASS3
539 replace: userPassword
540 userPassword: thatsAcomplPASS4
541 """)
542
543         # This surprisingly should work
544         self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
545         self.ldb.add({
546              "dn": "cn=testuser2,cn=users," + self.base_dn,
547              "objectclass": ["user", "person"],
548              "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS2"] })
549
550         # This surprisingly should work
551         self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
552         self.ldb.add({
553              "dn": "cn=testuser2,cn=users," + self.base_dn,
554              "objectclass": ["user", "person"],
555              "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS1"] })
556
557     def tearDown(self):
558         super(PasswordTests, self).tearDown()
559         self.delete_force(self.ldb, "cn=testuser,cn=users," + self.base_dn)
560         self.delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
561         # Close the second LDB connection (with the user credentials)
562         self.ldb2 = None
563
564 if not "://" in host:
565     if os.path.isfile(host):
566         host = "tdb://%s" % host
567     else:
568         host = "ldap://%s" % host
569
570 ldb = SamDB(url=host, session_info=system_session(), credentials=creds, lp=lp)
571
572 # Gets back the configuration basedn
573 res = ldb.search(base="", expression="", scope=SCOPE_BASE,
574                  attrs=["configurationNamingContext"])
575 configuration_dn = res[0]["configurationNamingContext"][0]
576
577 # Gets back the basedn
578 res = ldb.search(base="", expression="", scope=SCOPE_BASE,
579                  attrs=["defaultNamingContext"])
580 base_dn = res[0]["defaultNamingContext"][0]
581
582 # Get the old "dSHeuristics" if it was set
583 res = ldb.search("CN=Directory Service, CN=Windows NT, CN=Services, "
584                  + configuration_dn, scope=SCOPE_BASE, attrs=["dSHeuristics"])
585 if "dSHeuristics" in res[0]:
586   dsheuristics = res[0]["dSHeuristics"][0]
587 else:
588   dsheuristics = None
589
590 # Set the "dSHeuristics" to have the tests run against Windows Server
591 m = Message()
592 m.dn = Dn(ldb, "CN=Directory Service, CN=Windows NT, CN=Services, "
593   + configuration_dn)
594 m["dSHeuristics"] = MessageElement("000000001", FLAG_MOD_REPLACE,
595   "dSHeuristics")
596 ldb.modify(m)
597
598 # Get the old "minPwdAge"
599 res = ldb.search(base_dn, scope=SCOPE_BASE, attrs=["minPwdAge"])
600 minPwdAge = res[0]["minPwdAge"][0]
601
602 # Set it temporarely to "0"
603 m = Message()
604 m.dn = Dn(ldb, base_dn)
605 m["minPwdAge"] = MessageElement("0", FLAG_MOD_REPLACE, "minPwdAge")
606 ldb.modify(m)
607
608 runner = SubunitTestRunner()
609 rc = 0
610 if not runner.run(unittest.makeSuite(PasswordTests)).wasSuccessful():
611     rc = 1
612
613 # Reset the "dSHeuristics" as they were before
614 m = Message()
615 m.dn = Dn(ldb, "CN=Directory Service, CN=Windows NT, CN=Services, "
616   + configuration_dn)
617 if dsheuristics is not None:
618     m["dSHeuristics"] = MessageElement(dsheuristics, FLAG_MOD_REPLACE,
619       "dSHeuristics")
620 else:
621     m["dSHeuristics"] = MessageElement([], FLAG_MOD_DELETE, "dsHeuristics")
622 ldb.modify(m)
623
624 # Reset the "minPwdAge" as it was before
625 m = Message()
626 m.dn = Dn(ldb, base_dn)
627 m["minPwdAge"] = MessageElement(minPwdAge, FLAG_MOD_REPLACE, "minPwdAge")
628 ldb.modify(m)
629
630 sys.exit(rc)