/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* plugins/kdb/test/kdb_test.c - Test KDB module */
/*
 * Copyright (C) 2015 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 is a read-only KDB module intended to help test KDC behavior which
 * cannot be exercised with the DB2 module.  Responses are read from the
 * dbmodules subsection according to this example:
 *
 *     [dbmodules]
 *         test = {
 *             alias = {
 *                 aliasname = canonname
 *                 # For cross-realm aliases, only the realm part will
 *                 # matter to the client.
 *                 aliasname = @FOREIGN_REALM
 *                 enterprise@PRINC = @FOREIGN_REALM
 *             }
 *             princs = {
 *                 krbtgt/KRBTEST.COM = {
 *                     flags = +preauth +ok-to-auth-as-delegate
 *                     maxlife = 1d
 *                     maxrenewlife = 7d
 *                     expiration = 14d # relative to current time
 *                     pwexpiration = 1h
 *                     # Initial number is kvno; defaults to 1.
 *                     keys = 3 aes256-cts aes128-cts:normal
 *                     keys = 2 rc4-hmac
 *                     strings = key1:value1
 *                     strings = key2:value2
 *                 }
 *             }
 *             delegation = {
 *                 # Traditional constrained delegation; target_service
 *                 # must be in the same realm.
 *                 intermediate_service = target_service
 *             }
 *             rbcd = {
 *                 # Resource-based constrained delegation;
 *                 # intermediate_service may be in a different realm.
 *                 target_service = intermediate_service
 *             }
 *         }
 *
 * Key values are generated using a hash of the kvno, enctype, salt type,
 * principal name, and lookup realm.  This module does not use master key
 * encryption, so it serves as a partial test of the DAL's ability to avoid
 * that.
 *
 * Inbound cross-realm TGT entries are currently implicit; they will use the
 * same configuration and key enctypes as the local krbtgt principal, although
 * they will use different keys (because the lookup realm is hashed in).
 * Outgoing cross-realm TGT entries must be added explicitly
 * (krbtgt/OTHER_REALM).
 */

#include "k5-int.h"
#include "kdb5.h"
#include "adm_proto.h"
#include <ctype.h>

#define TEST_AD_TYPE -456

#define IS_TGS_PRINC(p) ((p)->length == 2 &&                            \
                         data_eq_string((p)->data[0], KRB5_TGS_NAME))

typedef struct {
    void *profile;
    char *section;
    const char *names[6];
} *testhandle;

static void *
ealloc(size_t sz)
{
    void *p = calloc(sz, 1);

    if (p == NULL)
        abort();
    return p;
}

static char *
estrdup(const char *s)
{
    char *copy = strdup(s);

    if (copy == NULL)
        abort();
    return copy;
}

static void
check(krb5_error_code code)
{
    if (code != 0)
        abort();
}

/* Set up for a profile query using h->names.  Look up s1 -> s2 -> s3 (some of
 * which may be NULL) within this database's dbmodules section. */
static void
set_names(testhandle h, const char *s1, const char *s2, const char *s3)
{
    h->names[0] = KDB_MODULE_SECTION;
    h->names[1] = h->section;
    h->names[2] = s1;
    h->names[3] = s2;
    h->names[4] = s3;
    h->names[5] = NULL;
}

/* Look up a string within this database's dbmodules section. */
static char *
get_string(testhandle h, const char *s1, const char *s2, const char *s3)
{
    krb5_error_code ret;
    char **values, *val;

    set_names(h, s1, s2, s3);
    ret = profile_get_values(h->profile, h->names, &values);
    if (ret == PROF_NO_RELATION)
        return NULL;
    if (ret)
        abort();
    val = estrdup(values[0]);
    profile_free_list(values);
    return val;
}

/* Look up a duration within this database's dbmodules section. */
static krb5_deltat
get_duration(testhandle h, const char *s1, const char *s2, const char *s3)
{
    char *strval = get_string(h, s1, s2, s3);
    krb5_deltat val;

    if (strval == NULL)
        return 0;
    check(krb5_string_to_deltat(strval, &val));
    free(strval);
    return val;
}

