diff options
-rw-r--r-- | .github/workflows/build.yml | 5 | ||||
-rw-r--r-- | src/clients/ksu/Makefile.in | 8 | ||||
-rw-r--r-- | src/clients/ksu/t_ksu.py | 271 |
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') |