1 # Copyright (C) 2010 by the Massachusetts Institute of Technology.
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.
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.
23 """A module for krb5 test scripts
25 To run test scripts during "make check" (if Python 2.5 or later is
26 available), add rules like the following to Makefile.in:
29 $(RUNPYTEST) $(srcdir)/t_testname.py $(PYTESTFLAGS)
35 # Run a test program under a variety of configurations:
36 for realm in multipass_realms():
37 realm.run(['./testprog', 'arg'])
39 # Run a test server and client under just the default configuration:
41 realm.start_server(['./serverprog'], 'starting...')
42 realm.run(['./clientprog', realm.host_princ])
44 # Inform framework that tests completed successfully.
45 success('World peace and cure for cancer')
47 By default, the realm will have:
49 * The name KRBTEST.COM
50 * Listener ports starting at 61000
51 * krb5.conf and kdc.conf files
53 * Running krb5kdc (but not kadmind)
54 * Principals named realm.user_princ and realm.admin_princ; call
55 password('user') and password('admin') to get the password
56 * Credentials for realm.user_princ in realm.ccache
57 * Admin rights for realm.admin_princ in the kadmind acl file
58 * A host principal named realm.host_princ with a random key
59 * A keytab for the host principal in realm.keytab
61 The realm's behaviour can be modified with the following constructor
64 * realm='realmname': Override the realm name
66 * portbase=NNN: Override the listener port base; currently three ports are
69 * testdir='dirname': Override the storage area for the realm's files
70 (path may be specified relative to the current working dir)
72 * krb5_conf={ ... }: krb5.conf options, expressed as a nested
73 dictionary, to be merged with the default krb5.conf settings. A key
74 may be mapped to None to delete a setting from the defaults. A key
75 may be mapped to a list in order to create multiple settings for the
76 same variable name. Keys and values undergo the following template
79 - $realm: The realm name
80 - $testdir: The realm storage directory (absolute path)
81 - $buildtop: The root of the build directory
82 - $srctop: The root of the source directory
83 - $plugins: The plugin directory in the build tree
84 - $hostname: The FQDN of the host
85 - $port0: The first listener port (portbase)
87 - $port9: The tenth listener port (portbase + 9)
89 When choosing ports, note the following:
91 - port0 is used in the default krb5.conf for the KDC
92 - port1 is used in the default krb5.conf for kadmind
93 - port2 is used in the default krb5.conf for kpasswd
94 - port3 is used in the default krb5.conf for kpropd
95 - port4 is used in the default krb5.conf for iprop (in kadmind)
96 - port5 is the return value of realm.server_port()
98 * kdc_conf={...}: kdc.conf options, expressed as a nested dictionary,
99 to be merged with the default kdc.conf settings. The same
100 conventions and substitutions for krb5_conf apply.
102 * create_kdb=False: Don't create a KDB. Implicitly disables all of
103 the other options since they all require a KDB.
105 * krbtgt_keysalt='enctype:salttype': After creating the KDB,
106 regenerate the krbtgt key using the specified key/salt combination,
107 using a kadmin.local cpw query.
109 * create_user=False: Don't create the user principal. Implies
112 * create_host=False: Don't create the host principal or the associated
115 * start_kdc=False: Don't start the KDC. Implies get_creds=False.
117 * start_kadmind=True: Start kadmind.
119 * get_creds=False: Don't get user credentials.
121 Scripts may use the following functions and variables:
123 * fail(message): Display message (plus leading marker and trailing
124 newline) and explanatory messages about debugging.
126 * success(message): Indicate that the test script has completed
127 successfully. Suppresses the display of explanatory debugging
128 messages in the on-exit handler. message should briefly summarize
129 the operations tested; it will only be displayed (with leading
130 marker and trailing newline) if the script is running verbosely.
132 * skipped(whatmsg, whymsg): Indicate that some tests were skipped.
133 whatmsg should concisely say what was skipped (e.g. "LDAP KDB
134 tests") and whymsg should give the reason (e.g. "because LDAP module
137 * skip_rest(message): Indicate that some tests were skipped, then exit
140 * output(message, force_verbose=False): Place message (without any
141 added newline) in testlog, and write it to stdout if running
144 * which(progname): Return the location of progname in the executable
145 path, or None if it is not found.
147 * password(name): Return a weakly random password based on name. The
148 password will be consistent across calls with the same name.
150 * stop_daemon(proc): Stop a daemon process started with
151 realm.start_server() or realm.start_in_inetd(). Only necessary if
152 the port needs to be reused; daemon processes will be stopped
153 automatically when the script exits.
155 * multipass_realms(**keywords): This is an iterator function. Yields
156 a realm for each of the standard test passes, each of which alters
157 the default configuration in some way to exercise different parts of
158 the krb5 code base. keywords may contain any K5Realm initializer
159 keyword with the exception of krbtgt_keysalt, which will not be
160 honored. If keywords contains krb5_conf and/or kdc_conf fragments,
161 they will be merged with the default and per-pass specifications.
163 * cross_realms(num, xtgts=None, args=None, **keywords): This function
164 returns a list of num realms, where each realm's configuration knows
165 how to contact all of the realms. By default, each realm will
166 contain cross TGTs in both directions for all other realms; this
167 default may be overridden by specifying a collection of tuples in
168 the xtgts parameter, where each tuple is a pair of zero-based realm
169 indexes, indicating that the first realm can authenticate to the
170 second (i.e. krbtgt/secondrealm@firstrealm exists in both realm's
171 databases). If args is given, it should be a list of keyword
172 arguments specific to each realm; these will be merged with the
173 global keyword arguments passed to cross_realms, with specific
174 arguments taking priority.
176 * buildtop: The top of the build directory (absolute path).
178 * srctop: The top of the source directory (absolute path).
180 * plugins: The plugin directory in the build tree (absolute path).
182 * hostname: This machine's fully-qualified domain name.
184 * null_input: A file opened to read /dev/null.
186 * args: Positional arguments left over after flags are processed.
188 * runenv: The contents of $srctop/runenv.py, containing a dictionary
189 'env' which specifies additional variables to be added to the realm
190 environment, and a variable 'tls_impl', which indicates which TLS
191 implementation (if any) is being used by libkrb5's support for
192 contacting KDCs and kpasswd servers over HTTPS.
194 * verbose: Whether the script is running verbosely.
196 * testpass: The command-line test pass argument. The script does not
197 need to examine this argument in most cases; it will be honored in
200 * Pathname variables for programs within the build directory:
204 - kadminl (kadmin.local)
219 Scripts may use the following realm methods and attributes:
221 * realm.run(args, env=None, **keywords): Run a command in a specified
222 environment (or the realm's environment by default), obeying the
223 command-line debugging options. Fail if the command does not return
224 0. Log the command output appropriately, and return it as a single
225 multi-line string. Keyword arguments can contain input='string' to
226 send an input string to the command, and expected_code=N to expect a
227 return code other than 0.
229 * realm.kprop_port(): Returns a port number based on realm.portbase
230 intended for use by kprop and kpropd.
232 * realm.server_port(): Returns a port number based on realm.portbase
233 intended for use by server processes.
235 * realm.start_server(args, sentinel, env=None): Start a daemon
236 process. Wait until sentinel appears as a substring of a line in
237 the server process's stdout or stderr (which are folded together).
238 Returns a subprocess.Popen object which can be passed to
239 stop_daemon() to stop the server, or used to read from the server's
242 * realm.start_in_inetd(args, port=None, env=None): Begin a t_inetd
243 process which will spawn a server process after accepting a client
244 connection. If port is not specified, realm.server_port() will be
245 used. Returns a process object which can be passed to stop_daemon()
248 * realm.create_kdb(): Create a new KDB.
250 * realm.start_kdc(args=[], env=None): Start a krb5kdc process. Errors
251 if a KDC is already running. If args is given, it contains a list
252 of additional krb5kdc arguments.
254 * realm.stop_kdc(): Stop the krb5kdc process. Errors if no KDC is
257 * realm.start_kadmind(env=None): Start a kadmind process. Errors if a
258 kadmind is already running.
260 * realm.stop_kadmind(): Stop the kadmind process. Errors if no
263 * realm.stop(): Stop any daemon processes running on behalf of the
266 * realm.addprinc(princname, password=None): Using kadmin.local, create
267 a principal in the KDB named princname, with either a random or
270 * realm.extract_keytab(princname, keytab): Using kadmin.local, create
271 a keytab for princname in the filename keytab. Uses the -norandkey
272 option to avoid re-randomizing princname's key.
274 * realm.kinit(princname, password=None, flags=[]): Acquire credentials
275 for princname using kinit, with additional flags []. If password is
276 specified, it will be used as input to the kinit process; otherwise
277 flags must cause kinit not to need a password (e.g. by specifying a
280 * realm.klist(client_princ, service_princ=None, ccache=None): Using
281 klist, list the credentials cache ccache (must be a filename;
282 self.ccache if not specified) and verify that the output shows
283 credentials for client_princ and service_princ (self.krbtgt_princ if
286 * realm.klist_keytab(princ, keytab=None): Using klist, list keytab
287 (must be a filename; self.keytab if not specified) and verify that
288 the output shows the keytab name and principal name.
290 * realm.prep_kadmin(princname=None, password=None, flags=[]): Populate
291 realm.kadmin_ccache with a ticket which can be used to run kadmin.
292 If princname is not specified, realm.admin_princ and its default
293 password will be used.
295 * realm.run_kadmin(args, **keywords): Run the specified query in
296 kadmin, using realm.kadmin_ccache to authenticate. Accepts the same
297 keyword arguments as run.
299 * realm.special_env(name, has_kdc_conf, krb5_conf=None,
300 kdc_conf=None): Create an environment with a modified krb5.conf
301 and/or kdc.conf. The specified krb5_conf and kdc_conf fragments, if
302 any, will be merged with the realm's existing configuration. If
303 has_kdc_conf is false, the new environment will have no kdc.conf.
304 The environment returned by this method can be used with realm.run()
307 * realm.start_kpropd(env, args=[]): Start a kpropd process. Pass an
308 environment created with realm.special_env() for the slave. If args
309 is given, it contains a list of additional kpropd arguments.
310 Returns a handle to the kpropd process.
312 * realm.run_kpropd_once(env, args=[]): Run kpropd once, using the -t
313 flag. Pass an environment created with realm.special_env() for the
314 slave. If args is given, it contains a list of additional kpropd
315 arguments. Returns the kpropd output.
317 * realm.realm: The realm's name.
319 * realm.testdir: The realm's storage directory (absolute path).
321 * realm.portbase: The realm's first listener port.
323 * realm.user_princ: The principal name user@<realmname>.
325 * realm.admin_princ: The principal name user/admin@<realmname>.
327 * realm.host_princ: The name of the host principal for this machine,
330 * realm.nfs_princ: The name of the nfs principal for this machine,
333 * realm.krbtgt_princ: The name of the krbtgt principal for the realm.
335 * realm.keytab: A keytab file in realm.testdir. Initially contains a
336 host keytab unless disabled by the realm construction options.
338 * realm.client_keytab: A keytab file in realm.testdir. Initially
341 * realm.ccache: A ccache file in realm.testdir. Initially contains
342 credentials for user unless disabled by the realm construction
345 * realm.kadmin_ccache: The ccache file initialized by prep_kadmin and
348 * env: The realm's environment, extended from os.environ to point at
349 the realm's config files and the build tree's shared libraries.
351 When the test script is run, its behavior can be modified with
352 command-line flags. These are documented in the --help output.
368 # Used when most things go wrong (other than programming errors) so
369 # that the user sees an error message rather than a Python traceback,
370 # without help from the test script. The on-exit handler will display
371 # additional explanatory text.
373 """Print a message and exit with failure."""
375 print "*** Failure:", msg
377 print "*** Last command (#%d): %s" % (_cmd_index - 1, _last_cmd)
379 print "*** Output of last command:"
380 sys.stdout.write(_last_cmd_output)
382 print "*** Failed in test pass:", _current_pass
388 output('*** Success: %s\n' % msg)
392 def skipped(whatmsg, whymsg):
393 output('*** Skipping: %s: %s\n' % (whatmsg, whymsg), force_verbose=True)
394 f = open(os.path.join(buildtop, 'skiptests'), 'a')
395 f.write('Skipped %s: %s\n' % (whatmsg, whymsg))
399 def skip_rest(whatmsg, whymsg):
401 skipped(whatmsg, whymsg)
406 def output(msg, force_verbose=False):
407 """Output a message to testlog, and to stdout if running verbosely."""
409 if verbose or force_verbose:
410 sys.stdout.write(msg)
413 # Return the location of progname in the executable path, or None if
416 for dir in os.environ["PATH"].split(os.pathsep):
417 path = os.path.join(dir, progname)
418 if os.access(path, os.X_OK):
424 """Choose a weakly random password from name, consistent across calls."""
425 return name + str(os.getpid())
428 # Exit handler which ensures processes are cleaned up and, on failure,
429 # prints messages to help developers debug the problem.
431 global _daemons, _success, srctop, verbose
432 global _debug, _stop_before, _stop_after, _shell_before, _shell_after
434 # In Python 2.5, if we exit as a side-effect of importing
435 # k5test, _onexit will execute in an empty global namespace.
436 # This can happen if argument processing fails or the build
437 # root isn't valid. In this case we can safely assume that no
438 # daemons have been launched and that we don't really need to
439 # amend the error message. The bug is fixed in Python 2.6.
441 if _debug or _stop_before or _stop_after or _shell_before or _shell_after:
442 # Wait before killing daemons in case one is being debugged.
443 sys.stdout.write('*** Press return to kill daemons and exit script: ')
445 for proc in _daemons:
446 os.kill(proc.pid, signal.SIGTERM)
450 testlogfile = os.path.join(os.getcwd(), 'testlog')
451 utildir = os.path.join(srctop, 'util')
452 print 'For details, see: %s' % testlogfile
453 print 'Or re-run this test script with the -v flag:'
454 print ' cd %s' % os.getcwd()
455 print ' PYTHONPATH=%s %s %s -v' % \
456 (utildir, sys.executable, sys.argv[0])
458 print 'Use --debug=NUM to run a command under a debugger. Use'
459 print '--stop-after=NUM to stop after a daemon is started in order to'
460 print 'attach to it with a debugger. Use --help to see other options.'
463 def _onsigint(signum, frame):
464 # Exit without displaying a stack trace. Suppress messages from _onexit.
470 # Find the parent of dir which is at the root of a build or source directory.
473 if os.path.exists(os.path.join(dir, 'lib', 'krb5', 'krb')):
475 parent = os.path.dirname(dir)
482 def _find_buildtop():
483 root = _find_root(os.getcwd())
485 fail('Cannot find root of krb5 build directory.')
486 if not os.path.exists(os.path.join(root, 'config.status')):
487 # Looks like an unbuilt source directory.
488 fail('This script must be run inside a krb5 build directory.')
493 scriptdir = os.path.abspath(os.path.dirname(sys.argv[0]))
495 scriptdir = os.getcwd()
496 root = _find_root(scriptdir)
498 fail('Cannot find root of krb5 source directory.')
499 return os.path.abspath(root)
502 # Return the local hostname as it will be canonicalized by
503 # krb5_sname_to_principal. We can't simply use socket.getfqdn()
504 # because it explicitly prefers results containing periods and
505 # krb5_sname_to_principal doesn't care.
507 hostname = socket.gethostname()
509 ai = socket.getaddrinfo(hostname, None, 0, 0, 0, socket.AI_CANONNAME)
510 except socket.gaierror, (error, errstr):
511 fail('Local hostname "%s" does not resolve: %s.' % (hostname, errstr))
512 (family, socktype, proto, canonname, sockaddr) = ai[0]
514 name = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD)
515 except socket.gaierror:
516 return canonname.lower()
517 return name[0].lower()
519 # Parse command line arguments, setting global option variables. Also
520 # sets the global variable args to the positional arguments, which may
521 # be used by the test script.
523 global args, verbose, testpass, _debug, _debugger_command
524 global _stop_before, _stop_after, _shell_before, _shell_after
525 parser = optparse.OptionParser()
526 parser.add_option('-v', '--verbose', action='store_true', dest='verbose',
527 default=False, help='Display verbose output')
528 parser.add_option('-p', '--pass', dest='testpass', metavar='PASS',
529 help='If a multi-pass test, run only PASS')
530 parser.add_option('--debug', dest='debug', metavar='NUM',
531 help='Debug numbered command (or "all")')
532 parser.add_option('--debugger', dest='debugger', metavar='COMMAND',
533 help='Debugger command (default is gdb --args)',
534 default='gdb --args')
535 parser.add_option('--stop-before', dest='stopb', metavar='NUM',
536 help='Stop before numbered command (or "all")')
537 parser.add_option('--stop-after', dest='stopa', metavar='NUM',
538 help='Stop after numbered command (or "all")')
539 parser.add_option('--shell-before', dest='shellb', metavar='NUM',
540 help='Spawn shell before numbered command (or "all")')
541 parser.add_option('--shell-after', dest='shella', metavar='NUM',
542 help='Spawn shell after numbered command (or "all")')
543 (options, args) = parser.parse_args()
544 verbose = options.verbose
545 testpass = options.testpass
546 _debug = _parse_cmdnum('--debug', options.debug)
547 _debugger_command = shlex.split(options.debugger)
548 _stop_before = _parse_cmdnum('--stop-before', options.stopb)
549 _stop_after = _parse_cmdnum('--stop-after', options.stopa)
550 _shell_before = _parse_cmdnum('--shell-before', options.shellb)
551 _shell_after = _parse_cmdnum('--shell-after', options.shella)
554 # Translate a command number spec. -1 means all, None means none.
555 def _parse_cmdnum(optname, str):
563 fail('%s value must be "all" or a number' % optname)
566 # Test if a command index matches a translated command number spec.
567 def _match_cmdnum(cmdnum, ind):
576 # Return an environment suitable for running programs in the build
577 # tree. It is safe to modify the result.
579 global buildtop, runenv
580 env = os.environ.copy()
581 for (k, v) in runenv.env.iteritems():
582 if v.find('./') == 0:
583 env[k] = os.path.join(buildtop, v)
586 # Make sure we don't get confused by translated messages
587 # or localized times.
592 def _import_runenv():
594 runenv_py = os.path.join(buildtop, 'runenv.py')
595 if not os.path.exists(runenv_py):
596 fail('You must run "make runenv.py" in %s first.' % buildtop)
597 return imp.load_source('runenv', runenv_py)
600 # Merge the nested dictionaries cfg1 and cfg2 into a new dictionary.
601 # cfg1 or cfg2 may be None, in which case the other is returned. If
602 # cfg2 contains keys mapped to None, the corresponding keys will be
603 # mapped to None in the result. The result may contain references to
604 # parts of cfg1 or cfg2, so is not safe to modify.
605 def _cfg_merge(cfg1, cfg2):
611 for key, value2 in cfg2.items():
612 if value2 is None or key not in result:
616 if isinstance(value1, dict):
617 if not isinstance(value2, dict):
619 result[key] = _cfg_merge(value1, value2)
625 # Python gives us shlex.split() to turn a shell command into a list of
626 # arguments, but oddly enough, not the easier reverse operation. For
627 # now, do a bad job of faking it.
628 def _shell_equiv(args):
629 return " ".join(args)
632 # Add a valgrind prefix to the front of args if specified in the
633 # environment. Under normal circumstances this just returns args.
635 valgrind = os.getenv('VALGRIND')
637 args = shlex.split(valgrind) + args
641 def _stop_or_shell(stop, shell, env, ind):
642 if (_match_cmdnum(stop, ind)):
643 sys.stdout.write('*** [%d] Waiting for return: ' % ind)
645 if (_match_cmdnum(shell, ind)):
646 output('*** [%d] Spawning shell\n' % ind, True)
647 subprocess.call(os.getenv('SHELL'), env=env)
650 def _run_cmd(args, env, input=None, expected_code=0):
651 global null_input, _cmd_index, _last_cmd, _last_cmd_output, _debug
652 global _stop_before, _stop_after, _shell_before, _shell_after
654 if (_match_cmdnum(_debug, _cmd_index)):
655 return _debug_cmd(args, env, input)
657 args = _valgrind(args)
658 _last_cmd = _shell_equiv(args)
660 output('*** [%d] Executing: %s\n' % (_cmd_index, _last_cmd))
661 _stop_or_shell(_stop_before, _shell_before, env, _cmd_index)
664 infile = subprocess.PIPE
668 # Run the command and log the result, folding stderr into stdout.
669 proc = subprocess.Popen(args, stdin=infile, stdout=subprocess.PIPE,
670 stderr=subprocess.STDOUT, env=env)
671 (outdata, dummy_errdata) = proc.communicate(input)
672 _last_cmd_output = outdata
673 code = proc.returncode
675 output('*** [%d] Completed with return code %d\n' % (_cmd_index, code))
676 _stop_or_shell(_stop_after, _shell_after, env, _cmd_index)
679 # Check the return code and return the output.
680 if code != expected_code:
681 fail('%s failed with code %d.' % (args[0], code))
685 def _debug_cmd(args, env, input):
686 global _cmd_index, _debugger_command
688 args = _debugger_command + list(args)
689 output('*** [%d] Executing in debugger: %s\n' %
690 (_cmd_index, _shell_equiv(args)), True)
693 print '*** Enter the following input when appropriate:'
697 code = subprocess.call(args, env=env)
698 output('*** [%d] Completed in debugger with return code %d\n' %
703 # Start a daemon process with the specified args and env. Wait until
704 # we see sentinel as a substring of a line on either stdout or stderr.
705 # Clean up the daemon process on exit.
706 def _start_daemon(args, env, sentinel):
707 global null_input, _cmd_index, _last_cmd, _last_cmd_output, _debug
708 global _stop_before, _stop_after, _shell_before, _shell_after
710 if (_match_cmdnum(_debug, _cmd_index)):
711 output('*** [%d] Warning: ' % _cmd_index, True)
712 output( 'test script cannot proceed after debugging a daemon\n', True)
713 _debug_cmd(args, env, None)
714 output('*** Exiting after debugging daemon\n', True)
717 args = _valgrind(args)
718 _last_cmd = _shell_equiv(args)
719 output('*** [%d] Starting: %s\n' % (_cmd_index, _last_cmd))
720 _stop_or_shell(_stop_before, _shell_before, env, _cmd_index)
722 # Start the daemon and look for the sentinel in stdout or stderr.
723 proc = subprocess.Popen(args, stdin=null_input, stdout=subprocess.PIPE,
724 stderr=subprocess.STDOUT, env=env)
725 _last_cmd_output = ''
727 line = proc.stdout.readline()
728 _last_cmd_output += line
731 fail('%s failed to start with code %d.' % (args[0], code))
735 output('*** [%d] Started with pid %d\n' % (_cmd_index, proc.pid))
736 _stop_or_shell(_stop_after, _shell_after, env, _cmd_index)
739 # Save the daemon in a list for cleanup. Note that we won't read
740 # any more of the daemon's output after the sentinel, which will
741 # cause the daemon to block if it generates enough. For now we
742 # assume all daemon processes are quiet enough to avoid this
743 # problem. If it causes an issue, some alternatives are:
744 # - Output to a file and poll the file for the sentinel
745 # (undesirable because it slows down the test suite by the
746 # polling interval times the number of daemons started)
747 # - Create an intermediate subprocess which discards output
748 # after the sentinel.
749 _daemons.append(proc)
751 # Return the process; the caller can stop it with stop_daemon.
755 def stop_daemon(proc):
756 output('*** Terminating process %d\n' % proc.pid)
757 os.kill(proc.pid, signal.SIGTERM)
759 _daemons.remove(proc)
762 class K5Realm(object):
763 """An object representing a functional krb5 test realm."""
765 def __init__(self, realm='KRBTEST.COM', portbase=61000, testdir='testdir',
766 krb5_conf=None, kdc_conf=None, create_kdb=True,
767 krbtgt_keysalt=None, create_user=True, get_creds=True,
768 create_host=True, start_kdc=True, start_kadmind=False,
770 global hostname, _default_krb5_conf, _default_kdc_conf
773 self.testdir = os.path.join(os.getcwd(), testdir)
774 self.portbase = portbase
775 self.user_princ = 'user@' + self.realm
776 self.admin_princ = 'user/admin@' + self.realm
777 self.host_princ = 'host/%s@%s' % (hostname, self.realm)
778 self.nfs_princ = 'nfs/%s@%s' % (hostname, self.realm)
779 self.krbtgt_princ = 'krbtgt/%s@%s' % (self.realm, self.realm)
780 self.keytab = os.path.join(self.testdir, 'keytab')
781 self.client_keytab = os.path.join(self.testdir, 'client_keytab')
782 self.ccache = os.path.join(self.testdir, 'ccache')
783 self.kadmin_ccache = os.path.join(self.testdir, 'kadmin_ccache')
784 self._krb5_conf = _cfg_merge(_default_krb5_conf, krb5_conf)
785 self._kdc_conf = _cfg_merge(_default_kdc_conf, kdc_conf)
786 self._kdc_proc = None
787 self._kadmind_proc = None
788 self._kpropd_procs = []
789 krb5_conf_path = os.path.join(self.testdir, 'krb5.conf')
790 kdc_conf_path = os.path.join(self.testdir, 'kdc.conf')
791 self.env = self._make_env(krb5_conf_path, kdc_conf_path)
793 self._create_empty_dir()
794 self._create_conf(self._krb5_conf, krb5_conf_path)
795 self._create_conf(self._kdc_conf, kdc_conf_path)
797 self._create_dictfile()
801 if krbtgt_keysalt and create_kdb:
802 self.run([kadminl, 'cpw', '-randkey', '-e', krbtgt_keysalt,
804 if create_user and create_kdb:
805 self.addprinc(self.user_princ, password('user'))
806 self.addprinc(self.admin_princ, password('admin'))
807 if create_host and create_kdb:
808 self.addprinc(self.host_princ)
809 self.extract_keytab(self.host_princ, self.keytab)
810 if start_kdc and create_kdb:
812 if start_kadmind and create_kdb:
814 if get_creds and create_kdb and create_user and start_kdc:
815 self.kinit(self.user_princ, password('user'))
816 self.klist(self.user_princ)
818 def _create_empty_dir(self):
820 shutil.rmtree(dir, True)
821 if (os.path.exists(dir)):
822 fail('Cannot remove %s to create test realm.' % dir)
825 def _create_conf(self, profile, filename):
826 file = open(filename, 'w')
827 for section, contents in profile.items():
828 file.write('[%s]\n' % section)
829 self._write_cfg_section(file, contents, 1)
832 def _write_cfg_section(self, file, contents, indent_level):
833 indent = '\t' * indent_level
834 for name, value in contents.items():
835 name = self._subst_cfg_value(name)
836 if isinstance(value, dict):
837 # A dictionary value yields a list subsection.
838 file.write('%s%s = {\n' % (indent, name))
839 self._write_cfg_section(file, value, indent_level + 1)
840 file.write('%s}\n' % indent)
841 elif isinstance(value, list):
842 # A list value yields multiple values for the same name.
844 item = self._subst_cfg_value(item)
845 file.write('%s%s = %s\n' % (indent, name, item))
846 elif isinstance(value, str):
847 # A string value yields a straightforward variable setting.
848 value = self._subst_cfg_value(value)
849 file.write('%s%s = %s\n' % (indent, name, value))
850 elif value is not None:
853 def _subst_cfg_value(self, value):
854 global buildtop, srctop, hostname
855 template = string.Template(value)
856 return template.substitute(realm=self.realm,
857 testdir=self.testdir,
863 port1=self.portbase + 1,
864 port2=self.portbase + 2,
865 port3=self.portbase + 3,
866 port4=self.portbase + 4,
867 port5=self.portbase + 5,
868 port6=self.portbase + 6,
869 port7=self.portbase + 7,
870 port8=self.portbase + 8,
871 port9=self.portbase + 9)
873 def _create_acl(self):
875 filename = os.path.join(self.testdir, 'acl')
876 file = open(filename, 'w')
877 file.write('%s *e\n' % self.admin_princ)
878 file.write('kiprop/%s@%s p\n' % (hostname, self.realm))
881 def _create_dictfile(self):
882 filename = os.path.join(self.testdir, 'dictfile')
883 file = open(filename, 'w')
884 file.write('weak_password\n')
887 def _make_env(self, krb5_conf_path, kdc_conf_path):
889 env['KRB5_CONFIG'] = krb5_conf_path
890 env['KRB5_KDC_PROFILE'] = kdc_conf_path or os.devnull
891 env['KRB5CCNAME'] = self.ccache
892 env['KRB5_KTNAME'] = self.keytab
893 env['KRB5_CLIENT_KTNAME'] = self.client_keytab
894 env['KRB5RCACHEDIR'] = self.testdir
895 env['KPROPD_PORT'] = str(self.kprop_port())
896 env['KPROP_PORT'] = str(self.kprop_port())
899 def run(self, args, env=None, **keywords):
902 return _run_cmd(args, env, **keywords)
904 def kprop_port(self):
905 return self.portbase + 3
907 def server_port(self):
908 return self.portbase + 5
910 def start_server(self, args, sentinel, env=None):
913 return _start_daemon(args, env, sentinel)
915 def start_in_inetd(self, args, port=None, env=None):
917 port = self.server_port()
920 inetd_args = [t_inetd, str(port)] + args
921 return _start_daemon(inetd_args, env, 'Ready!')
923 def create_kdb(self):
925 self.run([kdb5_util, 'create', '-W', '-s', '-P', 'master'])
927 def start_kdc(self, args=[], env=None):
931 assert(self._kdc_proc is None)
932 self._kdc_proc = _start_daemon([krb5kdc, '-n'] + args, env,
936 assert(self._kdc_proc is not None)
937 stop_daemon(self._kdc_proc)
938 self._kdc_proc = None
940 def start_kadmind(self, env=None):
944 assert(self._kadmind_proc is None)
945 dump_path = os.path.join(self.testdir, 'dump')
946 self._kadmind_proc = _start_daemon([kadmind, '-nofork', '-W',
947 '-p', kdb5_util, '-K', kprop,
948 '-F', dump_path], env,
951 def stop_kadmind(self):
952 assert(self._kadmind_proc is not None)
953 stop_daemon(self._kadmind_proc)
954 self._kadmind_proc = None
956 def _kpropd_args(self):
957 slavedump_path = os.path.join(self.testdir, 'incoming-slave-datatrans')
958 kpropdacl_path = os.path.join(self.testdir, 'kpropd-acl')
959 return [kpropd, '-D', '-P', str(self.kprop_port()),
960 '-f', slavedump_path, '-p', kdb5_util, '-a', kpropdacl_path]
962 def start_kpropd(self, env, args=[]):
963 proc = _start_daemon(self._kpropd_args() + args, env, 'ready')
964 self._kpropd_procs.append(proc)
967 def run_kpropd_once(self, env, args=[]):
968 return self.run(self._kpropd_args() + ['-t'] + args, env=env)
973 if self._kadmind_proc:
975 for p in self._kpropd_procs:
977 self._kpropd_procs = []
979 def addprinc(self, princname, password=None):
981 self.run([kadminl, 'addprinc', '-pw', password, princname])
983 self.run([kadminl, 'addprinc', '-randkey', princname])
985 def extract_keytab(self, princname, keytab):
986 self.run([kadminl, 'ktadd', '-k', keytab, '-norandkey', princname])
988 def kinit(self, princname, password=None, flags=[], **keywords):
990 input = password + "\n"
993 return self.run([kinit] + flags + [princname], input=input, **keywords)
995 def klist(self, client_princ, service_princ=None, ccache=None, **keywords):
996 if service_princ is None:
997 service_princ = self.krbtgt_princ
1001 if len(ccachestr) < 2 or ':' not in ccachestr[2:]:
1002 ccachestr = 'FILE:' + ccachestr
1003 output = self.run([klist, ccache], **keywords)
1004 if (('Ticket cache: %s\n' % ccachestr) not in output or
1005 ('Default principal: %s\n' % client_princ) not in output or
1006 service_princ not in output):
1007 fail('Unexpected klist output.')
1009 def klist_keytab(self, princ, keytab=None, **keywords):
1011 keytab = self.keytab
1012 output = self.run([klist, '-k', keytab], **keywords)
1013 if (('Keytab name: FILE:%s\n' % keytab) not in output or
1014 'KVNO Principal\n----' not in output or
1015 princ not in output):
1016 fail('Unexpected klist output.')
1018 def prep_kadmin(self, princname=None, pw=None, flags=[]):
1019 if princname is None:
1020 princname = self.admin_princ
1021 pw = password('admin')
1022 return self.kinit(princname, pw,
1023 flags=['-S', 'kadmin/admin',
1024 '-c', self.kadmin_ccache] + flags)
1026 def run_kadmin(self, args, **keywords):
1027 return self.run([kadmin, '-c', self.kadmin_ccache] + args, **keywords)
1029 def special_env(self, name, has_kdc_conf, krb5_conf=None, kdc_conf=None):
1030 krb5_conf_path = os.path.join(self.testdir, 'krb5.conf.%s' % name)
1031 krb5_conf = _cfg_merge(self._krb5_conf, krb5_conf)
1032 self._create_conf(krb5_conf, krb5_conf_path)
1034 kdc_conf_path = os.path.join(self.testdir, 'kdc.conf.%s' % name)
1035 kdc_conf = _cfg_merge(self._kdc_conf, kdc_conf)
1036 self._create_conf(kdc_conf, kdc_conf_path)
1038 kdc_conf_path = None
1039 return self._make_env(krb5_conf_path, kdc_conf_path)
1042 def multipass_realms(**keywords):
1043 global _current_pass, _passes, testpass
1044 caller_krb5_conf = keywords.get('krb5_conf')
1045 caller_kdc_conf = keywords.get('kdc_conf')
1047 (name, krbtgt_keysalt, krb5_conf, kdc_conf) = p
1048 if testpass and name != testpass:
1050 output('*** Beginning pass %s\n' % name)
1051 keywords['krb5_conf'] = _cfg_merge(krb5_conf, caller_krb5_conf)
1052 keywords['kdc_conf'] = _cfg_merge(kdc_conf, caller_kdc_conf)
1053 keywords['krbtgt_keysalt'] = krbtgt_keysalt
1054 _current_pass = name
1055 realm = K5Realm(**keywords)
1058 _current_pass = None
1061 def cross_realms(num, xtgts=None, args=None, **keywords):
1062 # Build keyword args for each realm.
1064 for i in range(num):
1066 # Start with any global keyword arguments to this function.
1068 if args and args[i]:
1069 # Merge in specific arguments for this realm. Use
1070 # _cfg_merge for config fragments.
1072 for cf in ('krb5_conf', 'kdc_conf'):
1073 if cf in keywords and cf in args[i]:
1074 a[cf] = _cfg_merge(keywords[cf], args[i][cf])
1075 # Set defaults for the realm name, testdir, and portbase.
1076 if not 'realm' in a:
1077 a['realm'] = 'KRBTEST%d.COM' % realmnumber
1078 if not 'testdir' in a:
1079 a['testdir'] = os.path.join('testdir', str(realmnumber))
1080 if not 'portbase' in a:
1081 a['portbase'] = 61000 + 10 * realmnumber
1082 realm_args.append(a)
1084 # Build a [realms] config fragment containing all of the realms.
1085 realmsection = { '$realm' : None }
1086 for a in realm_args:
1088 portbase = a['portbase']
1089 realmsection[name] = {
1090 'kdc' : '$hostname:%d' % portbase,
1091 'admin_server' : '$hostname:%d' % (portbase + 1),
1092 'kpasswd_server' : '$hostname:%d' % (portbase + 2)
1094 realmscfg = {'realms': realmsection}
1096 # Set realmsection in each realm's krb5_conf keyword argument.
1097 for a in realm_args:
1098 a['krb5_conf'] = _cfg_merge(realmscfg, a.get('krb5_conf'))
1101 # Default to cross tgts for every pair of realms.
1102 # (itertools.permutations would work here but is new in 2.6.)
1103 xtgts = [(x,y) for x in range(num) for y in range(num) if x != y]
1105 # Create the realms.
1107 for i in range(num):
1108 r = K5Realm(**realm_args[i])
1109 # Create specified cross TGTs in this realm's db.
1110 for j in range(num):
1114 jname = realm_args[j]['realm']
1116 # This realm can authenticate to realm j.
1117 r.addprinc('krbtgt/%s' % jname, password('cr-%d-%d-' % (i, j)))
1119 # Realm j can authenticate to this realm.
1120 r.addprinc('krbtgt/%s@%s' % (iname, jname),
1121 password('cr-%d-%d-' % (j, i)))
1126 _default_krb5_conf = {
1128 'default_realm': '$realm',
1129 'dns_lookup_kdc': 'false',
1130 'plugin_base_dir': '$plugins'},
1131 'realms': {'$realm': {
1132 'kdc': '$hostname:$port0',
1133 'admin_server': '$hostname:$port1',
1134 'kpasswd_server': '$hostname:$port2'}}}
1137 _default_kdc_conf = {
1138 'realms': {'$realm': {
1139 'database_module': 'db',
1140 'iprop_port': '$port4',
1141 'key_stash_file': '$testdir/stash',
1142 'acl_file': '$testdir/acl',
1143 'dictfile': '$testdir/dictfile',
1144 'kadmind_port': '$port1',
1145 'kpasswd_port': '$port2',
1146 'kdc_listen': '$port0',
1147 'kdc_tcp_listen': '$port0'}},
1149 'db_module_dir': '$plugins/kdb',
1150 'db': {'db_library': 'db2', 'database_name' : '$testdir/db'}},
1152 'admin_server': 'FILE:$testdir/kadmind5.log',
1153 'kdc': 'FILE:$testdir/kdc.log',
1154 'default': 'FILE:$testdir/others.log'}}
1157 # A pass is a tuple of: name, krbtgt_keysalt, krb5_conf, kdc_conf.
1159 # No special settings; exercises AES256.
1160 ('default', None, None, None),
1162 # Exercise a DES enctype and the v4 salt type.
1165 'default_tgs_enctypes': 'des-cbc-crc',
1166 'default_tkt_enctypes': 'des-cbc-crc',
1167 'permitted_enctypes': 'des-cbc-crc',
1168 'allow_weak_crypto': 'true'}},
1169 {'realms': {'$realm': {
1170 'supported_enctypes': 'des-cbc-crc:v4',
1171 'master_key_type': 'des-cbc-crc'}}}),
1173 # Exercise the DES3 enctype.
1176 'default_tgs_enctypes': 'des3',
1177 'default_tkt_enctypes': 'des3',
1178 'permitted_enctypes': 'des3'}},
1179 {'realms': {'$realm': {
1180 'supported_enctypes': 'des3-cbc-sha1:normal',
1181 'master_key_type': 'des3-cbc-sha1'}}}),
1183 # Exercise the arcfour enctype.
1186 'default_tgs_enctypes': 'rc4',
1187 'default_tkt_enctypes': 'rc4',
1188 'permitted_enctypes': 'rc4'}},
1189 {'realms': {'$realm': {
1190 'supported_enctypes': 'arcfour-hmac:normal',
1191 'master_key_type': 'arcfour-hmac'}}}),
1193 # Exercise the AES128 enctype.
1196 'default_tgs_enctypes': 'aes128-cts',
1197 'default_tkt_enctypes': 'aes128-cts',
1198 'permitted_enctypes': 'aes128-cts'}},
1199 {'realms': {'$realm': {
1200 'supported_enctypes': 'aes128-cts:normal',
1201 'master_key_type': 'aes128-cts'}}}),
1203 # Exercise the camellia256-cts enctype.
1204 ('camellia256', None,
1206 'default_tgs_enctypes': 'camellia256-cts',
1207 'default_tkt_enctypes': 'camellia256-cts',
1208 'permitted_enctypes': 'camellia256-cts'}},
1209 {'realms': {'$realm': {
1210 'supported_enctypes': 'camellia256-cts:normal',
1211 'master_key_type': 'camellia256-cts'}}}),
1213 # Exercise the aes128-sha2 enctype.
1214 ('aes128-sha2', None,
1216 'default_tgs_enctypes': 'aes128-sha2',
1217 'default_tkt_enctypes': 'aes128-sha2',
1218 'permitted_enctypes': 'aes128-sha2'}},
1219 {'realms': {'$realm': {
1220 'supported_enctypes': 'aes128-sha2:normal',
1221 'master_key_type': 'aes128-sha2'}}}),
1223 # Exercise the aes256-sha2 enctype.
1224 ('aes256-sha2', None,
1226 'default_tgs_enctypes': 'aes256-sha2',
1227 'default_tkt_enctypes': 'aes256-sha2',
1228 'permitted_enctypes': 'aes256-sha2'}},
1229 {'realms': {'$realm': {
1230 'supported_enctypes': 'aes256-sha2:normal',
1231 'master_key_type': 'aes256-sha2'}}}),
1233 # Test a setup with modern principal keys but an old TGT key.
1234 ('aes256.destgt', 'des-cbc-crc:normal',
1235 {'libdefaults': {'allow_weak_crypto': 'true'}},
1240 _current_pass = None
1243 atexit.register(_onexit)
1244 signal.signal(signal.SIGINT, _onsigint)
1245 _outfile = open('testlog', 'w')
1248 _last_cmd_output = None
1249 buildtop = _find_buildtop()
1250 srctop = _find_srctop()
1251 plugins = os.path.join(buildtop, 'plugins')
1252 runenv = _import_runenv()
1253 hostname = _get_hostname()
1254 null_input = open(os.devnull, 'r')
1256 krb5kdc = os.path.join(buildtop, 'kdc', 'krb5kdc')
1257 kadmind = os.path.join(buildtop, 'kadmin', 'server', 'kadmind')
1258 kadmin = os.path.join(buildtop, 'kadmin', 'cli', 'kadmin')
1259 kadminl = os.path.join(buildtop, 'kadmin', 'cli', 'kadmin.local')
1260 kdb5_ldap_util = os.path.join(buildtop, 'plugins', 'kdb', 'ldap', 'ldap_util',
1262 kdb5_util = os.path.join(buildtop, 'kadmin', 'dbutil', 'kdb5_util')
1263 ktutil = os.path.join(buildtop, 'kadmin', 'ktutil', 'ktutil')
1264 kinit = os.path.join(buildtop, 'clients', 'kinit', 'kinit')
1265 klist = os.path.join(buildtop, 'clients', 'klist', 'klist')
1266 kswitch = os.path.join(buildtop, 'clients', 'kswitch', 'kswitch')
1267 kvno = os.path.join(buildtop, 'clients', 'kvno', 'kvno')
1268 kdestroy = os.path.join(buildtop, 'clients', 'kdestroy', 'kdestroy')
1269 kpasswd = os.path.join(buildtop, 'clients', 'kpasswd', 'kpasswd')
1270 t_inetd = os.path.join(buildtop, 'tests', 'dejagnu', 't_inetd')
1271 kproplog = os.path.join(buildtop, 'slave', 'kproplog')
1272 kpropd = os.path.join(buildtop, 'slave', 'kpropd')
1273 kprop = os.path.join(buildtop, 'slave', 'kprop')