/* Look up an absolute time within this database's dbmodules section.  The time
 * is expressed in the profile as an interval relative to the current time. */
static krb5_timestamp
get_time(testhandle h, const char *s1, const char *s2, const char *s3)
{
    char *strval = get_string(h, s1, s2, s3);
    krb5_deltat val;

    if (strval == NULL)
        return 0;
    check(krb5_string_to_deltat(strval, &val));
    free(strval);
    return val + time(NULL);
}

/* Initialize kb_out with a key of type etype, using a hash of kvno, etype,
 * salttype, and princstr for the key bytes. */
static void
make_keyblock(krb5_kvno kvno, krb5_enctype etype, int32_t salttype,
              const char *princstr, const krb5_data *realm,
              krb5_keyblock *kb_out)
{
    size_t keybytes, keylength, pos, n;
    char *hashstr;
    krb5_data d, rndin;
    krb5_checksum cksum;

    check(krb5_c_keylengths(NULL, etype, &keybytes, &keylength));
    alloc_data(&rndin, keybytes);

    /* Hash the kvno, enctype, salt type, and principal name together. */
    if (asprintf(&hashstr, "%d %d %d %s %.*s", (int)kvno, (int)etype,
                 (int)salttype, princstr, (int)realm->length, realm->data) < 0)
        abort();
    d = string2data(hashstr);
    check(krb5_c_make_checksum(NULL, CKSUMTYPE_SHA1, NULL, 0, &d, &cksum));

    /* Make the appropriate number of input bytes from the hash result. */
    for (pos = 0; pos < keybytes; pos += n) {
        n = (cksum.length < keybytes - pos) ? cksum.length : keybytes - pos;
        memcpy(rndin.data + pos, cksum.contents, n);
    }

    kb_out->enctype = etype;
    kb_out->length = keylength;
    kb_out->contents = ealloc(keylength);
    check(krb5_c_random_to_key(NULL, etype, &rndin, kb_out));
    free(cksum.contents);
    free(rndin.data);
    free(hashstr);
}

/* Return key data for the given key/salt tuple strings, using hashes of the
 * enctypes, salts, and princstr for the key contents. */
static void
make_keys(char **strings, const char *princstr, const krb5_data *realm,
          krb5_db_entry *ent)
{
    krb5_key_data *key_data, *kd;
    krb5_keyblock kb;
    int32_t *ks_list_sizes, nstrings, nkeys, i, j;
    krb5_key_salt_tuple **ks_lists, *ks;
    krb5_kvno *kvnos;
    char *s;

    for (nstrings = 0; strings[nstrings] != NULL; nstrings++);
    ks_lists = ealloc(nstrings * sizeof(*ks_lists));
    ks_list_sizes = ealloc(nstrings * sizeof(*ks_list_sizes));
    kvnos = ealloc(nstrings * sizeof(*kvnos));

    /* Convert each string into a key/salt tuple list and count the total
     * number of key data structures needed. */
    nkeys = 0;
    for (i = 0; i < nstrings; i++) {
        s = strings[i];
        /* Read a leading kvno if present; otherwise assume kvno 1. */
        if (isdigit(*s)) {
            kvnos[i] = strtol(s, &s, 10);
            while (isspace(*s))
                s++;
        } else {
            kvnos[i] = 1;
        }
        check(krb5_string_to_keysalts(s, NULL, NULL, FALSE, &ks_lists[i],
                                      &ks_list_sizes[i]));
        nkeys += ks_list_sizes[i];
    }

    /* Turn each key/salt tuple into a key data entry. */
    kd = key_data = ealloc(nkeys * sizeof(*kd));
    for (i = 0; i < nstrings; i++) {
        ks = ks_lists[i];
        for (j = 0; j < ks_list_sizes[i]; j++) {
            make_keyblock(kvnos[i], ks[j].ks_enctype, ks[j].ks_salttype,
                          princstr, realm, &kb);
            kd->key_data_ver = 2;
            kd->key_data_kvno = kvnos[i];
            kd->key_data_type[0] = ks[j].ks_enctype;
            kd->key_data_length[0] = kb.length;
            kd->key_data_contents[0] = kb.contents;
            kd->key_data_type[1] = ks[j].ks_salttype;
            kd++;
        }
    }

    for (i = 0; i < nstrings; i++)
        free(ks_lists[i]);
    free(ks_lists);
    free(ks_list_sizes);
    free(kvnos);
    ent->key_data = key_data;
    ent->n_key_data = nkeys;
}

