CVE-2021-20251 tests/krb5: Add tests for password lockout race
[samba.git] / python / samba / tests / usage.py
1 # Unix SMB/CIFS implementation.
2 # Copyright © Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 import os
18 import sys
19 import subprocess
20 from samba.tests import TestCase, check_help_consistency
21 from unittest import TestSuite
22 import re
23 import stat
24
25 if 'SRCDIR_ABS' in os.environ:
26     BASEDIR = os.environ['SRCDIR_ABS']
27 else:
28     BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
29                                            '../../..'))
30
31 TEST_DIRS = [
32     "bootstrap",
33     "testdata",
34     "ctdb",
35     "dfs_server",
36     "pidl",
37     "auth",
38     "packaging",
39     "python",
40     "include",
41     "nsswitch",
42     "libcli",
43     "coverity",
44     "release-scripts",
45     "testprogs",
46     "bin",
47     "source3",
48     "docs-xml",
49     "buildtools",
50     "file_server",
51     "dynconfig",
52     "source4",
53     "tests",
54     "libds",
55     "selftest",
56     "lib",
57     "script",
58     "traffic",
59     "testsuite",
60     "libgpo",
61     "wintest",
62     "librpc",
63 ]
64
65
66 EXCLUDE_USAGE = {
67     'script/autobuild.py',  # defaults to mount /memdisk/
68     'script/bisect-test.py',
69     'ctdb/utils/etcd/ctdb_etcd_lock',
70     'selftest/filter-subunit',
71     'selftest/format-subunit',
72     'bin/gen_output.py',  # too much output!
73     'source4/scripting/bin/gen_output.py',
74     'lib/ldb/tests/python/index.py',
75     'lib/ldb/tests/python/api.py',
76     'source4/selftest/tests.py',
77     'buildtools/bin/waf',
78     'selftest/tap2subunit',
79     'script/show_test_time',
80     'source4/scripting/bin/subunitrun',
81     'bin/samba_downgrade_db',
82     'source4/scripting/bin/samba_downgrade_db',
83     'source3/selftest/tests.py',
84     'selftest/tests.py',
85     'python/samba/subunit/run.py',
86     'bin/python/samba/subunit/run.py',
87     'python/samba/tests/dcerpc/raw_protocol.py',
88     'python/samba/tests/smb-notify.py',
89     'python/samba/tests/krb5/kcrypto.py',
90     'python/samba/tests/krb5/simple_tests.py',
91     'python/samba/tests/krb5/s4u_tests.py',
92     'python/samba/tests/krb5/xrealm_tests.py',
93     'python/samba/tests/krb5/as_canonicalization_tests.py',
94     'python/samba/tests/krb5/compatability_tests.py',
95     'python/samba/tests/krb5/rfc4120_constants.py',
96     'python/samba/tests/krb5/kdc_tests.py',
97     'python/samba/tests/krb5/kdc_base_test.py',
98     'python/samba/tests/krb5/kdc_tgs_tests.py',
99     'python/samba/tests/krb5/test_ccache.py',
100     'python/samba/tests/krb5/test_ldap.py',
101     'python/samba/tests/krb5/test_rpc.py',
102     'python/samba/tests/krb5/test_smb.py',
103     'python/samba/tests/krb5/ms_kile_client_principal_lookup_tests.py',
104     'python/samba/tests/krb5/as_req_tests.py',
105     'python/samba/tests/krb5/fast_tests.py',
106     'python/samba/tests/krb5/rodc_tests.py',
107     'python/samba/tests/krb5/salt_tests.py',
108     'python/samba/tests/krb5/spn_tests.py',
109     'python/samba/tests/krb5/alias_tests.py',
110     'python/samba/tests/krb5/test_min_domain_uid.py',
111     'python/samba/tests/krb5/test_idmap_nss.py',
112     'python/samba/tests/krb5/pac_align_tests.py',
113     'python/samba/tests/krb5/protected_users_tests.py',
114     'python/samba/tests/krb5/nt_hash_tests.py',
115     'python/samba/tests/krb5/kpasswd_tests.py',
116     'python/samba/tests/krb5/claims_tests.py',
117     'python/samba/tests/krb5/lockout_tests.py',
118 }
119
120 EXCLUDE_HELP = {
121     'selftest/tap2subunit',
122     'wintest/test-s3.py',
123     'wintest/test-s4-howto.py',
124 }
125
126
127 EXCLUDE_DIRS = {
128     'source3/script/tests',
129     'python/examples',
130     'source4/dsdb/tests/python',
131     'bin/ab',
132     'bin/python/samba/tests',
133     'bin/python/samba/tests/dcerpc',
134     'bin/python/samba/tests/krb5',
135     'python/samba/tests/bin',
136 }
137
138
139 def _init_git_file_finder():
140     """Generate a function that quickly answers the question:
141     'is this a git file?'
142     """
143     git_file_cache = set()
144     p = subprocess.run(['git',
145                         '-C', BASEDIR,
146                         'ls-files',
147                         '-z'],
148                        stdout=subprocess.PIPE)
149     if p.returncode == 0:
150         for fn in p.stdout.split(b'\0'):
151             git_file_cache.add(os.path.join(BASEDIR, fn.decode('utf-8')))
152     return git_file_cache.__contains__
153
154
155 is_git_file = _init_git_file_finder()
156
157
158 def script_iterator(d=BASEDIR, cache=None,
159                     shebang_filter=None,
160                     filename_filter=None,
161                     subdirs=TEST_DIRS):
162     if not cache:
163         safename = re.compile(r'\W+').sub
164         for subdir in subdirs:
165             sd = os.path.join(d, subdir)
166             for root, dirs, files in os.walk(sd, followlinks=False):
167                 for fn in files:
168                     if fn.endswith('~'):
169                         continue
170                     if fn.endswith('.inst'):
171                         continue
172                     ffn = os.path.join(root, fn)
173                     try:
174                         s = os.stat(ffn)
175                     except FileNotFoundError:
176                         continue
177                     if not s.st_mode & stat.S_IXUSR:
178                         continue
179                     if not (subdir == 'bin' or is_git_file(ffn)):
180                         continue
181
182                     if filename_filter is not None:
183                         if not filename_filter(ffn):
184                             continue
185
186                     if shebang_filter is not None:
187                         try:
188                             f = open(ffn, 'rb')
189                         except OSError as e:
190                             print("could not open %s: %s" % (ffn, e))
191                             continue
192                         line = f.read(40)
193                         f.close()
194                         if not shebang_filter(line):
195                             continue
196
197                     name = safename('_', fn)
198                     while name in cache:
199                         name += '_'
200                     cache[name] = ffn
201
202     return cache.items()
203
204 # For ELF we only look at /bin/* top level.
205 def elf_file_name(fn):
206     fn = fn.partition('bin/')[2]
207     return fn and '/' not in fn and 'test' not in fn and 'ldb' in fn
208
209 def elf_shebang(x):
210     return x[:4] == b'\x7fELF'
211
212 elf_cache = {}
213 def elf_iterator():
214     return script_iterator(BASEDIR, elf_cache,
215                            shebang_filter=elf_shebang,
216                            filename_filter=elf_file_name,
217                            subdirs=['bin'])
218
219
220 perl_shebang = re.compile(br'#!.+perl').match
221
222 perl_script_cache = {}
223 def perl_script_iterator():
224     return script_iterator(BASEDIR, perl_script_cache, perl_shebang)
225
226
227 python_shebang = re.compile(br'#!.+python').match
228
229 python_script_cache = {}
230 def python_script_iterator():
231     return script_iterator(BASEDIR, python_script_cache, python_shebang)
232
233
234 class PerlScriptUsageTests(TestCase):
235     """Perl scripts run without arguments should print a usage string,
236         not fail with a traceback.
237     """
238
239     @classmethod
240     def initialise(cls):
241         for name, filename in perl_script_iterator():
242             print(name, filename)
243
244
245 class PythonScriptUsageTests(TestCase):
246     """Python scripts run without arguments should print a usage string,
247         not fail with a traceback.
248     """
249
250     @classmethod
251     def initialise(cls):
252         for name, filename in python_script_iterator():
253             # We add the actual tests after the class definition so we
254             # can give individual names to them, so we can have a
255             # knownfail list.
256             fn = filename.replace(BASEDIR, '').lstrip('/')
257
258             if fn in EXCLUDE_USAGE:
259                 print("skipping %s (EXCLUDE_USAGE)" % filename)
260                 continue
261
262             if os.path.dirname(fn) in EXCLUDE_DIRS:
263                 print("skipping %s (EXCLUDE_DIRS)" % filename)
264                 continue
265
266             def _f(self, filename=filename):
267                 print(filename)
268                 try:
269                     p = subprocess.Popen(['python3', filename],
270                                          stderr=subprocess.PIPE,
271                                          stdout=subprocess.PIPE)
272                     out, err = p.communicate(timeout=5)
273                 except OSError as e:
274                     self.fail("Error: %s" % e)
275                 except subprocess.SubprocessError as e:
276                     self.fail("Subprocess error: %s" % e)
277
278                 err = err.decode('utf-8')
279                 out = out.decode('utf-8')
280                 self.assertNotIn('Traceback', err)
281
282                 self.assertIn('usage', out.lower() + err.lower(),
283                               'stdout:\n%s\nstderr:\n%s' % (out, err))
284
285             setattr(cls, 'test_%s' % name, _f)
286
287
288 class HelpTestSuper(TestCase):
289     """Python scripts run with -h or --help should print a help string,
290     and exit with success.
291     """
292     check_return_code = True
293     check_consistency = True
294     check_contains_usage = True
295     check_multiline = True
296     check_merged_out_and_err = False
297
298     interpreter = None
299
300     options_start = None
301     options_end = None
302     def iterator(self):
303         raise NotImplementedError("Subclass this "
304                                   "and add an iterator function!")
305
306     @classmethod
307     def initialise(cls):
308         for name, filename in cls.iterator():
309             # We add the actual tests after the class definition so we
310             # can give individual names to them, so we can have a
311             # knownfail list.
312             fn = filename.replace(BASEDIR, '').lstrip('/')
313
314             if fn in EXCLUDE_HELP:
315                 print("skipping %s (EXCLUDE_HELP)" % filename)
316                 continue
317
318             if os.path.dirname(fn) in EXCLUDE_DIRS:
319                 print("skipping %s (EXCLUDE_DIRS)" % filename)
320                 continue
321
322             def _f(self, filename=filename):
323                 print(filename)
324                 for h in ('--help', '-h'):
325                     cmd = [filename, h]
326                     if self.interpreter:
327                         cmd.insert(0, self.interpreter)
328                     try:
329                         p = subprocess.Popen(cmd,
330                                              stderr=subprocess.PIPE,
331                                              stdout=subprocess.PIPE)
332                         out, err = p.communicate(timeout=5)
333                     except OSError as e:
334                         self.fail("Error: %s" % e)
335                     except subprocess.SubprocessError as e:
336                         self.fail("Subprocess error: %s" % e)
337
338                     err = err.decode('utf-8')
339                     out = out.decode('utf-8')
340                     if self.check_merged_out_and_err:
341                         out = "%s\n%s" % (out, err)
342
343                     outl = out[:500].lower()
344                     # NOTE:
345                     # These assertions are heuristics, not policy.
346                     # If your script fails this test when it shouldn't
347                     # just add it to EXCLUDE_HELP above or change the
348                     # heuristic.
349
350                     # --help should produce:
351                     #    * multiple lines of help on stdout (not stderr),
352                     #    * including a "Usage:" string,
353                     #    * not contradict itself or repeat options,
354                     #    * and return success.
355                     #print(out.encode('utf8'))
356                     #print(err.encode('utf8'))
357                     if self.check_consistency:
358                         errors = check_help_consistency(out,
359                                                         self.options_start,
360                                                         self.options_end)
361                         if errors is not None:
362                             self.fail(errors)
363
364                     if self.check_return_code:
365                         self.assertEqual(p.returncode, 0,
366                                          "%s %s\nreturncode should not be %d\n"
367                                          "err:\n%s\nout:\n%s" %
368                                          (filename, h, p.returncode, err, out))
369                     if self.check_contains_usage:
370                         self.assertIn('usage', outl, 'lacks "Usage:"\n')
371                     if self.check_multiline:
372                         self.assertIn('\n', out, 'expected multi-line output')
373
374             setattr(cls, 'test_%s' % name, _f)
375
376
377 class PythonScriptHelpTests(HelpTestSuper):
378     """Python scripts run with -h or --help should print a help string,
379     and exit with success.
380     """
381     iterator = python_script_iterator
382     interpreter = 'python3'
383
384
385 class ElfHelpTests(HelpTestSuper):
386     """ELF binaries run with -h or --help should print a help string,
387     and exit with success.
388     """
389     iterator = elf_iterator
390     check_return_code = False
391     check_merged_out_and_err = True
392
393
394 PerlScriptUsageTests.initialise()
395 PythonScriptUsageTests.initialise()
396 PythonScriptHelpTests.initialise()
397 ElfHelpTests.initialise()