krb5 commit: Add ksu test script

ghudson at mit.edu ghudson at mit.edu
Mon Oct 2 20:03:22 EDT 2023


https://github.com/krb5/krb5/commit/036f861b657f63852732a4c19d84a4f18a3938ec
commit 036f861b657f63852732a4c19d84a4f18a3938ec
Author: Greg Hudson <ghudson at mit.edu>
Date:   Tue Aug 29 16:40:58 2023 -0400

    Add ksu test script
    
    Add a test script for ksu, which must be run under sudo and makes
    potentially disruptive temporary changes to the host environment.  Do
    not run it as part of "make check", but do run it as part of the
    Github Actions CI.

 .github/workflows/build.yml |   5 +
 src/clients/ksu/Makefile.in |   8 ++
 src/clients/ksu/t_ksu.py    | 271 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 284 insertions(+)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 68a4788ad..e62f3fe34 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -41,12 +41,17 @@ jobs:
                 MAKEVARS: ${{ matrix.makevars }}
                 CONFIGURE_OPTS:  ${{ matrix.configureopts }}
               run: |
+                # For the ksu tests, allow homedir access from other users.
+                umask 022
+                chmod a+rx $HOME
+                chmod -R a+rX src
                 cd src
                 autoreconf
                 ./configure --enable-maintainer-mode --with-ldap $CONFIGURE_OPTS --prefix=$HOME/inst
                 make $MAKEVARS
                 make check
                 make install
+                (cd clients/ksu && make check-ksu)
             - name: Display skipped tests
               run: cat src/skiptests
             - name: Check for files unexpectedly not removed by make distclean
diff --git a/src/clients/ksu/Makefile.in b/src/clients/ksu/Makefile.in
index 8b4edce4d..9a892e665 100644
--- a/src/clients/ksu/Makefile.in
+++ b/src/clients/ksu/Makefile.in
@@ -33,3 +33,11 @@ install:
 	  $(INSTALL_SETUID) $$f \
 		$(DESTDIR)$(CLIENT_BINDIR)/`echo $$f|sed '$(transform)'`; \
 	done