static void
make_strings(char **stringattrs, krb5_db_entry *ent)
{
    struct k5buf buf;
    char **p;
    const char *str, *sep;
    krb5_tl_data *tl;

    k5_buf_init_dynamic(&buf);
    for (p = stringattrs; *p != NULL; p++) {
        str = *p;
        sep = strchr(str, ':');
        assert(sep != NULL);
        k5_buf_add_len(&buf, str, sep - str);
        k5_buf_add_len(&buf, "\0", 1);
        k5_buf_add_len(&buf, sep + 1, strlen(sep + 1) + 1);
    }
    assert(buf.data != NULL);

    tl = ealloc(sizeof(*ent->tl_data));
    tl->tl_data_next = NULL;
    tl->tl_data_type = KRB5_TL_STRING_ATTRS;
    tl->tl_data_length = buf.len;
    tl->tl_data_contents = buf.data;
    ent->tl_data = tl;
}

static krb5_error_code
test_init()
{
    return 0;
}

static krb5_error_code
test_cleanup()
{
    return 0;
}

static krb5_error_code
test_open(krb5_context context, char *conf_section, char **db_args, int mode)
{
    testhandle h;

    h = ealloc(sizeof(*h));
    h->profile = context->profile;
    h->section = estrdup(conf_section);
    context->dal_handle->db_context = h;
    return 0;
}

static krb5_error_code
test_close(krb5_context context)
{
    testhandle h = context->dal_handle->db_context;

    free(h->section);
    free(h);
    return 0;
}

/* Return the principal name krbtgt/tgs_realm@our_realm. */
static krb5_principal
tgtname(krb5_context context, const krb5_data *tgs_realm,
        const krb5_data *our_realm)
{
    krb5_principal princ;

    check(krb5_build_principal_ext(context, &princ,
                                   our_realm->length, our_realm->data,
                                   KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME,
                                   tgs_realm->length, tgs_realm->data, 0));
    princ->type = KRB5_NT_SRV_INST;
    return princ;
}

/* Return true if search_for is within context's default realm or is an
 * incoming cross-realm TGS name. */
static krb5_boolean
request_for_us(krb5_context context, krb5_const_principal search_for)
{
    char *defrealm;
    krb5_data realm;
    krb5_boolean for_us;
    krb5_principal local_tgs;

    check(krb5_get_default_realm(context, &defrealm));
    realm = string2data(defrealm);
    local_tgs = tgtname(context, &realm, &realm);
    krb5_free_default_realm(context, defrealm);

    for_us = krb5_realm_compare(context, local_tgs, search_for) ||
        krb5_principal_compare_any_realm(context, local_tgs, search_for);
    krb5_free_principal(context, local_tgs);
    return for_us;
}

