krb5 commit: Add automated tests for LDAP KDB module

Greg Hudson ghudson at MIT.EDU
Thu Nov 15 13:59:05 EST 2012


https://github.com/krb5/krb5/commit/4f349b6e154eb05e591cd0de10791e29f4c44fb9
commit 4f349b6e154eb05e591cd0de10791e29f4c44fb9
Author: Greg Hudson <ghudson at mit.edu>
Date:   Sat Nov 10 22:08:39 2012 -0500

    Add automated tests for LDAP KDB module
    
    Add new tests kdbtest.c and t_kdb.py.  Together these exercise most of
    the code in the LDAP back end.  kdbtest is also run against the DB2
    module, which is mostly redundant with other tests, but does exercise
    the lockout logic a little more thoroughly than t_lockout.py can.
    
    To test the LDAP back end, we look for slapd and ldapadd binaries in
    the path.  The system slapd is sometimes constrained by AppArmor or
    the like, which we can typically work around by making a copy of the
    binary.  slapd detaches before listening on its server socket (this
    got better in 2.4.27 but still isn't perfect), so we unfortunately
    have to use a one-second sleep in the slapd setup.

 .gitignore            |    1 +
 src/tests/Makefile.in |    9 +-
 src/tests/kdbtest.c   |  406 +++++++++++++++++++++++++++++++++++++++++++++++++
 src/tests/t_kdb.py    |  280 ++++++++++++++++++++++++++++++++++
 src/util/k5test.py    |    3 +
 5 files changed, 697 insertions(+), 2 deletions(-)

diff --git a/.gitignore b/.gitignore
index 8ec785d..f13d50c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -289,6 +289,7 @@ testlog
 /src/tests/misc/test_cxx_rpc
 /src/tests/misc/test_getpw
 
+/src/tests/ldap
 /src/tests/mkeystash_compat/bigendian
 /src/tests/mkeystash_compat/kdc.conf
 /src/tests/mkeystash_compat/krb5.conf
diff --git a/src/tests/Makefile.in b/src/tests/Makefile.in
index 5bb2b04..8886959 100644
--- a/src/tests/Makefile.in
+++ b/src/tests/Makefile.in
@@ -21,6 +21,10 @@ KTEST_OPTS= $(KADMIN_OPTS) -p $(TEST_PREFIX) -n $(TEST_NUM) -D $(TEST_DEPTH)
 hist: hist.o $(KDB5_DEPLIBS) $(KADMSRV_DEPLIBS) $(KRB5_BASE_DEPLIBS)
 	$(CC_LINK) -o $@ hist.o $(KDB5_LIBS) $(KADMSRV_LIBS) $(KRB5_BASE_LIBS)
 
+kdbtest: kdbtest.o $(KDB5_DEPLIBS) $(KADMSRV_DEPLIBS) $(KRB5_BASE_DEPLIBS)
+	$(CC_LINK) -o $@ kdbtest.o $(KDB5_LIBS) $(KADMSRV_LIBS) \
+		$(KRB5_BASE_LIBS)
+
 check-unix:: kdb_check
 
 kdc.conf: Makefile
@@ -63,7 +67,7 @@ kdb_check: kdc.conf krb5.conf
 	$(RUN_SETUP) $(VALGRIND) ../kadmin/dbutil/kdb5_util $(KADMIN_OPTS) destroy -f
 	$(RM) $(TEST_DB)* stash_file
 
-check-pytests:: hist
+check-pytests:: hist kdbtest
 	$(RUNPYTEST) $(srcdir)/t_general.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_iprop.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_anonpkinit.py $(PYTESTFLAGS)
@@ -82,10 +86,11 @@ check-pytests:: hist
 	$(RUNPYTEST) $(srcdir)/t_keytab.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_pwhist.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_kadmin_acl.py $(PYTESTFLAGS)
+	$(RUNPYTEST) $(srcdir)/t_kdb.py $(PYTESTFLAGS)
 #	$(RUNPYTEST) $(srcdir)/kdc_realm/kdcref.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_cve-2012-1014.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_cve-2012-1015.py $(PYTESTFLAGS)
 
 clean::
 	$(RM) krb5.conf kdc.conf
-	$(RM) -rf kdc_realm/sandbox
+	$(RM) -rf kdc_realm/sandbox ldap
diff --git a/src/tests/kdbtest.c b/src/tests/kdbtest.c
new file mode 100644
index 0000000..11c433d
--- /dev/null
+++ b/src/tests/kdbtest.c
@@ -0,0 +1,406 @@
+/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/* tests/kdbtest.c - test program to exercise KDB modules */
+/*
+ * Copyright (C) 2012 by the Massachusetts Institute of Technology.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in
+ *   the documentation and/or other materials provided with the
+ *   distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * This test program uses libkdb5 APIs to exercise as much of the LDAP and DB2
+ * back ends.
+ */
+
+#include <krb5.h>
+#include <kadm5/admin.h>
+#include <string.h>
+
+static krb5_context ctx;
+
+#define CHECK(code) check(code, __LINE__)
+#define CHECK_COND(val) check_cond(val, __LINE__)
+
+static void
+check(krb5_error_code code, int lineno)
+{
+    const char *errmsg;
+
+    if (code) {
+        errmsg = krb5_get_error_message(ctx, code);
+        fprintf(stderr, "Unexpected error at line %d: %s\n", lineno, errmsg);
+        krb5_free_error_message(ctx, errmsg);
+        exit(1);
+    }
+}
+
+static void
+check_cond(int value, int lineno)
+{
+    if (!value) {
+        fprintf(stderr, "Unexpected result at line %d\n", lineno);
+        exit(1);
+    }
+}
+
+static krb5_data princ_data[2] = {
+    { KV5M_DATA, 3, "xyz" },
+    { KV5M_DATA, 3, "abc" }
+};
+
+static krb5_principal_data sample_princ = {
+    KV5M_PRINCIPAL,
+    { KV5M_DATA, 11, "KRBTEST.COM" },
+    princ_data, 2, KRB5_NT_UNKNOWN
+};
+
+static krb5_principal_data xrealm_princ = {
+    KV5M_PRINCIPAL,
+    { KV5M_DATA, 12, "KRBTEST2.COM" },
+    princ_data, 2, KRB5_NT_UNKNOWN
+};
+
+#define U(x) (unsigned char *)x
+
+/*
+ * tl1 through tl4 are normalized to attributes in the LDAP back end.  tl5 is
+ * stored as untranslated tl-data.  tl3 contains an encoded osa_princ_ent with
+ * a policy reference to "testpol".
+ */
+static krb5_tl_data tl5 = { NULL, KRB5_TL_MKVNO, 2, U("\0\1") };
+static krb5_tl_data tl4 = { &tl5, KRB5_TL_LAST_ADMIN_UNLOCK, 4,
+                            U("\6\0\0\0") };
+static krb5_tl_data tl3 = { &tl4, KRB5_TL_KADM_DATA, 32,
+                            U("\x12\x34\x5C\x01\x00\x00\x00\x08"
+                              "\x74\x65\x73\x74\x70\x6F\x6C\x00"
+                              "\x00\x00\x08\x00\x00\x00\x00\x00"
+                              "\x00\x00\x00\x02\x00\x00\x00\x00") };
+static krb5_tl_data tl2 = { &tl3, KRB5_TL_MOD_PRINC, 8, U("\5\6\7\0x at Y\0") };
+static krb5_tl_data tl1 = { &tl2, KRB5_TL_LAST_PWD_CHANGE, 4, U("\1\2\3\4") };
+
+/* An encoded osa_print_enc with no policy reference. */
+static krb5_tl_data tl_no_policy = { NULL, KRB5_TL_KADM_DATA, 24,
+                                     U("\x12\x34\x5C\x01\x00\x00\x00\x00"
+                                       "\x00\x00\x00\x00\x00\x00\x00\x00"
+                                       "\x00\x00\x00\x02\x00\x00\x00\x00") };
+
+static krb5_key_data keys[] = {
+    {
+        2,                          /* key_data_ver */
+        2,                          /* key_data_kvno */
+        { ENCTYPE_AES256_CTS_HMAC_SHA1_96, KRB5_KDB_SALTTYPE_SPECIAL },
+        { 32, 7 },
+        { U("\x17\xF2\x75\xF2\x95\x4F\x2E\xD1"
+            "\xF9\x0C\x37\x7B\xA7\xF4\xD6\xA3"
+            "\x69\xAA\x01\x36\xE0\xBF\x0C\x92"
+            "\x7A\xD6\x13\x3C\x69\x37\x59\xA9"),
+          U("expsalt") }
+    },
+    {
+        1,                          /* key_data_ver */
+        2,                          /* key_data_kvno */
+        { ENCTYPE_AES128_CTS_HMAC_SHA1_96, 0 },
+        { 16, 0 },
+        { U("\xDC\xEE\xB7\x0B\x3D\xE7\x65\x62"
+            "\xE6\x89\x22\x6C\x76\x42\x91\x48"),
+          NULL }
+    }
+};
+#undef U
+
+static krb5_db_entry sample_entry = {
+    0,
+    KRB5_KDB_V1_BASE_LENGTH,
+    /* mask */
+    KADM5_PRINCIPAL | KADM5_PRINC_EXPIRE_TIME | KADM5_PW_EXPIRATION |
+    KADM5_ATTRIBUTES | KADM5_MAX_LIFE | KADM5_POLICY | KADM5_MAX_RLIFE |
+    KADM5_LAST_SUCCESS | KADM5_LAST_FAILED | KADM5_FAIL_AUTH_COUNT |
+    KADM5_KEY_DATA | KADM5_TL_DATA,
+    /* attributes */
+    KRB5_KDB_REQUIRES_PRE_AUTH | KRB5_KDB_REQUIRES_HW_AUTH |
+    KRB5_KDB_DISALLOW_SVR,
+    1234,                       /* max_life */
+    5678,                       /* max_renewable_life */
+    9012,                       /* expiration */
+    3456,                       /* pw_expiration */
+    1,                          /* last_success */
+    5,                          /* last_failed */
+    2,                          /* fail_auth_count */
+    5,                          /* n_tl_data */
+    2,                          /* n_key_data */
+    0, NULL,                    /* e_length, e_data */
+    &sample_princ,
+    &tl1,
+    keys
+};
+
+static osa_policy_ent_rec sample_policy = {
+    0,                          /* version */
+    "testpol",                  /* name */
+    1357,                       /* pw_min_life */
+    100,                        /* pw_max_life */
+    6,                          /* pw_min_length */
+    2,                          /* pw_min_classes */
+    3,                          /* pw_history_num */
+    1,                          /* policy_refcnt */
+    2,                          /* pw_max_fail */
+    60,                         /* pw_failcnt_interval */
+    120,                        /* pw_lockout_duration */
+    0,                          /* attributes */
+    2468,                       /* max_life */
+    3579,                       /* max_renewable_life */
+    "aes",                      /* allowed_keysalts */
+    0, NULL                     /* n_tl_data, tl_data */
+};
+
+/* Compare pol against sample_policy. */
+static void
+check_policy(osa_policy_ent_t pol)
+{
+    CHECK_COND(strcmp(pol->name, sample_policy.name) == 0);
+    CHECK_COND(pol->pw_min_life == sample_policy.pw_min_life);
+    CHECK_COND(pol->pw_max_life == sample_policy.pw_max_life);
+    CHECK_COND(pol->pw_min_length == sample_policy.pw_min_length);
+    CHECK_COND(pol->pw_min_classes == sample_policy.pw_min_classes);
+    CHECK_COND(pol->pw_history_num == sample_policy.pw_history_num);
+    CHECK_COND(pol->pw_max_life == sample_policy.pw_max_life);
+    CHECK_COND(pol->pw_failcnt_interval == sample_policy.pw_failcnt_interval);
+    CHECK_COND(pol->pw_lockout_duration == sample_policy.pw_lockout_duration);
+    CHECK_COND(pol->attributes == sample_policy.attributes);
+    CHECK_COND(pol->max_life == sample_policy.max_life);
+    CHECK_COND(pol->max_renewable_life == sample_policy.max_renewable_life);
+    CHECK_COND(strcmp(pol->allowed_keysalts,
+                      sample_policy.allowed_keysalts) == 0);
+}
+
+/* Compare ent against sample_entry. */
+static void
+check_entry(krb5_db_entry *ent)
+{
+    krb5_int16 i, j;
+    krb5_key_data *k1, *k2;
+    krb5_tl_data *tl, etl;
+
+    CHECK_COND(ent->attributes == sample_entry.attributes);
+    CHECK_COND(ent->max_life == sample_entry.max_life);
+    CHECK_COND(ent->max_renewable_life == sample_entry.max_renewable_life);
+    CHECK_COND(ent->expiration == sample_entry.expiration);
+    CHECK_COND(ent->pw_expiration == sample_entry.pw_expiration);
+    CHECK_COND(ent->last_success == sample_entry.last_success);
+    CHECK_COND(ent->last_failed == sample_entry.last_failed);
+    CHECK_COND(ent->fail_auth_count == sample_entry.fail_auth_count);
+    CHECK_COND(krb5_principal_compare(ctx, ent->princ, sample_entry.princ));
+    CHECK_COND(ent->n_key_data == sample_entry.n_key_data);
+    for (i = 0; i < ent->n_key_data; i++) {
+        k1 = &ent->key_data[i];
+        k2 = &sample_entry.key_data[i];
+        CHECK_COND(k1->key_data_ver == k2->key_data_ver);
+        CHECK_COND(k1->key_data_kvno == k2->key_data_kvno);
+        for (j = 0; j < k1->key_data_ver; j++) {
+            CHECK_COND(k1->key_data_type[j] == k2->key_data_type[j]);
+            CHECK_COND(k1->key_data_length[j] == k2->key_data_length[j]);
+            CHECK_COND(memcmp(k1->key_data_contents[j],
+                              k2->key_data_contents[j],
+                              k1->key_data_length[j]) == 0);
+        }
+    }
+    for (tl = sample_entry.tl_data; tl != NULL; tl = tl->tl_data_next) {
+        etl.tl_data_type = tl->tl_data_type;
+        CHECK(krb5_dbe_lookup_tl_data(ctx, ent, &etl));
+        CHECK_COND(tl->tl_data_length == etl.tl_data_length);
+        CHECK_COND(memcmp(tl->tl_data_contents, etl.tl_data_contents,
+                          tl->tl_data_length) == 0);
+    }
+}
+
+/* Audit a successful or failed preauth attempt for *entp.  Then reload *entp
+ * (by fetching sample_princ) so we can see the effect. */
+static void
+sim_preauth(krb5_timestamp authtime, krb5_boolean ok, krb5_db_entry **entp)
+{
+    /* Both back ends ignore the request parameter for now. */
+    krb5_db_audit_as_req(ctx, NULL, *entp, *entp, authtime,
+                         ok ? 0 : KRB5KDC_ERR_PREAUTH_FAILED);
+    krb5_db_free_principal(ctx, *entp);
+    CHECK(krb5_db_get_principal(ctx, &sample_princ, 0, entp));
+}
+
+static krb5_error_code
+iter_princ_handler(void *data, krb5_db_entry *ent)
+{
+    int *count = data;
+
+    CHECK_COND(krb5_principal_compare(ctx, ent->princ, sample_entry.princ));
+    (*count)++;
+    return 0;
+}
+
+static void
+iter_pol_handler(void *data, osa_policy_ent_t pol)
+{
+    int *count = data;
+
+    CHECK_COND(strcmp(pol->name, sample_policy.name) == 0);
+    (*count)++;
+}
+
+int
+main()
+{
+    krb5_db_entry *ent;
+    osa_policy_ent_t pol;
+    krb5_pa_data **e_data;
+    const char *status;
+    char *defrealm;
+    int count;
+
+    CHECK(krb5_init_context_profile(NULL, KRB5_INIT_CONTEXT_KDC, &ctx));
+
+    /* Currently necessary for krb5_db_open to work. */
+    CHECK(krb5_get_default_realm(ctx, &defrealm));
+
+    /* If we can, revert to requiring all entries match sample_princ in
+     * iter_princ_handler */
+    CHECK_COND(krb5_db_inited(ctx) != 0);
+    CHECK(krb5_db_create(ctx, NULL));
+    CHECK(krb5_db_inited(ctx));
+    CHECK(krb5_db_fini(ctx));
+    CHECK_COND(krb5_db_inited(ctx) != 0);
+
+    CHECK_COND(krb5_db_inited(ctx) != 0);
+    CHECK(krb5_db_open(ctx, NULL, KRB5_KDB_OPEN_RW | KRB5_KDB_SRV_TYPE_ADMIN));
+    CHECK(krb5_db_inited(ctx));
+
+    /* Manipulate a policy, leaving testpol in place at the end. */
+    CHECK_COND(krb5_db_put_policy(ctx, &sample_policy) != 0);
+    CHECK_COND(krb5_db_delete_policy(ctx, "testpol") != 0);
+    CHECK_COND(krb5_db_get_policy(ctx, "testpol", &pol) == KRB5_KDB_NOENTRY);
+    CHECK(krb5_db_create_policy(ctx, &sample_policy));
+    CHECK_COND(krb5_db_create_policy(ctx, &sample_policy) != 0);
+    CHECK(krb5_db_get_policy(ctx, "testpol", &pol));
+    check_policy(pol);
+    pol->pw_min_length--;
+    CHECK(krb5_db_put_policy(ctx, pol));
+    krb5_db_free_policy(ctx, pol);
+    CHECK(krb5_db_get_policy(ctx, "testpol", &pol));
+    CHECK_COND(pol->pw_min_length == sample_policy.pw_min_length - 1);
+    krb5_db_free_policy(ctx, pol);
+    CHECK(krb5_db_delete_policy(ctx, "testpol"));
+    CHECK_COND(krb5_db_put_policy(ctx, &sample_policy) != 0);
+    CHECK_COND(krb5_db_delete_policy(ctx, "testpol") != 0);
+    CHECK_COND(krb5_db_get_policy(ctx, "testpol", &pol) == KRB5_KDB_NOENTRY);
+    CHECK(krb5_db_create_policy(ctx, &sample_policy));
+    count = 0;
+    CHECK(krb5_db_iter_policy(ctx, NULL, iter_pol_handler, &count));
+    CHECK_COND(count == 1);
+
+    /* Create a principal. */
+    CHECK_COND(krb5_db_delete_principal(ctx, &sample_princ) ==
+               KRB5_KDB_NOENTRY);
+    CHECK_COND(krb5_db_get_principal(ctx, &xrealm_princ, 0, &ent) ==
+               KRB5_KDB_NOENTRY);
+    CHECK(krb5_db_put_principal(ctx, &sample_entry));
+    /* Putting again will fail with LDAP (due to KADM5_PRINCIPAL in mask)
+     * but succeed with DB2, so don't check the result. */
+    (void)krb5_db_put_principal(ctx, &sample_entry);
+    /* But it should succeed in both back ends with KADM5_LOAD in mask. */
+    sample_entry.mask |= KADM5_LOAD;
+    CHECK(krb5_db_put_principal(ctx, &sample_entry));
+    sample_entry.mask &= ~KADM5_LOAD;
+    /* Fetch and compare the added principal. */
+    CHECK(krb5_db_get_principal(ctx, &sample_princ, 0, &ent));
+    check_entry(ent);
+
+    /* We can't set up a successful allowed-to-delegate check through existing
+     * APIs yet, but we can make a failed check. */
+    CHECK_COND(krb5_db_check_allowed_to_delegate(ctx, &sample_princ, ent,
+                                                 &sample_princ) != 0);
+
+    /* Exercise lockout code. */
+    /* Policy params: max_fail 2, failcnt_interval 60, lockout_duration 120 */
+    /* Initial state: last_success 1, last_failed 5, fail_auth_count 2,
+     * last admin unlock 6 */
+    /* Check succeeds due to last admin unlock. */
+    CHECK(krb5_db_check_policy_as(ctx, NULL, ent, ent, 7, &status, &e_data));
+    /* Failure count resets to 1 due to last admin unlock. */
+    sim_preauth(8, FALSE, &ent);
+    CHECK_COND(ent->fail_auth_count == 1 && ent->last_failed == 8);
+    /* Failure count resets to 1 due to failcnt_interval */
+    sim_preauth(70, FALSE, &ent);
+    CHECK_COND(ent->fail_auth_count == 1 && ent->last_failed == 70);
+    /* Failure count resets to 0 due to successful preauth. */
+    sim_preauth(75, TRUE, &ent);
+    CHECK_COND(ent->fail_auth_count == 0 && ent->last_success == 75);
+    /* Failure count increments to 2 and stops incrementing. */
+    sim_preauth(80, FALSE, &ent);
+    CHECK_COND(ent->fail_auth_count == 1 && ent->last_failed == 80);
+    sim_preauth(100, FALSE, &ent);
+    CHECK_COND(ent->fail_auth_count == 2 && ent->last_failed == 100);
+    sim_preauth(110, FALSE, &ent);
+    CHECK_COND(ent->fail_auth_count == 2 && ent->last_failed == 100);
+    /* Check fails due to reaching maximum failure count. */
+    CHECK_COND(krb5_db_check_policy_as(ctx, NULL, ent, ent, 170, &status,
+                                       &e_data) == KRB5KDC_ERR_CLIENT_REVOKED);
+    /* Check succeeds after lockout_duration has passed. */
+    CHECK(krb5_db_check_policy_as(ctx, NULL, ent, ent, 230, &status, &e_data));
+    /* Failure count resets to 1 on next failure. */
+    sim_preauth(240, FALSE, &ent);
+    CHECK_COND(ent->fail_auth_count == 1 && ent->last_failed == 240);
+
+    /* Exercise LDAP code to clear a policy reference and to set the key
+     * data on an existing principal. */
+    CHECK(krb5_dbe_update_tl_data(ctx, ent, &tl_no_policy));
+    ent->mask = KADM5_POLICY_CLR | KADM5_KEY_DATA;
+    CHECK(krb5_db_put_principal(ctx, ent));
+    /* Deleting testpol should work now that the reference is gone. */
+    CHECK(krb5_db_delete_policy(ctx, "testpol"));
+
+    /* Put the modified entry again (with KDB_TL_USER_INFO tl-data for LDAP) as
+     * from a load operation. */
+    ent->mask = (sample_entry.mask & ~KADM5_POLICY) | KADM5_LOAD;
+    CHECK(krb5_db_put_principal(ctx, ent));
+
+    /* Exercise LDAP code to create a new principal at a DN from
+     * KDB_TL_USER_INFO tl-data. */
+    CHECK(krb5_db_delete_principal(ctx, &sample_princ));
+    CHECK(krb5_db_put_principal(ctx, ent));
+    krb5_db_free_principal(ctx, ent);
+
+    /* Exercise principal iteration code. */
+    count = 0;
+    CHECK(krb5_db_iterate(ctx, "xyz*", iter_princ_handler, &count));
+    CHECK_COND(count == 1);
+
+    CHECK(krb5_db_fini(ctx));
+    CHECK_COND(krb5_db_inited(ctx) != 0);
+
+    /* It might be nice to exercise krb5_db_destroy here, but the LDAP module
+     * doesn't support it. */
+
+    krb5_free_default_realm(ctx, defrealm);
+    krb5_free_context(ctx);
+    return 0;
+}
diff --git a/src/tests/t_kdb.py b/src/tests/t_kdb.py
new file mode 100644
index 0000000..dd79b42
--- /dev/null
+++ b/src/tests/t_kdb.py
@@ -0,0 +1,280 @@
+#!/usr/bin/python
+from k5test import *
+import time
+
+# Return the location of progname in tht executable path, or None if
+# it is not found.
+def which(progname):
+    for dir in os.environ["PATH"].split(os.pathsep):
+        path = os.path.join(dir, progname)
+        if os.access(path, os.X_OK):
+            return path
+    return None
+
+# Run kdbtest against the BDB module.
+realm = K5Realm(create_kdb=False)
+realm.run_as_master(['./kdbtest'])
+
+# Set up an OpenLDAP test server if we can.
+
+if (not os.path.exists(os.path.join(plugins, 'kdb', 'kldap.so')) and
+    not os.path.exists(os.path.join(buildtop, 'lib', 'libkdb_ldap.a'))):
+    success('Warning: not testing LDAP back end because it is not built')
+    exit(0)
+
+system_slapd = which('slapd')
+if not system_slapd:
+    success('Warning: not testing LDAP module because slapd not found')
+    exit(0)
+
+ldapdir = os.path.abspath('ldap')
+slapd = os.path.join(ldapdir, 'slapd')
+dbdir = os.path.join(ldapdir, 'ldap')
+slapd_conf = os.path.join(ldapdir, 'slapd.conf')
+slapd_out = os.path.join(ldapdir, 'slapd.out')
+slapd_pidfile = os.path.join(ldapdir, 'pid')
+ldap_pwfile = os.path.join(ldapdir, 'pw')
+ldap_sock = os.path.join(ldapdir, 'sock')
+ldap_uri = 'ldapi://%s/' % ldap_sock.replace(os.path.sep, '%2F')
+schema = os.path.join(srctop, 'plugins', 'kdb', 'ldap', 'libkdb_ldap',
+                      'kerberos.schema')
+top_dn = 'cn=krb5'
+admin_dn = 'cn=admin,cn=krb5'
+admin_pw = 'admin'
+
+shutil.rmtree(ldapdir, True)
+os.mkdir(ldapdir)
+os.mkdir(dbdir)
+
+# Some Linux installations have AppArmor or similar restrictions on
+# the slapd binary, which would prevent it from accessing the build
+# directory.  Try to defeat this by copying the binary.
+shutil.copy(system_slapd, slapd)
+
+# Make a slapd config file.  This is deprecated in OpenLDAP 2.3 and
+# later, but it's easier than using LDIF and slapadd.
+file = open(slapd_conf, 'w')
+file.write('pidfile %s\n' % slapd_pidfile)
+file.write('include %s\n' % schema)
+file.write('moduleload back_bdb\n')
+file.write('database bdb\n')
+file.write('suffix %s\n' % top_dn)
+file.write('rootdn %s\n' % admin_dn)
+file.write('rootpw %s\n' % admin_pw)
+file.write('directory %s\n' % dbdir)
+file.close()
+
+slapd_pid = -1
+def kill_slapd():
+    global slapd_pid
+    if slapd_pid != -1:
+        os.kill(slapd_pid, signal.SIGTERM)
+        slapd_pid = -1
+atexit.register(kill_slapd)
+
+out = open(slapd_out, 'w')
+subprocess.call([slapd, '-h', ldap_uri, '-f', slapd_conf], stdout=out,
+                stderr=out)
+out.close()
+pidf = open(slapd_pidfile, 'r')
+slapd_pid = int(pidf.read())
+pidf.close()
+output('*** Started slapd (pid %d, output in %s)\n' % (slapd_pid, slapd_out))
+
+# slapd detaches before it finishes setting up its listener sockets
+# (they are bound but listen() has not been called).  Give it a second
+# to finish.
+time.sleep(1)
+
+# Run kdbtest against the LDAP module.
+kdc_conf = {'all': {
+                'realms': {'$realm': {'database_module': 'ldap'}},
+                'dbmodules': {'ldap': {
+                        'db_library': 'kldap',
+                        'ldap_kerberos_container_dn': top_dn,
+                        'ldap_kdc_dn': admin_dn,
+                        'ldap_kadmind_dn': admin_dn,
+                        'ldap_service_password_file': ldap_pwfile,
+                        'ldap_servers': ldap_uri}}}}
+realm = K5Realm(create_kdb=False, kdc_conf=kdc_conf)
+input = admin_pw + '\n' + admin_pw + '\n'
+realm.run_as_master([kdb5_ldap_util, 'stashsrvpw', admin_dn], input=input)
+realm.run_as_master(['./kdbtest'])
+
+# Run a kdb5_ldap_util command using the test server's admin DN and password.
+def kldaputil(args, **kw):
+    return realm.run_as_master([kdb5_ldap_util, '-D', admin_dn, '-w',
+                                admin_pw] + args, **kw)
+
+# kdbtest can't currently clean up after itself since the LDAP module
+# doesn't support krb5_db_destroy.  So clean up after it with
+# kdb5_ldap_util before proceeding.
+kldaputil(['destroy', '-f'])
+
+ldapadd = which('ldapadd')
+if not ldapadd:
+    success('Warning: skipping some LDAP tests because ldapadd not found')
+    exit(0)
+
+def ldap_add(dn, objectclass, attrs=[]):
+    proc = subprocess.Popen([ldapadd, '-H', ldap_uri, '-D', admin_dn, '-x',
+                             '-w', admin_pw], stdin=subprocess.PIPE,
+                            stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    in_data = 'dn: %s\nobjectclass: %s\n' % (dn, objectclass)
+    in_data += '\n'.join(attrs) + '\n'
+    (out, dummy) = proc.communicate(in_data)
+    output(out)
+
+# Create krbContainer objects for use as subtrees.
+ldap_add('cn=t1,cn=krb5', 'krbContainer')
+ldap_add('cn=t2,cn=krb5', 'krbContainer')
+ldap_add('cn=x,cn=t1,cn=krb5', 'krbContainer')
+ldap_add('cn=y,cn=t2,cn=krb5', 'krbContainer')
+
+# Create a realm, exercising all of the realm options.
+kldaputil(['create', '-s', '-P', 'master', '-subtrees', 'cn=t2,cn=krb5',
+           '-containerref', 'cn=t2,cn=krb5', '-sscope', 'one',
+           '-maxtktlife', '5min', '-maxrenewlife', '10min', '-allow_svr'])
+
+# Modify the realm, exercising overlapping subtree pruning.
+kldaputil(['modify', '-subtrees',
+           'cn=x,cn=t1,cn=krb5:cn=t1,cn=krb5:cn=t2,cn=krb5:cn=y,cn=t2,cn=krb5',
+           '-containerref', 'cn=t1,cn=krb5', '-sscope', 'sub',
+           '-maxtktlife', '5hour', '-maxrenewlife', '10hour', '+allow_svr'])
+
+out = kldaputil(['list'])
+if out != 'KRBTEST.COM\n':
+    fail('Unexpected kdb5_ldap_util list output')
+
+# Create a principal at a specified DN.  This is a little dodgy
+# because we're sticking a krbPrincipalAux objectclass onto a subtree
+# krbContainer, but it works and it avoids having to load core.schema
+# in the test LDAP server.
+out = realm.run_kadminl('ank -randkey -x dn=cn=krb5 princ1')
+if 'DN is out of the realm subtree' not in out:
+    fail('Unexpected kadmin.local output for out-of-realm dn')
+out = realm.run_kadminl('ank -randkey -x dn=cn=t2,cn=krb5 princ1')
+if 'Principal "princ1 at KRBTEST.COM" created.\n' not in  out:
+    fail('Unexpected kadmin.local output for specified dn')
+out = realm.run_kadminl('getprinc princ1')
+if 'Principal: princ1' not in out:
+    fail('Unexpected kadmin.local output after creating princ1')
+out = realm.run_kadminl('ank -randkey -x dn=cn=t2,cn=krb5 again')
+if 'ldap object is already kerberized' not in out:
+    fail('Unexpected kadmin.local output trying to re-kerberize DN')
+# Check that we can't set linkdn on a non-standalone object.
+out = realm.run_kadminl('modprinc -x linkdn=cn=t1,cn=krb5 princ1')
+if 'link information can not be set' not in out:
+    fail('Unexpected kadmin.local output trying to set linkdn on princ1')
+
+# Create a principal with a specified linkdn.
+out = realm.run_kadminl('ank -randkey -x linkdn=cn=krb5 princ2')
+if 'DN is out of the realm subtree' not in out:
+    fail('Unexpected kadmin.local output for out-of-realm linkdn')
+out = realm.run_kadminl('ank -randkey -x linkdn=cn=t1,cn=krb5 princ2')
+if 'Principal "princ2 at KRBTEST.COM" created.\n' not in out:
+    fail('Unexpected kadmin.local output for specified linkdn')
+# Check that we can't reset linkdn.
+out = realm.run_kadminl('modprinc -x linkdn=cn=t2,cn=krb5 princ2')
+if 'kerberos principal is already linked' not in out:
+    fail('Unexpected kadmin.local output for re-specified linkdn')
+
+# Create a principal with a specified containerdn.
+out = realm.run_kadminl('ank -randkey -x containerdn=cn=krb5 princ3')
+if 'DN is out of the realm subtree' not in out:
+    fail('Unexpected kadmin.local output for out-of-realm containerdn')
+out = realm.run_kadminl('ank -randkey -x containerdn=cn=t1,cn=krb5 princ3')
+if 'Principal "princ3 at KRBTEST.COM" created.\n' not in out:
+    fail('Unexpected kadmin.local output for specified containerdn')
+out = realm.run_kadminl('modprinc -x containerdn=cn=t2,cn=krb5 princ3')
+if 'containerdn option not supported' not in out:
+    fail('Unexpected kadmin.local output trying to reset containerdn')
+
+# Create and modify a ticket policy.
+kldaputil(['create_policy', '-maxtktlife', '3hour', '-maxrenewlife', '6hour',
+           '-allow_forwardable', 'tktpol'])
+kldaputil(['modify_policy', '-maxtktlife', '4hour', '-maxrenewlife', '8hour',
+           '+requires_preauth', 'tktpol'])
+out = kldaputil(['view_policy', 'tktpol'])
+if ('Ticket policy: tktpol\n' not in out or
+    'Maximum ticket life: 0 days 04:00:00\n' not in out or
+    'Maximum renewable life: 0 days 08:00:00\n' not in out or
+    'Ticket flags: DISALLOW_FORWARDABLE REQUIRES_PRE_AUTH' not in out):
+    fail('Unexpected kdb5_ldap_util view_policy output')
+
+out = kldaputil(['list_policy'])
+if out != 'tktpol\n':
+    fail('Unexpected kdb5_ldap_util list_policy output')
+
+# Associate the ticket policy to a principal.
+realm.run_kadminl('ank -randkey -x tktpolicy=tktpol princ4')
+out = realm.run_kadminl('getprinc princ4')
+if ('Maximum ticket life: 0 days 04:00:00\n' not in out or
+    'Maximum renewable life: 0 days 08:00:00\n' not in out or
+    'Attributes: DISALLOW_FORWARDABLE REQUIRES_PRE_AUTH\n' not in out):
+    fail('Unexpected getprinc output with ticket policy')
+
+# Destroying the policy should fail while a principal references it.
+kldaputil(['destroy_policy', '-force', 'tktpol'], expected_code=1)
+
+# Dissociate the ticket policy from the principal.
+realm.run_kadminl('modprinc -x tktpolicy= princ4')
+out = realm.run_kadminl('getprinc princ4')
+if ('Maximum ticket life: 0 days 05:00:00\n' not in out or
+    'Maximum renewable life: 0 days 10:00:00\n' not in out or
+    'Attributes:\n' not in out):
+    fail('Unexpected getprinc output without ticket policy')
+
+# Destroy the ticket policy.
+kldaputil(['destroy_policy', '-force', 'tktpol'])
+kldaputil(['view_policy', 'tktpol'], expected_code=1)
+out = kldaputil(['list_policy'])
+if out:
+    fail('Unexpected kdb5_ldap_util list_policy output after destroy')
+
+# Create another ticket policy to be destroyed with the realm.
+kldaputil(['create_policy', 'tktpol2'])
+
+# Do some basic tests with a KDC against the LDAP module, exercising the
+# db_args processing code.
+realm.start_kdc(['-x', 'nconns=3', '-x', 'host=' + ldap_uri,
+                 '-x', 'binddn=' + admin_dn, '-x', 'bindpwd=' + admin_pw])
+realm.addprinc(realm.user_princ, password('user'))
+realm.addprinc(realm.host_princ)
+realm.extract_keytab(realm.host_princ, realm.keytab)
+realm.kinit(realm.user_princ, password('user'))
+realm.run_as_client([kvno, realm.host_princ])
+realm.klist(realm.user_princ, realm.host_princ)
+realm.stop()
+
+# Briefly test dump and load.
+dumpfile = os.path.join(realm.testdir, 'dump')
+realm.run_as_master([kdb5_util, 'dump', dumpfile])
+out = realm.run_as_master([kdb5_util, 'load', dumpfile], expected_code=1)
+if 'plugin requires -update argument' not in out:
+    fail('Unexpected error from kdb5_util load without -update')
+realm.run_as_master([kdb5_util, 'load', '-update', dumpfile])
+
+# Destroy the realm.
+kldaputil(['destroy', '-f'])
+out = kldaputil(['list'])
+if out:
+    fail('Unexpected kdb5_ldap_util list output after destroy')
+
+# We could still use tests to exercise:
+# * DB arg handling in krb5_ldap_create
+# * krbAllowedToDelegateTo attribute processing
+# * Special character handling in ldap_filter_correct (some bugs to
+#   fix first, see #7296 and September 2012 krbdev discussion)
+# * A load operation overwriting a standalone principal entry which
+#   already exists but doesn't have a krbPrincipalName attribute
+#   matching the principal name.
+# * A bunch of invalid-input error conditions
+#
+# There is no coverage for the following because it would be difficult:
+# * Out-of-memory error conditions
+# * Handling of failures from slapd (including krb5_retry_get_ldap_handle)
+# * Handling of servers which don't support mod-increment
+# * krb5_ldap_delete_krbcontainer (only happens if krb5_ldap_create fails)
+
+success('LDAP and DB2 KDB tests')
diff --git a/src/util/k5test.py b/src/util/k5test.py
index 3400154..37e8929 100644
--- a/src/util/k5test.py
+++ b/src/util/k5test.py
@@ -194,6 +194,7 @@ Scripts may use the following functions and variables:
   - kadmind
   - kadmin
   - kadmin_local
+  - kdb5_ldap_util
   - kdb5_util
   - ktutil
   - kinit
@@ -1238,6 +1239,8 @@ krb5kdc = os.path.join(buildtop, 'kdc', 'krb5kdc')
 kadmind = os.path.join(buildtop, 'kadmin', 'server', 'kadmind')
 kadmin = os.path.join(buildtop, 'kadmin', 'cli', 'kadmin')
 kadmin_local = os.path.join(buildtop, 'kadmin', 'cli', 'kadmin.local')
+kdb5_ldap_util = os.path.join(buildtop, 'plugins', 'kdb', 'ldap', 'ldap_util',
+                              'kdb5_ldap_util')
 kdb5_util = os.path.join(buildtop, 'kadmin', 'dbutil', 'kdb5_util')
 ktutil = os.path.join(buildtop, 'kadmin', 'ktutil', 'ktutil')
 kinit = os.path.join(buildtop, 'clients', 'kinit', 'kinit')


More information about the cvs-krb5 mailing list