aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGreg Hudson <ghudson@mit.edu>2023-08-29 16:40:58 -0400
committerGreg Hudson <ghudson@mit.edu>2023-10-02 19:03:13 -0400
commit036f861b657f63852732a4c19d84a4f18a3938ec (patch)
tree3f6801a7d09c935e885b25148284fdd2be510765
parent6ed1f8e27eb624710c4aa152d8dee4cf2e528082 (diff)
downloadkrb5-036f861b657f63852732a4c19d84a4f18a3938ec.zip
krb5-036f861b657f63852732a4c19d84a4f18a3938ec.tar.gz
krb5-036f861b657f63852732a4c19d84a4f18a3938ec.tar.bz2
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.
-rw-r--r--.github/workflows/build.yml5
-rw-r--r--src/clients/ksu/Makefile.in8
-rw-r--r--src/clients/ksu/t_ksu.py271
3 files changed, 284 insertions, 0 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 68a4788..e62f3fe 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 8b4edce..9a892e6 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 0000000..9740972
--- /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@KRBTEST.COM failed')
+
+mark('an2ln authorization')
+realm.kinit('ksutest', 'pwksutest')
+realm.run([ksu, 'ksutest', '-a', '-c', klist],
+ expected_msg='authorization for ksutest@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@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@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@KRBTEST.COM'])
+realm.run([ksu, 'ksutest', '-a', '-c', klist],
+ expected_msg='authorization for alice@KRBTEST.COM successful')
+realm.run([ksu, 'ksutest', '-e', klist],
+ expected_msg='authorization for alice@KRBTEST.COM for execution of')
+write_authz_file('.k5login', ['bob@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@KRBTEST.COM'])
+realm.run([ksu, 'ksutest', '-a', '-c', klist],
+ expected_msg='authorization for alice@KRBTEST.COM successful')
+realm.run([ksu, 'ksutest', '-e', klist], expected_code=1,
+ expected_msg='account ksutest: authorization failed')
+write_authz_file('.k5users', ['bob@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@KRBTEST.COM *'])
+realm.run([ksu, 'ksutest', '-a', '-c', klist],
+ expected_msg='authorization for alice@KRBTEST.COM successful')
+realm.run([ksu, 'ksutest', '-e', klist],
+ expected_msg='authorization for alice@KRBTEST.COM for execution of')
+
+mark('k5users authorization (command list)')
+write_authz_file('.k5users', ['alice@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@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@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@KRBTEST.COM'])
+realm.run([ksu, 'ksutest', '-a', '-c', klist], input='pwksutest\n',
+ expected_msg='Authenticated ksutest@KRBTEST.COM')
+realm.run([ksu, 'ksutest', '-e', klist], input='pwksutest\n',
+ expected_msg='Authenticated ksutest@KRBTEST.COM')
+write_authz_file('.k5login', [caller_username + '@KRBTEST.COM'])
+realm.run([ksu, 'ksutest', '-e', klist], input='pwcaller\n',
+ expected_msg='Authenticated %s@KRBTEST.COM' % caller_username)
+remove_authz_file('.k5login')
+
+mark('principal heuristic (.k5users)')
+write_authz_file('.k5users', ['alice@KRBTEST.COM ' + klist,
+ 'ksutest@KRBTEST.COM',
+ caller_username + '@KRBTEST.COM *'])
+realm.run([ksu, 'ksutest', '-e', klist],
+ expected_msg='Authenticated alice@KRBTEST.COM')
+realm.run([ksu, 'ksutest', '-a', '-c', klist], input='pwksutest\n',
+ expected_msg='Authenticated ksutest@KRBTEST.COM')
+realm.run([ksu, 'ksutest', '-e', kvno, 'alice'], input='pwcaller\n',
+ expected_msg='Authenticated %s@KRBTEST.COM' % caller_username)
+write_authz_file('.k5users', ['alice@KRBTEST.COM ' + klist,
+ 'ksutest/root@KRBTEST.COM ' + kvno])
+realm.run([ksu, 'ksutest', '-e', kvno, 'alice'], input='pwroot\n',
+ expected_msg='Authenticated ksutest/root@KRBTEST.COM')
+
+mark('principal heuristic (no authorization)')
+realm.run([ksu, '.', '-e', klist],
+ expected_msg='Default principal: alice@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@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')