static krb5_error_code
test_get_principal(krb5_context context, krb5_const_principal search_for,
                   unsigned int flags, krb5_db_entry **entry)
{
    krb5_error_code ret;
    krb5_principal princ = NULL, tgtprinc;
    krb5_principal_data empty_princ = { KV5M_PRINCIPAL };
    testhandle h = context->dal_handle->db_context;
    char *search_name = NULL, *canon = NULL, *flagstr;
    char **names, **key_strings, **stringattrs;
    const char *ename;
    krb5_db_entry *ent;

    *entry = NULL;

    if (!request_for_us(context, search_for))
        return KRB5_KDB_NOENTRY;

    check(krb5_unparse_name_flags(context, search_for,
                                  KRB5_PRINCIPAL_UNPARSE_NO_REALM,
                                  &search_name));
    canon = get_string(h, "alias", search_name, NULL);
    if (canon != NULL) {
        check(krb5_parse_name(context, canon, &princ));
        if (!krb5_realm_compare(context, search_for, princ)) {
            /* Out of realm */
            if ((flags & KRB5_KDB_FLAG_CLIENT) &&
                (flags & KRB5_KDB_FLAG_REFERRAL_OK)) {
                /* Return a client referral by creating an entry with only the
                 * principal set. */
                *entry = ealloc(sizeof(**entry));
                (*entry)->princ = princ;
                princ = NULL;
                ret = 0;
                goto cleanup;
            } else if (flags & KRB5_KDB_FLAG_REFERRAL_OK) {
                /* Generate a server referral by looking up the TGT for the
                 * canonical name's realm. */
                tgtprinc = tgtname(context, &princ->realm, &search_for->realm);
                krb5_free_principal(context, princ);
                princ = tgtprinc;

                krb5_free_unparsed_name(context, search_name);
                check(krb5_unparse_name_flags(context, princ,
                                              KRB5_PRINCIPAL_UNPARSE_NO_REALM,
                                              &search_name));
                ename = search_name;
            } else {
                ret = KRB5_KDB_NOENTRY;
                goto cleanup;
            }
        } else {
            ename = canon;
        }
    } else {
        check(krb5_copy_principal(context, search_for, &princ));
        ename = search_name;
    }

    /* Check that the entry exists. */
    set_names(h, "princs", ename, NULL);
    ret = profile_get_relation_names(h->profile, h->names, &names);
    if (ret == PROF_NO_RELATION) {
        ret = KRB5_KDB_NOENTRY;
        goto cleanup;
    }
    profile_free_list(names);

    /* No error exits after this point. */

    ent = ealloc(sizeof(*ent));
    ent->princ = princ;
    princ = NULL;

    flagstr = get_string(h, "princs", ename, "flags");
    if (flagstr != NULL) {
        check(krb5_flagspec_to_mask(flagstr, &ent->attributes,
                                    &ent->attributes));
    }
    free(flagstr);

    ent->max_life = get_duration(h, "princs", ename, "maxlife");
    ent->max_renewable_life = get_duration(h, "princs", ename, "maxrenewlife");
    ent->expiration = get_time(h, "princs", ename, "expiration");
    ent->pw_expiration = get_time(h, "princs", ename, "pwexpiration");

    /* Leave last_success, last_failed, fail_auth_count zeroed. */
    /* Leave e_data empty. */

    set_names(h, "princs", ename, "keys");
    ret = profile_get_values(h->profile, h->names, &key_strings);
    if (ret != PROF_NO_RELATION) {
        make_keys(key_strings, ename, &search_for->realm, ent);
        profile_free_list(key_strings);
    }

    set_names(h, "princs", ename, "strings");
    ret = profile_get_values(h->profile, h->names, &stringattrs);
    if (ret != PROF_NO_RELATION) {
        make_strings(stringattrs, ent);
        profile_free_list(stringattrs);
    }

    /* We must include mod-princ data or kadm5_get_principal() won't work and
     * we can't extract keys with kadmin.local. */
    check(krb5_dbe_update_mod_princ_data(context, ent, 0, &empty_princ));

    *entry = ent;
    ret = 0;

cleanup:
    krb5_free_unparsed_name(context, search_name);
    krb5_free_principal(context, princ);
    free(canon);
    return ret;
}

