Return kinit output in k5test's K5Realm.kinit
[krb5.git] / src / util / k5test.py
1 # Copyright (C) 2010 by the Massachusetts Institute of Technology.
2 # All rights reserved.
3
4 # Export of this software from the United States of America may
5 #   require a specific license from the United States Government.
6 #   It is the responsibility of any person or organization contemplating
7 #   export to obtain such a license before exporting.
8 #
9 # WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
10 # distribute this software and its documentation for any purpose and
11 # without fee is hereby granted, provided that the above copyright
12 # notice appear in all copies and that both that copyright notice and
13 # this permission notice appear in supporting documentation, and that
14 # the name of M.I.T. not be used in advertising or publicity pertaining
15 # to distribution of the software without specific, written prior
16 # permission.  Furthermore if you modify this software you must label
17 # your software as modified software and not distribute it in such a
18 # fashion that it might be confused with the original M.I.T. software.
19 # M.I.T. makes no representations about the suitability of
20 # this software for any purpose.  It is provided "as is" without express
21 # or implied warranty.
22
23 """A module for krb5 test scripts
24
25 To run test scripts during "make check" (if Python 2.4 or later is
26 available), add rules like the following to Makefile.in:
27
28     check-pytests::
29         $(RUNPYTEST) $(srcdir)/t_testname.py $(PYTESTFLAGS)
30
31 A sample test script:
32
33     from k5test import *
34
35     # Run a test program under a variety of configurations:
36     for realm in multipass_realms():
37         realm.run_as_client(['./testprog', 'arg'])
38
39     # Run a test server and client under just the default configuration:
40     realm = K5Realm()
41     realm.start_server(['./serverprog'], 'starting...')
42     realm.run_as_client(['./clientprog', realm.host_princ])
43
44     # Inform framework that tests completed successfully.
45     success('World peace and cure for cancer')
46
47 By default, the realm will have:
48
49 * The name KRBTEST.COM
50 * Listener ports starting at 61000
51 * Four different krb5.conf files for the client, server, master KDC,
52   and slave KDC, specifying only the variables necessary for
53   self-contained test operation
54 * Two different kdc.conf files for the master and slave KDCs
55 * A fresh DB2 KDB
56 * Running krb5kdc and kadmind processes
57 * Principals named realm.user_princ and realm.admin_princ; call
58   password('user') and password('admin') to get the password
59 * Credentials for realm.user_princ in realm.ccache
60 * Admin rights for realm.admin_princ in the kadmind acl file
61 * A host principal named realm.host_princ with a random key
62 * A keytab for the host principal in realm.keytab
63
64 The realm's behaviour can be modified with the following constructor
65 keyword arguments:
66
67 * realm='realmname': Override the realm name
68
69 * portbase=NNN: Override the listener port base; currently three ports are
70   used
71
72 * testdir='dirname': Override the storage area for the realm's files
73   (path may be specified relative to the current working dir)
74
75 * krb5_conf={ ... }: krb5.conf options, expressed as a nested
76   dictionary, to be merged with the default krb5.conf settings.  The
77   top level keys of the dictionary should be 'all' to apply to all
78   four krb5.conf files, and/or 'client'/'server'/'master'/'slave' to
79   apply to a particular one.  A key may be mapped to None to delete a
80   setting from the defaults.  A key may be mapped to a list in order
81   to create multiple settings for the same variable name.  Keys and
82   values undergo the following template substitutions:
83
84     - $type:     The configuration type (client/server/master/slave)
85     - $realm:    The realm name
86     - $testdir:  The realm storage directory (absolute path)
87     - $buildtop: The root of the build directory
88     - $srctop:   The root of the source directory
89     - $plugins:  The plugin directory in the build tree
90     - $hostname: The FQDN of the host
91     - $port0:    The first listener port (portbase)
92     - ...
93     - $port9:    The tenth listener port (portbase + 9)
94
95   When choosing ports, note the following:
96
97     - port0 is used in the default krb5.conf for the KDC
98     - port1 is used in the default krb5.conf for kadmind
99     - port2 is used in the default krb5.conf for kpasswd
100     - port3 is the return value of realm.server_port()
101
102 * kdc_conf={...}: kdc.conf options, expressed as a nested dictionary,
103   to be merged with the default kdc.conf settings.  The top level keys
104   should be 'all' or 'master'/'slave'.  The same conventions and
105   substitutions for krb5_conf apply.
106
107 * create_kdb=False: Don't create a KDB.  Implicitly disables all of
108   the other options since they all require a KDB.
109
110 * krbtgt_keysalt='enctype:salttype': After creating the KDB,
111   regenerate the krbtgt key using the specified key/salt combination,
112   using a kadmin.local cpw query.
113
114 * create_user=False: Don't create the user principal.  Implies
115   get_creds=False.
116
117 * create_host=False: Don't create the host principal or the associated
118   keytab.
119
120 * start_kdc=False: Don't start the KDC.  Implies get_creds=False.
121
122 * start_kadmind=False: Don't start kadmind.
123
124 * get_creds=False: Don't get user credentials.
125
126 Scripts may use the following functions and variables:
127
128 * fail(message): Display message (plus leading marker and trailing
129   newline) and explanatory messages about debugging.
130
131 * success(message): Indicate that the test script has completed
132   successfully.  Suppresses the display of explanatory debugging
133   messages in the on-exit handler.  message should briefly summarize
134   the operations tested; it will only be displayed (with leading
135   marker and trailing newline) if the script is running verbosely.
136
137 * output(message, force_verbose=False): Place message (without any
138   added newline) in testlog, and write it to stdout if running
139   verbosely.
140
141 * password(name): Return a weakly random password based on name.  The
142   password will be consistent across calls with the same name.
143
144 * stop_daemon(proc): Stop a daemon process started with
145   realm.start_server() or realm.start_in_inetd().  Only necessary if
146   the port needs to be reused; daemon processes will be stopped
147   automatically when the script exits.
148
149 * multipass_realms(**keywords): This is an iterator function.  Yields
150   a realm for each of the standard test passes, each of which alters
151   the default configuration in some way to exercise different parts of
152   the krb5 code base.  keywords may contain any K5Realm initializer
153   keyword with the exception of krbtgt_keysalt, which will not be
154   honored.  If keywords contains krb5_conf and/or kdc_conf fragments,
155   they will be merged with the default and per-pass specifications.
156
157 * cross_realms(num, xtgts=None, args=None, **keywords): This function
158   returns a list of num realms, where each realm's configuration knows
159   how to contact all of the realms.  By default, each realm will
160   contain cross TGTs in both directions for all other realms; this
161   default may be overridden by specifying a collection of tuples in
162   the xtgts parameter, where each tuple is a pair of zero-based realm
163   indexes, indicating that the first realm can authenticate to the
164   second (i.e. krbtgt/secondrealm@firstrealm exists in both realm's
165   databases).  If args is given, it should be a list of keyword
166   arguments specific to each realm; these will be merged with the
167   global keyword arguments passed to cross_realms, with specific
168   arguments taking priority.
169
170 * buildtop: The top of the build directory (absolute path).
171
172 * srctop: The top of the source directory (absolute path).
173
174 * plugins: The plugin directory in the build tree (absolute path).
175
176 * hostname: This machine's fully-qualified domain name.
177
178 * null_input: A file opened to read /dev/null.
179
180 * args: Positional arguments left over after flags are processed.
181
182 * verbose: Whether the script is running verbosely.
183
184 * testpass: The command-line test pass argument.  The script does not
185   need to examine this argument in most cases; it will be honored in
186   multipass_realms().
187
188 * Pathname variables for programs within the build directory:
189   - krb5kdc
190   - kadmind
191   - kadmin
192   - kadmin_local
193   - kdb5_util
194   - ktutil
195   - kinit
196   - klist
197   - kswitch
198   - kvno
199   - kdestroy
200   - kpasswd
201   - t_inetd
202   - kproplog
203   - kpropd
204   - kprop
205
206 Scripts may use the following realm methods and attributes:
207
208 * realm.run_as_client(args, **keywords): Run a command with an
209   environment pointing at the client krb5.conf, obeying the
210   command-line debugging options.  Fail if the command does not return
211   0.  Log the command output appropriately, and return it as a single
212   multi-line string.  Keyword arguments can contain input='string' to
213   send an input string to the command, and expected_code=N to expect a
214   return code other than 0.
215
216 * Similar methods for the server, master KDC, and slave KDC
217   environments:
218   - realm.run_as_server
219   - realm.run_as_master
220   - realm.run_as_slave
221
222 * realm.server_port(): Returns a port number based on realm.portbase
223   intended for use by server processes.
224
225 * realm.start_server(args, sentinel): Start a process in the server
226   environment.  Wait until sentinel appears as a substring of a line
227   in the server process's stdout or stderr (which are folded
228   together).  Returns a subprocess.Popen object which can be passed to
229   stop_daemon() to stop the server, or used to read from the server's
230   output.
231
232 * realm.start_in_inetd(args, port=None): Begin a t_inetd process which
233   will spawn a server process within the server environment after
234   accepting a client connection.  If port is not specified,
235   realm.server_port() will be used.  Returns a process object which
236   can be passed to stop_daemon() to stop the server.
237
238 * realm.create_kdb(): Create a new master KDB.
239
240 * realm.start_kdc(args=[]): Start a krb5kdc with the realm's master
241   KDC environment.  Errors if a KDC is already running.  If args is
242   given, it contains a list of additional krb5kdc arguments.
243
244 * realm.stop_kdc(): Stop the krb5kdc process.  Errors if no KDC is
245   running.
246
247 * realm.start_kadmind(): Start a kadmind with the realm's master KDC
248   environment.  Errors if a kadmind is already running.
249
250 * realm.stop_kadmind(): Stop the kadmind process.  Errors if no
251   kadmind is running.
252
253 * realm.stop(): Stop any KDC and kadmind processes running on behalf
254   of the realm.
255
256 * realm.addprinc(princname, password=None): Using kadmin.local, create
257   a principle in the KDB named princname, with either a random or
258   specified key.
259
260 * realm.extract_keytab(princname, keytab): Using kadmin.local, create
261   a keytab for princname in the filename keytab.  Uses the -norandkey
262   option to avoid re-randomizing princname's key.
263
264 * realm.kinit(princname, password=None, flags=[]): Acquire credentials
265   for princname using kinit, with additional flags [].  If password is
266   specified, it will be used as input to the kinit process; otherwise
267   flags must cause kinit not to need a password (e.g. by specifying a
268   keytab).
269
270 * realm.klist(client_princ, service_princ=None, ccache=None): Using
271   klist, list the credentials cache ccache (must be a filename;
272   self.ccache if not specified) and verify that the output shows
273   credentials for client_princ and service_princ (self.krbtgt_princ if
274   not specified).
275
276 * realm.klist_keytab(princ, keytab=None): Using klist, list keytab
277   (must be a filename; self.keytab if not specified) and verify that
278   the output shows the keytab name and principal name.
279
280 * realm.run_kadminl(query): Run the specified query in kadmin.local.
281
282 * realm.realm: The realm's name.
283
284 * realm.testdir: The realm's storage directory (absolute path).
285
286 * realm.portbase: The realm's first listener port.
287
288 * realm.user_princ: The principal name user@<realmname>.
289
290 * realm.admin_princ: The principal name user/admin@<realmname>.
291
292 * realm.host_princ: The name of the host principal for this machine,
293   with realm.
294
295 * realm.krbtgt_princ: The name of the krbtgt principal for the realm.
296
297 * realm.keytab: A keytab file in realm.testdir.  Initially contains a
298   host keytab unless disabled by the realm construction options.
299
300 * realm.ccache: A ccache file in realm.testdir.  Initially contains
301   credentials for user unless disabled by the realm construction
302   options.
303
304 * Attributes for the client, server, master, and slave environments.
305   These environments are extensions of os.environ.
306   - realm.env_client
307   - realm.env_server
308   - realm.env_master
309   - realm.env_slave
310
311 When the test script is run, its behavior can be modified with
312 command-line flags.  These are documented in the --help output.
313
314 """
315
316 import atexit
317 import optparse
318 import os
319 import shlex
320 import shutil
321 import signal
322 import socket
323 import string
324 import subprocess
325 import sys
326 import imp
327
328 # Used when most things go wrong (other than programming errors) so
329 # that the user sees an error message rather than a Python traceback,
330 # without help from the test script.  The on-exit handler will display
331 # additional explanatory text.
332 def fail(msg):
333     """Print a message and exit with failure."""
334     global _current_pass
335     print "*** Failure:", msg
336     if _current_pass:
337         print "*** Failed in test pass:", _current_pass
338     sys.exit(1)
339
340
341 def success(msg):
342     global _success
343     output('*** Success: %s\n' % msg)
344     _success = True
345
346
347 def output(msg, force_verbose=False):
348     """Output a message to testlog, and to stdout if running verbosely."""
349     _outfile.write(msg)
350     if verbose or force_verbose:
351         sys.stdout.write(msg)
352
353
354 def password(name):
355     """Choose a weakly random password from name, consistent across calls."""
356     return name + str(os.getpid())
357
358
359 # Exit handler which ensures processes are cleaned up and, on failure,
360 # prints messages to help developers debug the problem.
361 def _onexit():
362     global _daemons, _success, verbose
363     global _debug, _stop_before, _stop_after, _shell_before, _shell_after
364     if _daemons is None:
365         # In Python 2.5, if we exit as a side-effect of importing
366         # k5test, _onexit will execute in an empty global namespace.
367         # This can happen if argument processing fails or the build
368         # root isn't valid.  In this case we can safely assume that no
369         # daemons have been launched and that we don't really need to
370         # amend the error message.  The bug is fixed in Python 2.6.
371         return
372     if _debug or _stop_before or _stop_after or _shell_before or _shell_after:
373         # Wait before killing daemons in case one is being debugged.
374         sys.stdout.write('*** Press return to kill daemons and exit script: ')
375         sys.stdin.readline()
376     for proc in _daemons:
377         os.kill(proc.pid, signal.SIGTERM)
378     if not _success:
379         print
380         if not verbose:
381             print 'See testlog for details, or re-run with -v flag.'
382             print
383         print 'Use --debug=NUM to run a command (other than a daemon) under a'
384         print 'debugger.  Use --stop-after=NUM to stop after a daemon is'
385         print 'started in order to attach to it with a debugger.  Use --help'
386         print 'to see other options.'
387
388 # Find the parent of dir which is at the root of a build or source directory.
389 def _find_root(dir):
390     while True:
391         if os.path.exists(os.path.join(dir, 'lib', 'krb5', 'krb')):
392             break
393         parent = os.path.dirname(dir)
394         if (parent == dir):
395             return None
396         dir = parent
397     return dir
398
399
400 def _find_buildtop():
401     root = _find_root(os.getcwd())
402     if root is None:
403         fail('Cannot find root of krb5 build directory.')
404     if not os.path.exists(os.path.join(root, 'config.status')):
405         # Looks like an unbuilt source directory.
406         fail('This script must be run inside a krb5 build directory.')
407     return root
408
409
410 def _find_srctop():
411     scriptdir = os.path.abspath(os.path.dirname(sys.argv[0]))
412     if not scriptdir:
413         scriptdir = os.getcwd()
414     root = _find_root(scriptdir)
415     if root is None:
416         fail('Cannot find root of krb5 source directory.')
417     return os.path.abspath(root)
418
419
420 # Return the local hostname as it will be canonicalized by
421 # krb5_sname_to_principal.  We can't simply use socket.getfqdn()
422 # because it explicitly prefers results containing periods and
423 # krb5_sname_to_principal doesn't care.
424 def _get_hostname():
425     hostname = socket.gethostname()
426     try:
427         ai = socket.getaddrinfo(hostname, None, 0, 0, 0,
428                                 socket.AI_CANONNAME | socket.AI_ADDRCONFIG)
429     except socket.gaierror, (error, errstr):
430         fail('Local hostname "%s" does not resolve: %s.' % (hostname, errstr))
431     (family, socktype, proto, canonname, sockaddr) = ai[0]
432     try:
433         name = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD)
434     except socket.gaierror:
435         return canonname.lower()
436     return name[0].lower()
437
438 # Parse command line arguments, setting global option variables.  Also
439 # sets the global variable args to the positional arguments, which may
440 # be used by the test script.
441 def _parse_args():
442     global args, verbose, testpass, _debug, _debugger_command
443     global _stop_before, _stop_after, _shell_before, _shell_after
444     parser = optparse.OptionParser()
445     parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
446                       default=False, help='Display verbose output')
447     parser.add_option('-p', '--pass', dest='testpass', metavar='PASS',
448                       help='If a multi-pass test, run only PASS')
449     parser.add_option('--debug', dest='debug', metavar='NUM',
450                       help='Debug numbered command (or "all")')
451     parser.add_option('--debugger', dest='debugger', metavar='COMMAND',
452                       help='Debugger command (default is gdb --args)',
453                       default='gdb --args')
454     parser.add_option('--stop-before', dest='stopb', metavar='NUM',
455                       help='Stop before numbered command (or "all")')
456     parser.add_option('--stop-after', dest='stopa', metavar='NUM',
457                       help='Stop after numbered command (or "all")')
458     parser.add_option('--shell-before', dest='shellb', metavar='NUM',
459                       help='Spawn shell before numbered command (or "all")')
460     parser.add_option('--shell-after', dest='shella', metavar='NUM',
461                       help='Spawn shell after numbered command (or "all")')
462     (options, args) = parser.parse_args()
463     verbose = options.verbose
464     testpass = options.testpass
465     _debug = _parse_cmdnum('--debug', options.debug)
466     _debugger_command = shlex.split(options.debugger)
467     _stop_before = _parse_cmdnum('--stop-before', options.stopb)
468     _stop_after = _parse_cmdnum('--stop-after', options.stopa)
469     _shell_before = _parse_cmdnum('--shell-before', options.shellb)
470     _shell_after = _parse_cmdnum('--shell-after', options.shella)
471
472
473 # Translate a command number spec.  -1 means all, None means none.
474 def _parse_cmdnum(optname, str):
475     if not str:
476         return None
477     if str == 'all':
478         return -1
479     try:
480         return int(str)
481     except ValueError:
482         fail('%s value must be "all" or a number' % optname)
483
484
485 # Test if a command index matches a translated command number spec.
486 def _match_cmdnum(cmdnum, ind):
487     if cmdnum is None:
488         return False
489     elif cmdnum == -1:
490         return True
491     else:
492         return cmdnum == ind
493
494
495 # Return an environment suitable for running programs in the build
496 # tree.  It is safe to modify the result.
497 def _build_env():
498     global buildtop, _runenv
499     env = os.environ.copy()
500     for (k, v) in _runenv.iteritems():
501         if v.find('./') == 0:
502             env[k] = os.path.join(buildtop, v)
503         else:
504             env[k] = v
505     # Make sure we don't get confused by translated messages.
506     env['LC_MESSAGES'] = 'C'
507     return env
508
509
510 def _import_runenv():
511     global buildtop
512     runenv_py = os.path.join(buildtop, 'runenv.py')
513     if not os.path.exists(runenv_py):
514         fail('You must run "make runenv.py" in %s first.' % buildtop)
515     module = imp.load_source('runenv', runenv_py)
516     return module.env
517
518
519 # Merge the nested dictionaries cfg1 and cfg2 into a new dictionary.
520 # cfg1 or cfg2 may be None, in which case the other is returned.  If
521 # cfg2 contains keys mapped to None, the corresponding keys will be
522 # mapped to None in the result.  The result may contain references to
523 # parts of cfg1 or cfg2, so is not safe to modify.
524 def _cfg_merge(cfg1, cfg2):
525     if not cfg2:
526         return cfg1
527     if not cfg1:
528         return cfg2
529     result = cfg1.copy()
530     for key, value2 in cfg2.items():
531         if value2 is None or key not in result:
532             result[key] = value2
533         else:
534             value1 = result[key]
535             if isinstance(value1, dict):
536                 if not isinstance(value2, dict):
537                     raise TypeError()
538                 result[key] = _cfg_merge(value1, value2)
539             else:
540                 result[key] = value2
541     return result
542
543
544 # Python gives us shlex.split() to turn a shell command into a list of
545 # arguments, but oddly enough, not the easier reverse operation.  For
546 # now, do a bad job of faking it.
547 def _shell_equiv(args):
548     return " ".join(args)
549
550
551 # Add a valgrind prefix to the front of args if specified in the
552 # environment.  Under normal circumstances this just returns args.
553 def _valgrind(args):
554     valgrind = os.getenv('VALGRIND')
555     if valgrind:
556         args = shlex.split(valgrind) + args
557     return args
558
559
560 def _stop_or_shell(stop, shell, env, ind):
561     if (_match_cmdnum(stop, ind)):
562         sys.stdout.write('*** [%d] Waiting for return: ' % ind)
563         sys.stdin.readline()
564     if (_match_cmdnum(shell, ind)):
565         output('*** [%d] Spawning shell\n' % ind, True)
566         subprocess.call(os.getenv('SHELL'), env=env)
567
568
569 def _run_cmd(args, env, input=None, expected_code=0):
570     global null_input, _cmd_index, _debug
571     global _stop_before, _stop_after, _shell_before, _shell_after
572
573     if (_match_cmdnum(_debug, _cmd_index)):
574         return _debug_cmd(args, env, input)
575
576     args = _valgrind(args)
577
578     output('*** [%d] Executing: %s\n' % (_cmd_index, _shell_equiv(args)))
579     _stop_or_shell(_stop_before, _shell_before, env, _cmd_index)
580
581     if input:
582         infile = subprocess.PIPE
583     else:
584         infile = null_input
585
586     # Run the command and log the result, folding stderr into stdout.
587     proc = subprocess.Popen(args, stdin=infile, stdout=subprocess.PIPE,
588                             stderr=subprocess.STDOUT, env=env)
589     (outdata, dummy_errdata) = proc.communicate(input)
590     code = proc.returncode
591     output(outdata)
592     output('*** [%d] Completed with return code %d\n' % (_cmd_index, code))
593     _stop_or_shell(_stop_after, _shell_after, env, _cmd_index)
594     _cmd_index += 1
595
596     # Check the return code and return the output.
597     if code != expected_code:
598         fail('%s failed with code %d.' % (args[0], code))
599     return outdata
600
601
602 def _debug_cmd(args, env, input):
603     global _cmd_index, _debugger_command
604
605     args = _debugger_command + list(args)
606     output('*** [%d] Executing in debugger: %s\n' %
607            (_cmd_index, _shell_equiv(args)), True)
608     if input:
609         print
610         print '*** Enter the following input when appropriate:'
611         print 
612         print input
613         print
614     code = subprocess.call(args, env=env)
615     output('*** [%d] Completed in debugger with return code %d\n' %
616            (_cmd_index, code))
617     _cmd_index += 1
618
619
620 # Start a daemon process with the specified args and env.  Wait until
621 # we see sentinel as a substring of a line on either stdout or stderr.
622 # Clean up the daemon process on exit.
623 def _start_daemon(args, env, sentinel):
624     global null_input, _cmd_index, _debug
625     global _stop_before, _stop_after, _shell_before, _shell_after
626
627     # Make this non-fatal so that --debug=all works.
628     if (_match_cmdnum(_debug, _cmd_index)):
629         output('*** [%d] Cannot run daemon in debugger\n' % _cmd_index, True)
630
631     args = _valgrind(args)
632     output('*** [%d] Starting: %s\n' %
633            (_cmd_index, _shell_equiv(args)))
634     _stop_or_shell(_stop_before, _shell_before, env, _cmd_index)
635
636     # Start the daemon and look for the sentinel in stdout or stderr.
637     proc = subprocess.Popen(args, stdin=null_input, stdout=subprocess.PIPE,
638                             stderr=subprocess.STDOUT, env=env)
639     while True:
640         line = proc.stdout.readline()
641         if line == "":
642             code = proc.wait()
643             fail('%s failed to start with code %d.' % (args[0], code))
644         output(line)
645         if sentinel in line:
646             break
647     output('*** [%d] Started with pid %d\n' % (_cmd_index, proc.pid))
648     _stop_or_shell(_stop_after, _shell_after, env, _cmd_index)
649     _cmd_index += 1
650
651     # Save the daemon in a list for cleanup.  Note that we won't read
652     # any more of the daemon's output after the sentinel, which will
653     # cause the daemon to block if it generates enough.  For now we
654     # assume all daemon processes are quiet enough to avoid this
655     # problem.  If it causes an issue, some alternatives are:
656     #   - Output to a file and poll the file for the sentinel
657     #     (undesirable because it slows down the test suite by the
658     #     polling interval times the number of daemons started)
659     #   - Create an intermediate subprocess which discards output
660     #     after the sentinel.
661     _daemons.append(proc)
662
663     # Return the process; the caller can stop it with stop_daemon.
664     return proc
665
666
667 def stop_daemon(proc):
668     output('*** Terminating process %d\n' % proc.pid)
669     os.kill(proc.pid, signal.SIGTERM)
670     proc.wait()
671     _daemons.remove(proc)
672
673
674 class K5Realm(object):
675     """An object representing a functional krb5 test realm."""
676
677     def __init__(self, realm='KRBTEST.COM', portbase=61000, testdir='testdir',
678                  krb5_conf=None, kdc_conf=None, create_kdb=True,
679                  krbtgt_keysalt=None, create_user=True, get_creds=True,
680                  create_host=True, start_kdc=True, start_kadmind=True):
681         global hostname, _default_krb5_conf, _default_kdc_conf
682
683         self.realm = realm
684         self.testdir = os.path.join(os.getcwd(), testdir)
685         self.portbase = portbase
686         self.user_princ = 'user@' + self.realm
687         self.admin_princ = 'user/admin@' + self.realm
688         self.host_princ = 'host/%s@%s' % (hostname, self.realm)
689         self.krbtgt_princ = 'krbtgt/%s@%s' % (self.realm, self.realm)
690         self.keytab = os.path.join(self.testdir, 'keytab')
691         self.ccache = os.path.join(self.testdir, 'ccache')
692         self._krb5_conf = _cfg_merge(_default_krb5_conf, krb5_conf)
693         self._kdc_conf = _cfg_merge(_default_kdc_conf, kdc_conf)
694         self._kdc_proc = None
695         self._kadmind_proc = None
696
697         self._create_empty_dir()
698         self._create_krb5_conf('client')
699         self._create_krb5_conf('server')
700         self._create_krb5_conf('master')
701         self._create_krb5_conf('slave')
702         self._create_kdc_conf('master')
703         self._create_kdc_conf('slave')
704         self._create_acl()
705         self._create_dictfile()
706
707         self.env_client = self._make_env('client', False)
708         self.env_server = self._make_env('server', False)
709         self.env_master = self._make_env('master', True)
710         self.env_slave = self._make_env('slave', True)
711
712         if create_kdb:
713             self.create_kdb()
714         if krbtgt_keysalt and create_kdb:
715             self.run_kadminl('cpw -randkey -e %s %s' %
716                              (krbtgt_keysalt, self.krbtgt_princ))
717         if create_user and create_kdb:
718             self.addprinc(self.user_princ, password('user'))
719             self.addprinc(self.admin_princ, password('admin'))
720         if create_host and create_kdb:
721             self.addprinc(self.host_princ)
722             self.extract_keytab(self.host_princ, self.keytab)
723         if start_kdc and create_kdb:
724             self.start_kdc()
725         if start_kadmind and create_kdb:
726             self.start_kadmind()
727         if get_creds and create_kdb and create_user and start_kdc:
728             self.kinit(self.user_princ, password('user'))
729             self.klist(self.user_princ)
730
731     def _create_empty_dir(self):
732         dir = self.testdir
733         shutil.rmtree(dir, True)
734         if (os.path.exists(dir)):
735             fail('Cannot remove %s to create test realm.' % dir)
736         os.mkdir(dir)
737
738     def _create_krb5_conf(self, type):
739         filename = os.path.join(self.testdir, 'krb5.%s.conf' % type)
740         file = open(filename, 'w')
741         profile = _cfg_merge(self._krb5_conf['all'], self._krb5_conf.get(type))
742         for section, contents in profile.items():
743             file.write('[%s]\n' % section)
744             self._write_cfg_section(file, type, contents, 1)
745         file.close()
746
747     def _create_kdc_conf(self, type):
748         filename = os.path.join(self.testdir, 'kdc.%s.conf' % type)
749         file = open(filename, 'w')
750         profile = _cfg_merge(self._kdc_conf['all'], self._kdc_conf.get(type))
751         for section, contents in profile.items():
752             file.write('[%s]\n' % section)
753             self._write_cfg_section(file, type, contents, 1)
754         file.close()
755
756     def _write_cfg_section(self, file, type, contents, indent_level):
757         indent = '\t' * indent_level
758         for name, value in contents.items():
759             name = self._subst_cfg_value(name, type)
760             if isinstance(value, dict):
761                 # A dictionary value yields a list subsection.
762                 file.write('%s%s = {\n' % (indent, name))
763                 self._write_cfg_section(file, type, value, indent_level + 1)
764                 file.write('%s}\n' % indent)
765             elif isinstance(value, list):
766                 # A list value yields multiple values for the same name.
767                 for item in value:
768                     item = self._subst_cfg_value(item, type)
769                     file.write('%s%s = %s\n' % (indent, name, item))
770             elif isinstance(value, str):
771                 # A string value yields a straightforward variable setting.
772                 value = self._subst_cfg_value(value, type)
773                 file.write('%s%s = %s\n' % (indent, name, value))
774             elif value is not None:
775                 raise TypeError()
776
777     def _subst_cfg_value(self, value, type):
778         global buildtop, srctop, hostname
779         template = string.Template(value)
780         return template.substitute(type=type,
781                                    realm=self.realm,
782                                    testdir=self.testdir,
783                                    buildtop=buildtop,
784                                    srctop=srctop,
785                                    plugins=plugins,
786                                    hostname=hostname,
787                                    port0=self.portbase,
788                                    port1=self.portbase + 1,
789                                    port2=self.portbase + 2,
790                                    port3=self.portbase + 3,
791                                    port4=self.portbase + 4,
792                                    port5=self.portbase + 5,
793                                    port6=self.portbase + 6,
794                                    port7=self.portbase + 7,
795                                    port8=self.portbase + 8,
796                                    port9=self.portbase + 9)
797
798     def _create_acl(self):
799         global hostname
800         filename = os.path.join(self.testdir, 'acl')
801         file = open(filename, 'w')
802         file.write('%s *\n' % self.admin_princ)
803         file.write('kiprop/%s@%s p\n' % (hostname, self.realm))
804         file.close()
805
806     def _create_dictfile(self):
807         filename = os.path.join(self.testdir, 'dictfile')
808         file = open(filename, 'w')
809         file.write('weak_password\n')
810         file.close()
811
812     def _make_env(self, type, has_kdc_conf):
813         env = _build_env()
814         env['KRB5_CONFIG'] = os.path.join(self.testdir, 'krb5.%s.conf' % type)
815         if has_kdc_conf:
816             filename = os.path.join(self.testdir, 'kdc.%s.conf' % type)
817             env['KRB5_KDC_PROFILE'] = filename
818         env['KRB5CCNAME'] = self.ccache
819         env['KRB5_KTNAME'] = self.keytab
820         env['KRB5RCACHEDIR'] = self.testdir
821         return env
822
823     def run_as_client(self, args, **keywords):
824         return _run_cmd(args, self.env_client, **keywords)
825
826     def run_as_server(self, args, **keywords):
827         return _run_cmd(args, self.env_server, **keywords)
828
829     def run_as_master(self, args, **keywords):
830         return _run_cmd(args, self.env_master, **keywords)
831
832     def run_as_slave(self, args, **keywords):
833         return _run_cmd(args, self.env_slave, **keywords)
834
835     def server_port(self):
836         return self.portbase + 3
837
838     def start_server(self, args, sentinel):
839         return _start_daemon(args, self.env_server, sentinel)
840
841     def start_in_inetd(self, args, port=None):
842         if not port:
843             port = self.server_port()
844         inetd_args = [t_inetd, str(port)] + args
845         return _start_daemon(inetd_args, self.env_server, 'Ready!')
846
847     def create_kdb(self):
848         global kdb5_util
849         self.run_as_master([kdb5_util, 'create', '-W', '-s', '-P', 'master'])
850
851     def start_kdc(self, args=[]):
852         global krb5kdc
853         assert(self._kdc_proc is None)
854         self._kdc_proc = _start_daemon([krb5kdc, '-n'] + args, self.env_master,
855                                         'starting...')
856
857     def stop_kdc(self):
858         assert(self._kdc_proc is not None)
859         stop_daemon(self._kdc_proc)
860         self._kdc_proc = None
861
862     def start_kadmind(self):
863         global krb5kdc
864         assert(self._kadmind_proc is None)
865         self._kadmind_proc = _start_daemon([kadmind, '-nofork', '-W'],
866                                             self.env_master, 'starting...')
867
868     def stop_kadmind(self):
869         assert(self._kadmind_proc is not None)
870         stop_daemon(self._kadmind_proc)
871         self._kadmind_proc = None
872
873     def stop(self):
874         if self._kdc_proc:
875             self.stop_kdc()
876         if self._kadmind_proc:
877             self.stop_kadmind()
878
879     def addprinc(self, princname, password=None):
880         if password:
881             self.run_kadminl('addprinc -pw %s %s' % (password, princname))
882         else:
883             self.run_kadminl('addprinc -randkey %s' % princname)
884
885     def extract_keytab(self, princname, keytab):
886         self.run_kadminl('ktadd -k %s -norandkey %s' % (keytab, princname))
887
888     def kinit(self, princname, password=None, flags=[], **keywords):
889         if password:
890             input = password + "\n"
891         else:
892             input = None
893         return self.run_as_client([kinit] + flags + [princname], input=input,
894                                   **keywords)
895
896     def klist(self, client_princ, service_princ=None, ccache=None, **keywords):
897         if service_princ is None:
898             service_princ = self.krbtgt_princ
899         if ccache is None:
900             ccache = self.ccache
901         output = self.run_as_client([klist, ccache], **keywords)
902         if (('Ticket cache: FILE:%s\n' % ccache) not in output or
903             ('Default principal: %s\n' % client_princ) not in output or
904             service_princ not in output):
905             fail('Unexpected klist output.')
906
907     def klist_keytab(self, princ, keytab=None, **keywords):
908         if keytab is None:
909             keytab = self.keytab
910         output = self.run_as_client([klist, '-k', keytab], **keywords)
911         if (('Keytab name: FILE:%s\n' % keytab) not in output or
912             'KVNO Principal\n----' not in output or
913             princ not in output):
914             fail('Unexpected klist output.')
915
916     def run_kadminl(self, query):
917         global kadmin_local
918         return self.run_as_master([kadmin_local, '-q', query])
919
920
921 def multipass_realms(**keywords):
922     global _current_pass, _passes, testpass
923     caller_krb5_conf = keywords.get('krb5_conf')
924     caller_kdc_conf = keywords.get('kdc_conf')
925     for p in _passes:
926         (name, krbtgt_keysalt, krb5_conf, kdc_conf) = p
927         if testpass and name != testpass:
928             continue
929         output('*** Beginning pass %s\n' % name)
930         keywords['krb5_conf'] = _cfg_merge(krb5_conf, caller_krb5_conf)
931         keywords['kdc_conf'] = _cfg_merge(kdc_conf, caller_kdc_conf)
932         keywords['krbtgt_keysalt'] = krbtgt_keysalt
933         _current_pass = name
934         realm = K5Realm(**keywords)
935         yield realm
936         realm.stop()
937         _current_pass = None
938
939
940 def cross_realms(num, xtgts=None, args=None, **keywords):
941     # Build keyword args for each realm.
942     realm_args = []
943     for i in range(num):
944         realmnumber = i + 1
945         # Start with any global keyword arguments to this function.
946         a = keywords.copy()
947         if args and args[i]:
948             # Merge in specific arguments for this realm.  Use
949             # _cfg_merge for config fragments.
950             a.update(args[i])
951             for cf in ('krb5_conf', 'kdc_conf'):
952                 if cf in keywords and cf in args[i]:
953                     a[cf] = _cfg_merge(keywords[cf], args[i][cf])
954         # Set defaults for the realm name, testdir, and portbase.
955         if not 'realm' in a:
956             a['realm'] = 'KRBTEST%d.COM' % realmnumber
957         if not 'testdir' in a:
958             a['testdir'] = os.path.join('testdir', str(realmnumber))
959         if not 'portbase' in a:
960             a['portbase'] = 61000 + 10 * realmnumber
961         realm_args.append(a)
962         
963     # Build a [realms] config fragment containing all of the realms.
964     realmsection = { '$realm' : None }
965     for a in realm_args:
966         name = a['realm']
967         portbase = a['portbase']
968         realmsection[name] = {
969             'kdc' : '$hostname:%d' % portbase,
970             'admin_server' : '$hostname:%d' % (portbase + 1),
971             'kpasswd_server' : '$hostname:%d' % (portbase + 2)
972             }
973     realmscfg = { 'all' : { 'realms' : realmsection } }
974
975     # Set realmsection in each realm's krb5_conf keyword argument.
976     for a in realm_args:
977         a['krb5_conf'] = _cfg_merge(realmscfg, a.get('krb5_conf'))
978
979     if xtgts is None:
980         # Default to cross tgts for every pair of realms.
981         # (itertools.permutations would work here but is new in 2.6.)
982         xtgts = [(x,y) for x in range(num) for y in range(num) if x != y]
983
984     # Create the realms.
985     realms = []
986     for i in range(num):
987         r = K5Realm(**realm_args[i])
988         # Create specified cross TGTs in this realm's db.
989         for j in range(num):
990             if j == i:
991                 continue
992             iname = r.realm
993             jname = realm_args[j]['realm']
994             if (i, j) in xtgts:
995                 # This realm can authenticate to realm j.
996                 r.addprinc('krbtgt/%s' % jname, password('cr-%d-%d-' % (i, j)))
997             if (j, i) in xtgts:
998                 # Realm j can authenticate to this realm.
999                 r.addprinc('krbtgt/%s@%s' % (iname, jname),
1000                            password('cr-%d-%d-' % (j, i)))
1001         realms.append(r)
1002     return realms
1003
1004
1005 _default_krb5_conf = {
1006     'all' : {
1007         'libdefaults' : {
1008             'default_realm' : '$realm',
1009             'dns_lookup_kdc' : 'false',
1010             'plugin_base_dir' : '$plugins'
1011         },
1012         'realms' : {
1013             '$realm' : {
1014                 'kdc' : '$hostname:$port0',
1015                 'admin_server' : '$hostname:$port1',
1016                 'kpasswd_server' : '$hostname:$port2'
1017             }
1018         }
1019     }
1020 }
1021
1022
1023 _default_kdc_conf = {
1024     'all' : {
1025         'realms' : {
1026             '$realm' : {
1027                 'database_module' : 'foo_db2'
1028             }
1029         },
1030         'dbmodules' : {
1031             'db_module_dir' : '$plugins/kdb',
1032             'foo_db2' : {
1033                 'db_library' : 'db2',
1034                 'database_name' : '$testdir/$type-db'
1035             }
1036         },
1037         'logging' : {
1038             'admin_server' : 'FILE:$testdir/kadmind5.log',
1039             'kdc' : 'FILE:$testdir/kdc.log',
1040             'default' : 'FILE:$testdir/others.log'
1041         }
1042     },
1043     'master' : {
1044         'realms' : {
1045             '$realm' : {
1046                 'key_stash_file' : '$testdir/stash',
1047                 'acl_file' : '$testdir/acl',
1048                 'dictfile' : '$testdir/dictfile',
1049                 'kadmind_port' : '$port1',
1050                 'kpasswd_port' : '$port2',
1051                 'kdc_ports' : '$port0',
1052                 'kdc_tcp_ports' : '$port0'
1053             }
1054         }
1055     },
1056     'slave' : {
1057         'realms' : {
1058             '$realm' : {
1059                 'key_stash_file' : '$testdir/slave-stash',
1060             }
1061         }
1062     }
1063 }
1064
1065
1066 # A pass is a tuple of: name, krbtgt_keysalt, krb5_conf, kdc_conf.
1067 _passes = [
1068     # No special settings; exercises AES256.
1069     ('default', None, None, None),
1070
1071     # Exercise a DES enctype and the v4 salt type.
1072     ('desv4', None,
1073      {'all' : {'libdefaults' : {
1074                     'default_tgs_enctypes' : 'des-cbc-crc',
1075                     'default_tkt_enctypes' : 'des-cbc-crc',
1076                     'permitted_enctypes' : 'des-cbc-crc',
1077                     'allow_weak_crypto' : 'true'}}},
1078      {'master' : {'realms' : {'$realm' : {
1079                         'supported_enctypes' : 'des-cbc-crc:v4',
1080                         'master_key_type' : 'des-cbc-crc'}}}}),
1081
1082     # Exercise the DES3 enctype.
1083     ('des3', None,
1084      {'all' : {'libdefaults' : {
1085                     'default_tgs_enctypes' : 'des3',
1086                     'default_tkt_enctypes' : 'des3',
1087                     'permitted_enctypes' : 'des3'}}},
1088      {'master' : {'realms' : {'$realm' : {
1089                         'supported_enctypes' : 'des3-cbc-sha1:normal',
1090                         'master_key_type' : 'des3-cbc-sha1'}}}}),
1091
1092     # Exercise the arcfour enctype.
1093     ('arcfour', None,
1094      {'all' : {'libdefaults' : {
1095                     'default_tgs_enctypes' : 'rc4',
1096                     'default_tkt_enctypes' : 'rc4',
1097                     'permitted_enctypes' : 'rc4'}}},
1098      {'master' : {'realms' : {'$realm' : {
1099                         'supported_enctypes' : 'arcfour-hmac:normal',
1100                         'master_key_type' : 'arcfour-hmac'}}}}),
1101
1102     # Exercise the AES128 enctype.
1103     ('aes128', None,
1104       {'all' : {'libdefaults' : {
1105                     'default_tgs_enctypes' : 'aes128-cts',
1106                     'default_tkt_enctypes' : 'aes128-cts',
1107                     'permitted_enctypes' : 'aes128-cts'}}},
1108       {'master' : {'realms' : {'$realm' : {
1109                         'supported_enctypes' : 'aes128-cts:normal',
1110                         'master_key_type' : 'aes128-cts'}}}}),
1111
1112     # Exercise the camellia256-cts enctype.
1113 # Enable when Camellia support becomes unconditional.
1114 #    ('camellia256', None,
1115 #      {'all' : {'libdefaults' : {
1116 #                    'default_tgs_enctypes' : 'camellia256-cts',
1117 #                    'default_tkt_enctypes' : 'camellia256-cts',
1118 #                    'permitted_enctypes' : 'camellia256-cts'}}},
1119 #      {'master' : {'realms' : {'$realm' : {
1120 #                        'supported_enctypes' : 'camellia256-cts:normal',
1121 #                        'master_key_type' : 'camellia256-cts'}}}}),
1122
1123     # Test a setup with modern principal keys but an old TGT key.
1124     ('aes256.destgt', 'des-cbc-crc:normal',
1125      {'all' : {'libdefaults' : {'allow_weak_crypto' : 'true'}}},
1126      None)
1127 ]
1128
1129 _success = False
1130 _current_pass = None
1131 _daemons = []
1132 _parse_args()
1133 atexit.register(_onexit)
1134 _outfile = open('testlog', 'w')
1135 _cmd_index = 1
1136 buildtop = _find_buildtop()
1137 srctop = _find_srctop()
1138 plugins = os.path.join(buildtop, 'plugins')
1139 _runenv = _import_runenv()
1140 hostname = _get_hostname()
1141 null_input = open(os.devnull, 'r')
1142
1143 krb5kdc = os.path.join(buildtop, 'kdc', 'krb5kdc')
1144 kadmind = os.path.join(buildtop, 'kadmin', 'server', 'kadmind')
1145 kadmin = os.path.join(buildtop, 'kadmin', 'cli', 'kadmin')
1146 kadmin_local = os.path.join(buildtop, 'kadmin', 'cli', 'kadmin.local')
1147 kdb5_util = os.path.join(buildtop, 'kadmin', 'dbutil', 'kdb5_util')
1148 ktutil = os.path.join(buildtop, 'kadmin', 'ktutil', 'ktutil')
1149 kinit = os.path.join(buildtop, 'clients', 'kinit', 'kinit')
1150 klist = os.path.join(buildtop, 'clients', 'klist', 'klist')
1151 kswitch = os.path.join(buildtop, 'clients', 'kswitch', 'kswitch')
1152 kvno = os.path.join(buildtop, 'clients', 'kvno', 'kvno')
1153 kdestroy = os.path.join(buildtop, 'clients', 'kdestroy', 'kdestroy')
1154 kpasswd = os.path.join(buildtop, 'clients', 'kpasswd', 'kpasswd')
1155 t_inetd = os.path.join(buildtop, 'tests', 'dejagnu', 't_inetd')
1156 kproplog = os.path.join(buildtop, 'slave', 'kproplog')
1157 kpropd = os.path.join(buildtop, 'slave', 'kpropd')
1158 kprop = os.path.join(buildtop, 'slave', 'kprop')
1159
1160 # Currently there are no helpers for doing cross-realm testing, but
1161 # the necessary flexibility is present in K5Realm to create them.  A
1162 # cross-realm test setup would need to:
1163 #
1164 # * Select distinct realm names, port bases, and directories for each
1165 #   realm.
1166
1167 # * Create a krb5_conf fragment with a comprehensive [realms] section
1168 #   so that each realm knows how to reach the others, since there
1169 #   won't be DNS SRV records.  The fragment should probably None out
1170 #   'realms' -> '$realm' to avoid a duplicate section for the home
1171 #   realm.  capaths configuration may also be desired for some test
1172 #   cases.
1173 #
1174 # * Create cross-TGS principals for some or all of the pairs of
1175 #   realms.