+
+# The ksu tests must be run as root and may be disruptive to the host
+# system, so they are not included in "make check".  asan's leak
+# checker does not work with setuid binaries (and causes them to
+# always exit with status 1), so it is disabled here.
+check-ksu:
+	sudo LSAN_OPTIONS=detect_leaks=0 $(RUNPYTEST) $(srcdir)/t_ksu.py \
+		$(PYTESTFLAGS)
diff --git a/src/clients/ksu/t_ksu.py b/src/clients/ksu/t_ksu.py
new file mode 100644
index 000000000..9740972ba
--- /dev/null
+++ b/src/clients/ksu/t_ksu.py
@@ -0,0 +1,271 @@
+from k5test import *
+import pwd
+import stat
+
+krb5_conf = '/etc/krb5.conf'
+krb5_conf_save = krb5_conf + '.save-ksutest'
+krb5_conf_nosave = krb5_conf + '.nosave-ksutest'
+ksu = './ksu.ksutest'
+if 'SUDO_UID' not in os.environ or os.geteuid() != 0:
+    fail('this script must be run as root via sudo')
+caller_uid = int(os.environ['SUDO_UID'])
+if caller_uid == 0:
+    fail('the user invoking sudo must not be root')
+caller_username = os.environ['SUDO_USER']
+os.chown('testlog', caller_uid, -1)
+
+# Set the real and effective UIDs to the calling user, but preserve
+# the ability to restore root privileges.
+def be_caller():
+    os.setresuid(caller_uid, caller_uid, 0)
+
+
+# Restore root privileges.
+def be_root():
+    os.setresuid(0, 0, 0)
+
+
+# Remove the ksutest account.
+def cleanup_user():
+    # userdel commonly gives a warning about being unable to delete
+    # the mail spool; filter it out.
+    out = subprocess.check_output(['userdel', '-r', 'ksutest'],
+                                  stderr=subprocess.STDOUT)
+    if out.count(b'\n') > 1 or b'ksutest mail spool' not in out:
+        print(out)
+
+
+# Restore /etc/krb5.conf to the state it was in previously.
+def cleanup_krb5_conf():
+    if os.path.exists(krb5_conf_save):
+        os.unlink(krb5_conf)
+        os.rename(krb5_conf_save, krb5_conf)
+    elif os.path.exists(krb5_conf_nosave):
+        os.unlink(krb5_conf)
+        os.unlink(krb5_conf_nosave)
+
+
+def onexit():
+    if len(sys.argv) >= 2 and sys.argv[1] == 'nocleanup':
+        return
+    be_root()
+    cleanup_user()
+    cleanup_krb5_conf()
+    if os.path.exists(ksu):
+        os.unlink(ksu)
+
+
+# Create a ksutest account and return its home directory.
+def setup_user():
+    try:
+        ent = pwd.getpwnam('ksutest')
+        return ent.pw_dir
+    except KeyError:
+        subprocess.check_call(['useradd', '-m', '-r', 'ksutest'])
+        return pwd.getpwnam('ksutest').pw_dir
+
+
+# Make krb5.conf a copy of realm's krb5.conf file.  Save the old
+# contents in krb5_conf_save, or create krb5_conf_noexist to indicate
+# that the file didn't previously exist.
+def setup_krb5_conf(realm):
+    if not os.path.exists(krb5_conf):
+        open(krb5_conf_nosave, 'w').close()
+    elif not os.path.exists(krb5_conf_save):
+        os.rename(krb5_conf, krb5_conf_save)
+    shutil.copyfile(os.path.join(realm.testdir, 'krb5.conf'), krb5_conf)
+
+
+# Temporarily acting as root, write a file named fname in ksutest's
+# home directory with the given contents.  If wrong_owner is set, make
+# the file owned by the caller uid in order to trip ksu's owner check.
+def write_authz_file(fname, contents, wrong_owner=False):
+    be_root()
+    path = os.path.join(ksutest_home, fname)
+    with open(path, 'w') as f:
+        f.write('\n'.join(contents) + '\n')
+    if wrong_owner:
+        os.chown(path, caller_uid, -1)
+    be_caller()
+
+
+# Temporarily acting as root, remove fname from ksutest's home
+# directory.
+def remove_authz_file(fname):
+    be_root()
+    path = os.path.join(ksutest_home, fname)
+    if os.path.exists(path):
+        os.remove(path)
+    be_caller()
+
+
+be_caller()
+
+# Set up a realm.  Set default_keytab_name since ksu won't respect the
+# KRB5_KTNAME environment variable.
+keytab = os.path.join(os.getcwd(), 'testdir', 'keytab')
+realm = K5Realm(create_user=False,
+                krb5_conf={'libdefaults': {'default_keytab_name': keytab}})
+realm.addprinc('alice', 'pwalice')
+realm.addprinc('ksutest', 'pwksutest')
+realm.addprinc('ksutest/root', 'pwroot')
+realm.addprinc(caller_username, 'pwcaller')
+
+# Root setup:
+# - /etc/krb5.conf is a copy of the test realm krb5.conf
+# - a newly created user named ksutest exists (with homedir ksutest_home)
+# - a setuid copy of ksu exists in the build dir
+# Register an atexit handler to undo these changes.
+atexit.register(onexit)
+be_root()
+ksutest_home = setup_user()
+setup_krb5_conf(realm)
+if os.path.exists(ksu):
+    os.unlink(ksu)
+shutil.copyfile('ksu', ksu)
+os.chmod(ksu, 0o4755)
+be_caller()
+
+mark('no authorization')
+realm.kinit('alice', 'pwalice')
+realm.run([ksu, 'ksutest', '-n', 'alice', '-a', '-c', klist], expected_code=1,
+          expected_msg='authorization of alice at KRBTEST.COM failed')
+
+mark('an2ln authorization')
+realm.kinit('ksutest', 'pwksutest')
+realm.run([ksu, 'ksutest', '-a', '-c', klist],
+          expected_msg='authorization for ksutest at KRBTEST.COM successful')
+realm.run([ksu, 'ksutest', '-e', klist], expected_code=1,
+          expected_msg='account ksutest: authorization failed')
+
+mark('.k5login wrong owner')
+write_authz_file('.k5login', ['ksutest at KRBTEST.COM'], wrong_owner=True)
+realm.run([ksu, 'ksutest', '-e', klist], expected_code=1,
+          expected_msg='account ksutest: authorization failed')
+remove_authz_file('.k5login')
+
+mark('.k5users wrong owner')
+write_authz_file('.k5users', ['ksutest at KRBTEST.COM'], wrong_owner=True)
+realm.run([ksu, 'ksutest', '-e', klist], expected_code=1,
+          expected_msg='account ksutest: authorization failed')
+remove_authz_file('.k5users')
+
+mark('.k5login authorization')
+realm.kinit('alice', 'pwalice')
+write_authz_file('.k5login', ['alice at KRBTEST.COM'])
+realm.run([ksu, 'ksutest', '-a', '-c', klist],
+          expected_msg='authorization for alice at KRBTEST.COM successful')
+realm.run([ksu, 'ksutest', '-e', klist],
+          expected_msg='authorization for alice at KRBTEST.COM for execution of')
+write_authz_file('.k5login', ['bob at KRBTEST.COM'])
+realm.run([ksu, 'ksutest', '-a', '-c', klist], expected_code=1,
+          expected_msg='account ksutest: authorization failed')
+remove_authz_file('.k5login')
+
+mark('.k5users authorization (no second field)')
+write_authz_file('.k5users', ['alice at KRBTEST.COM'])
+realm.run([ksu, 'ksutest', '-a', '-c', klist],
+          expected_msg='authorization for alice at KRBTEST.COM successful')
+realm.run([ksu, 'ksutest', '-e', klist], expected_code=1,
+          expected_msg='account ksutest: authorization failed')
+write_authz_file('.k5users', ['bob at KRBTEST.COM'])
+realm.run([ksu, 'ksutest', '-a', '-c', klist], expected_code=1,
+          expected_msg='account ksutest: authorization failed')
+
+mark('k5users authorization (wildcard)')
+write_authz_file('.k5users', ['alice at KRBTEST.COM *'])
+realm.run([ksu, 'ksutest', '-a', '-c', klist],
+          expected_msg='authorization for alice at KRBTEST.COM successful')
+realm.run([ksu, 'ksutest', '-e', klist],
+          expected_msg='authorization for alice at KRBTEST.COM for execution of')
+
+mark('k5users authorization (command list)')
+write_authz_file('.k5users', ['alice at KRBTEST.COM doesnotexist ' + klist])
+realm.run([ksu, 'ksutest', '-a', '-c', klist], expected_code=1,
+          expected_msg='account ksutest: authorization failed')
+realm.run([ksu, 'ksutest', '-e', klist],
+          expected_msg='authorization for alice at KRBTEST.COM for execution of')
+realm.run([ksu, 'ksutest', '-e', kvno], expected_code=1,
+          expected_msg='account ksutest: authorization failed')
+realm.run([ksu, 'ksutest', '-e', 'doesnotexist'], expected_code=1,
+          expected_msg='Error: not found ->')
+remove_authz_file('.k5users')
+
+mark('principal heuristic (no authz files)')
+realm.run([ksu, 'ksutest', '-a', '-c', klist], input='pwksutest\n',
+          expected_msg='Authenticated ksutest at KRBTEST.COM')
+realm.run([ksu, 'ksutest', '-e', klist], expected_code=1,
+          expected_msg='account ksutest: authorization failed')
+
+mark('principal heuristic (empty authz files)')
+write_authz_file('.k5login', [])
+write_authz_file('.k5users', [])
+realm.run([ksu, 'ksutest', '-e', klist], expected_code=1,
+          expected_msg='account ksutest: authorization failed')
+remove_authz_file('.k5login')
+remove_authz_file('.k5users')
+
+# Untested: if the ccache default principal is not authorized,
+# get_best_princ_for_target() looks for a TGT or host service ticket
+# for the target and source users (if authorized) or any other
+# authorized user.  This is not really useful because a ccache usually
+# only contains tickets for its default client principal (aside from
+# caches created for S4U2Proxy).  If the heuristic is ever changed to
+# search the cache collection instead of only the primary cache, we
+# should add tests for that here.
+
+mark('principal heuristic (.k5login)')
+write_authz_file('.k5login', ['ksutest at KRBTEST.COM'])
+realm.run([ksu, 'ksutest', '-a', '-c', klist], input='pwksutest\n',
+          expected_msg='Authenticated ksutest at KRBTEST.COM')
+realm.run([ksu, 'ksutest', '-e', klist], input='pwksutest\n',
+          expected_msg='Authenticated ksutest at KRBTEST.COM')
+write_authz_file('.k5login', [caller_username + '@KRBTEST.COM'])
+realm.run([ksu, 'ksutest', '-e', klist], input='pwcaller\n',
+          expected_msg='Authenticated %s at KRBTEST.COM' % caller_username)
+remove_authz_file('.k5login')
+
+mark('principal heuristic (.k5users)')
+write_authz_file('.k5users', ['alice at KRBTEST.COM ' + klist,
+                              'ksutest at KRBTEST.COM',
+                              caller_username + '@KRBTEST.COM *'])
+realm.run([ksu, 'ksutest', '-e', klist],
+          expected_msg='Authenticated alice at KRBTEST.COM')
+realm.run([ksu, 'ksutest', '-a', '-c', klist], input='pwksutest\n',
+          expected_msg='Authenticated ksutest at KRBTEST.COM')
+realm.run([ksu, 'ksutest', '-e', kvno, 'alice'], input='pwcaller\n',
+          expected_msg='Authenticated %s at KRBTEST.COM' % caller_username)
+write_authz_file('.k5users', ['alice at KRBTEST.COM ' + klist,
+                              'ksutest/root at KRBTEST.COM ' + kvno])
+realm.run([ksu, 'ksutest', '-e', kvno, 'alice'], input='pwroot\n',
+          expected_msg='Authenticated ksutest/root at KRBTEST.COM')
+
+mark('principal heuristic (no authorization)')
+realm.run([ksu, '.', '-e', klist],
+          expected_msg='Default principal: alice at KRBTEST.COM')
+be_root()
+realm.run([ksu, 'ksutest', '-e', klist],
+          expected_msg='No credentials cache found')
+be_caller()
+realm.kinit('ksutest', 'pwksutest')
+be_root()
+realm.run([ksu, 'ksutest', '-e', klist],
+          expected_msg='Default principal: ksutest at KRBTEST.COM')
+be_caller()
+realm.run([kdestroy])
+realm.run([ksu, '.', '-e', klist], expected_msg='No credentials cache found')
+
+mark('authentication without authorization')
+realm.run([ksu, '.', '-n', 'ksutest', '-e', klist], input='pwksutest\n',
+          expected_msg='Leaving uid as ' + caller_username)
+
+# It's hard to make this flag do anything detectable, but we can
+# exercise the code.
+mark('-z flag')
+realm.kinit(caller_username, 'pwcaller')
+realm.run([ksu, '.', '-z', '-e', klist],
+          expected_msg='Default principal: ' + caller_username)
+
+realm.run([ksu, '.', '-Z', '-e', klist])
+
+success('ksu tests')


More information about the cvs-krb5 mailing list