static void
lookup_princ_by_cert(krb5_context context, const krb5_data *client_cert,
                     krb5_principal *princ)
{
    krb5_error_code ret;
    char *cert_princ_name;

    /* The test client sends a principal string instead of a cert. */
    cert_princ_name = k5memdup0(client_cert->data, client_cert->length, &ret);
    check(ret);

    check(krb5_parse_name_flags(context, cert_princ_name,
                                KRB5_PRINCIPAL_PARSE_ENTERPRISE, princ));
    free(cert_princ_name);
}

static krb5_error_code
test_get_s4u_x509_principal(krb5_context context, const krb5_data *client_cert,
                            krb5_const_principal princ, unsigned int flags,
                            krb5_db_entry **entry)
{
    krb5_error_code ret;
    krb5_principal cert_princ, canon_princ;
    testhandle h = context->dal_handle->db_context;
    krb5_boolean match;
    char *canon, *princ_name;

    lookup_princ_by_cert(context, client_cert, &cert_princ);

    ret = test_get_principal(context, cert_princ, flags, entry);
    krb5_free_principal(context, cert_princ);
    if (ret || (flags & KRB5_KDB_FLAG_REFERRAL_OK))
        return ret;

    if (!krb5_realm_compare(context, princ, (*entry)->princ))
        abort();

    if (princ->length == 0 ||
        krb5_principal_compare(context, princ, (*entry)->princ))
        return 0;

    match = FALSE;
    check(krb5_unparse_name_flags(context, princ,
                                  KRB5_PRINCIPAL_UNPARSE_NO_REALM,
                                  &princ_name));
    canon = get_string(h, "alias", princ_name, NULL);
    krb5_free_unparsed_name(context, princ_name);
    if (canon != NULL) {
        check(krb5_parse_name(context, canon, &canon_princ));
        match = krb5_principal_compare(context, canon_princ, (*entry)->princ);
        krb5_free_principal(context, canon_princ);
    }

    free(canon);
    return match ? 0 : KRB5KDC_ERR_CLIENT_NAME_MISMATCH;
}

static krb5_error_code
test_fetch_master_key(krb5_context context, krb5_principal mname,
                      krb5_keyblock *key_out, krb5_kvno *kvno_out,
                      char *db_args)
{
    memset(key_out, 0, sizeof(*key_out));
    *kvno_out = 0;
    return 0;
}

static krb5_error_code
test_fetch_master_key_list(krb5_context context, krb5_principal mname,
                           const krb5_keyblock *key,
                           krb5_keylist_node **mkeys_out)
{
    /* krb5_dbe_get_mkvno() returns an error if we produce NULL, so return an
     * empty node to make kadm5_get_principal() work. */
    *mkeys_out = ealloc(sizeof(**mkeys_out));
    return 0;
}

static krb5_error_code
test_decrypt_key_data(krb5_context context, const krb5_keyblock *mkey,
                      const krb5_key_data *kd, krb5_keyblock *key_out,
                      krb5_keysalt *salt_out)
{
    key_out->magic = KV5M_KEYBLOCK;
    key_out->enctype = kd->key_data_type[0];
    key_out->length = kd->key_data_length[0];
    key_out->contents = ealloc(key_out->length);
    memcpy(key_out->contents, kd->key_data_contents[0], key_out->length);
    if (salt_out != NULL) {
        salt_out->type = (kd->key_data_ver > 1) ? kd->key_data_type[1] :
            KRB5_KDB_SALTTYPE_NORMAL;
        salt_out->data = empty_data();
    }
    return 0;
}

static krb5_error_code
test_encrypt_key_data(krb5_context context, const krb5_keyblock *mkey,
                      const krb5_keyblock *key, const krb5_keysalt *salt,
                      int kvno, krb5_key_data *kd_out)
{
    memset(kd_out, 0, sizeof(*kd_out));
    kd_out->key_data_ver = 2;
    kd_out->key_data_kvno = kvno;
    kd_out->key_data_type[0] = key->enctype;
    kd_out->key_data_length[0] = key->length;
    kd_out->key_data_contents[0] = ealloc(key->length);
    memcpy(kd_out->key_data_contents[0], key->contents, key->length);
    kd_out->key_data_type[1] = (salt != NULL) ? salt->type :
        KRB5_KDB_SALTTYPE_NORMAL;
    return 0;
}

static void
change_auth_indicators(krb5_context context, krb5_data ***auth_indicators)
{
    krb5_data **inds, d;
    int i, val;

    /* If we see an auth indicator "dbincrX", replace the whole indicator list
     * with "dbincr{X+1}". */
    inds = *auth_indicators;
    for (i = 0; inds != NULL && inds[i] != NULL; i++) {
        if (inds[i]->length == 7 && memcmp(inds[i]->data, "dbincr", 6) == 0) {
            val = inds[i]->data[6];
            k5_free_data_ptr_list(inds);
            inds = ealloc(2 * sizeof(*inds));
            d = string2data("dbincr0");
            check(krb5_copy_data(context, &d, &inds[0]));
            inds[0]->data[6] = val + 1;
            inds[1] = NULL;
            *auth_indicators = inds;
            break;
        }
    }
}

static krb5_error_code
test_issue_pac(krb5_context context, unsigned int flags, krb5_db_entry *client,
               krb5_keyblock *replaced_reply_key, krb5_db_entry *server,
               krb5_db_entry *krb5tgt, krb5_timestamp authtime,
               krb5_pac old_pac, krb5_pac new_pac,
               krb5_data ***auth_indicators)
{
    krb5_data data = empty_data();
    krb5_boolean found_logon_info = FALSE;
    krb5_ui_4 *types;
    size_t num_buffers = 0, i;

    change_auth_indicators(context, auth_indicators);

    if (old_pac == NULL ||
        (client != NULL && (flags & KRB5_KDB_FLAG_PROTOCOL_TRANSITION))) {
        /* Generating an initial PAC. */
        assert(client != NULL);
        data = string2data("fake");
        check(krb5_pac_add_buffer(context, new_pac, KRB5_PAC_LOGON_INFO,
                                  &data));

        if (replaced_reply_key != NULL) {
            /* Add a fake PAC_CREDENTIALS_INFO buffer so we can test whether
             * this parameter was set. */
            data = string2data("fake credinfo");
            check(krb5_pac_add_buffer(context, new_pac,
                                      KRB5_PAC_CREDENTIALS_INFO, &data));
        }
        return 0;
    } else {
        /* Field copying - my favorite! */
        if (old_pac != NULL)
            check(krb5_pac_get_types(context, old_pac, &num_buffers, &types));

        for (i = 0; i < num_buffers; i++) {
            /* Skip buffer types handled by KDC. */
            if (types[i] == KRB5_PAC_SERVER_CHECKSUM ||
                types[i] == KRB5_PAC_PRIVSVR_CHECKSUM ||
                types[i] == KRB5_PAC_TICKET_CHECKSUM ||
                types[i] == KRB5_PAC_CLIENT_INFO ||
                types[i] == KRB5_PAC_DELEGATION_INFO)
                continue;

            check(krb5_pac_get_buffer(context, old_pac, types[i], &data));

            if (types[i] == KRB5_PAC_LOGON_INFO) {
                found_logon_info = TRUE;
                assert(data_eq_string(data, "fake"));
            }

            check(krb5_pac_add_buffer(context, new_pac, types[i], &data));
            krb5_free_data_contents(context, &data);
        }

        if (old_pac != NULL)
            assert(found_logon_info);
    }

    return 0;
}

static krb5_boolean
match_in_table(krb5_context context, const char *table, const char *sprinc,
               const char *tprinc)
{
    testhandle h = context->dal_handle->db_context;
    krb5_error_code ret;
    char **values, **v;
    krb5_boolean found = FALSE;

    set_names(h, table, sprinc, NULL);
    ret = profile_get_values(h->profile, h->names, &values);
    assert(ret == 0 || ret == PROF_NO_RELATION);
    if (ret)
        return FALSE;
    for (v = values; *v != NULL; v++) {
        if (tprinc == NULL || strcmp(*v, tprinc) == 0) {
            found = TRUE;
            break;
        }
    }
    profile_free_list(values);
    return found;
}

static krb5_error_code
test_check_allowed_to_delegate(krb5_context context,
                               krb5_const_principal client,
                               const krb5_db_entry *server,
                               krb5_const_principal proxy)
{
    char *sprinc, *tprinc = NULL;
    krb5_boolean found = FALSE;

    check(krb5_unparse_name_flags(context, server->princ,
                                  KRB5_PRINCIPAL_UNPARSE_NO_REALM, &sprinc));
    if (proxy != NULL) {
        check(krb5_unparse_name_flags(context, proxy,
                                      KRB5_PRINCIPAL_UNPARSE_NO_REALM,
                                      &tprinc));
    }
    found = match_in_table(context, "delegation", sprinc, tprinc);
    krb5_free_unparsed_name(context, sprinc);
    krb5_free_unparsed_name(context, tprinc);
    return found ? 0 : KRB5KDC_ERR_BADOPTION;
}

static krb5_error_code
test_allowed_to_delegate_from(krb5_context context,
                              krb5_const_principal client,
                              krb5_const_principal server,
                              krb5_pac server_pac,
                              const krb5_db_entry *proxy)
{
    char *proxy_princ, *server_princ, *pac_client_princ, *client_princ;
    krb5_boolean found = FALSE;

    assert(server_pac != NULL);

    check(krb5_unparse_name(context, proxy->princ, &proxy_princ));
    check(krb5_unparse_name(context, server, &server_princ));
    check(krb5_unparse_name(context, client, &client_princ));

    check(krb5_pac_get_client_info(context, server_pac, NULL,
                                   &pac_client_princ));

    /* Skip realm portion if not present in PAC. */
    assert(strncmp(pac_client_princ, server_princ,
                   strlen(pac_client_princ)) == 0);

    free(pac_client_princ);

    found = match_in_table(context, "rbcd", proxy_princ, server_princ);
    krb5_free_unparsed_name(context, proxy_princ);
    krb5_free_unparsed_name(context, server_princ);
    return found ? 0 : KRB5KDC_ERR_BADOPTION;
}

kdb_vftabl PLUGIN_SYMBOL_NAME(krb5_test, kdb_function_table) = {
    KRB5_KDB_DAL_MAJOR_VERSION,             /* major version number */
    0,                                      /* minor version number */
    test_init,
    test_cleanup,
    test_open,
    test_close,
    NULL, /* create */
    NULL, /* destroy */
    NULL, /* get_age */
    NULL, /* lock */
    NULL, /* unlock */
    test_get_principal,
    NULL, /* put_principal */
    NULL, /* delete_principal */
    NULL, /* rename_principal */
    NULL, /* iterate */
    NULL, /* create_policy */
    NULL, /* get_policy */
    NULL, /* put_policy */
    NULL, /* iter_policy */
    NULL, /* delete_policy */
    test_fetch_master_key,
    test_fetch_master_key_list,
    NULL, /* store_master_key_list */
    NULL, /* dbe_search_enctype */
    NULL, /* change_pwd */
    NULL, /* promote_db */
    test_decrypt_key_data,
    test_encrypt_key_data,
    NULL, /* check_transited_realms */
    NULL, /* check_policy_as */
    NULL, /* check_policy_tgs */
    NULL, /* audit_as_req */
    NULL, /* refresh_config */
    test_check_allowed_to_delegate,
    NULL, /* free_principal_e_data */
    test_get_s4u_x509_principal,
    test_allowed_to_delegate_from,
    test_issue_pac,
};
