root/crypto/krb5/src/lib/kadm5/t_kadm5.c
/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* lib/kadm5/t_kadm5.c - API tests for libkadm5 */
/*
 * Copyright (C) 2021 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.
 */

#include "k5-int.h"
#include <kadm5/admin.h>

static uint32_t api;
static krb5_boolean rpc;

static krb5_context context;

/* These must match the creation commands in t_kadm5.py. */
#define ADMIN_PASSWORD "admin"
#define USER_PASSWORD "us3r"

/* This list must match the supported_enctypes setting in t_kadm5.py. */
static krb5_enctype
default_supported_enctypes[] = {
    ENCTYPE_AES256_CTS_HMAC_SHA1_96, ENCTYPE_AES128_CTS_HMAC_SHA1_96,
    ENCTYPE_NULL
};

static void
check(krb5_error_code code)
{
    assert(code == 0);
}

static void
check_fail(krb5_error_code code, krb5_error_code expected)
{
    assert(code == expected);
}

/*
 * Initialize a handle using the global context.  The caller must destroy this
 * handle before initializing another one.  If the client name begins with '$',
 * authenticate to kadmin/changepw; otherwise authenticate to kadmin/admin.  If
 * client is null, return a null handle.
 */
static void *
get_handle(char *client)
{
    void *handle;
    char *service, *pass;

    if (client == NULL)
        return NULL;

    if (*client == '$') {
        service = KADM5_CHANGEPW_SERVICE;
        client++;
    } else {
        service = KADM5_ADMIN_SERVICE;
    }
    pass = (strcmp(client, "user") == 0) ? USER_PASSWORD : ADMIN_PASSWORD;

    check(kadm5_init(context, client, pass, service, NULL,
                     KADM5_STRUCT_VERSION, api, NULL, &handle));
    return handle;
}

static void
free_handle(void *handle)
{
    if (handle != NULL)
        check(kadm5_destroy(handle));
}

static krb5_principal
parse_princ(const char *str)
{
    krb5_principal princ;

    check(krb5_parse_name(context, str, &princ));
    return princ;
}

static void
create_simple_policy(char *name)
{
    void *handle = get_handle("admin");
    kadm5_policy_ent_rec ent;

    memset(&ent, 0, sizeof(ent));
    ent.policy = name;
    check(kadm5_create_policy(handle, &ent, KADM5_POLICY));
    free_handle(handle);
}

static void
delete_policy(char *name)
{
    void *handle = get_handle("admin");

    check(kadm5_delete_policy(handle, name));
    free_handle(handle);
}

static void
compare_policy(kadm5_policy_ent_t x, uint32_t mask)
{
    kadm5_policy_ent_rec g;
    void *handle = get_handle("admin");

    check(kadm5_get_policy(handle, x->policy, &g));

    assert(strcmp(g.policy, x->policy) == 0);
    if (mask & KADM5_PW_MAX_LIFE)
        assert(g.pw_max_life == x->pw_max_life);
    if (mask & KADM5_PW_MIN_LIFE)
        assert(g.pw_min_life == x->pw_min_life);
    if (mask & KADM5_PW_MIN_LENGTH)
        assert(g.pw_min_length == x->pw_min_length);
    if (mask & KADM5_PW_MIN_CLASSES)
        assert(g.pw_min_classes == x->pw_min_classes);
    if (mask & KADM5_PW_HISTORY_NUM)
        assert(g.pw_history_num == x->pw_history_num);
    if (mask & KADM5_PW_MAX_FAILURE)
        assert(g.pw_max_fail == x->pw_max_fail);
    if (mask & KADM5_PW_FAILURE_COUNT_INTERVAL)
        assert(g.pw_failcnt_interval == x->pw_failcnt_interval);
    if (mask & KADM5_PW_LOCKOUT_DURATION)
        assert(g.pw_lockout_duration == x->pw_lockout_duration);

    check(kadm5_free_policy_ent(handle, &g));
    free_handle(handle);
}

static void
create_simple_princ(krb5_principal princ, char *policy)
{
    void *handle = get_handle("admin");
    kadm5_principal_ent_rec ent;
    uint32_t mask = KADM5_PRINCIPAL;

    memset(&ent, 0, sizeof(ent));
    ent.principal = princ;
    ent.policy = policy;
    if (policy != NULL)
        mask |= KADM5_POLICY;
    check(kadm5_create_principal(handle, &ent, mask, "pw"));
    free_handle(handle);
}

static void
delete_princ(krb5_principal princ)
{
    void *handle = get_handle("admin");

    check(kadm5_delete_principal(handle, princ));
    free_handle(handle);
}

static void
compare_key_data(kadm5_principal_ent_t ent, const krb5_enctype *etypes)
{
    int i;

    for (i = 0; etypes[i] != ENCTYPE_NULL; i++) {
        assert(i < ent->n_key_data);
        assert(ent->key_data[i].key_data_ver >= 1);
        assert(ent->key_data[i].key_data_type[0] == etypes[i]);
    }
}

static void
compare_princ(kadm5_principal_ent_t x, uint32_t mask)
{
    void *handle = get_handle("admin");
    kadm5_principal_ent_rec g;
    kadm5_policy_ent_rec pol;

    check(kadm5_get_principal(handle, x->principal, &g,
                              KADM5_PRINCIPAL_NORMAL_MASK));

    assert(krb5_principal_compare(context, g.principal, x->principal));
    if (mask & KADM5_POLICY)
        assert(strcmp(g.policy, x->policy) == 0);
    if (mask & KADM5_PRINC_EXPIRE_TIME)
        assert(g.princ_expire_time == x->princ_expire_time);
    if (mask & KADM5_MAX_LIFE)
        assert(g.max_life == x->max_life);
    if (mask & KADM5_MAX_RLIFE)
        assert(g.max_renewable_life == x->max_renewable_life);
    if (mask & KADM5_FAIL_AUTH_COUNT)
        assert(g.fail_auth_count == x->fail_auth_count);
    if (mask & KADM5_ATTRIBUTES)
        assert(g.attributes == x->attributes);
    if (mask & KADM5_KVNO)
        assert(g.kvno == x->kvno);

    if (mask & KADM5_PW_EXPIRATION) {
        assert(g.pw_expiration == x->pw_expiration);
    } else if ((mask & KADM5_POLICY) &&
               kadm5_get_policy(handle, g.policy, &pol) == 0) {
        /* Check the policy pw_max_life computation. */
        if (pol.pw_max_life != 0) {
            assert(ts_incr(g.last_pwd_change, pol.pw_max_life) ==
                   g.pw_expiration);
        } else {
            assert(g.pw_expiration == 0);
        }
        check(kadm5_free_policy_ent(handle, &pol));
    }

    if (mask & KADM5_POLICY_CLR) {
        assert(g.policy == NULL);
        if (!(mask & KADM5_PW_EXPIRATION))
            assert(g.pw_expiration == 0);
    }

    check(kadm5_free_principal_ent(handle, &g));
    free_handle(handle);
}

static void
kinit(krb5_ccache cc, const char *user, const char *pass, const char *service)
{
    krb5_get_init_creds_opt *opt;
    krb5_principal client = parse_princ(user);
    krb5_creds creds;

    check(krb5_get_init_creds_opt_alloc(context, &opt));
    check(krb5_get_init_creds_opt_set_out_ccache(context, opt, cc));
    check(krb5_get_init_creds_password(context, &creds, client, pass, NULL,
                                       NULL, 0, service, opt));
    krb5_get_init_creds_opt_free(context, opt);
    krb5_free_cred_contents(context, &creds);
    krb5_free_principal(context, client);
}

static void
cpw_test_fail(char *user, krb5_principal princ, char *pass,
              krb5_error_code code)
{
    void *handle = get_handle(user);

    check_fail(kadm5_chpass_principal(handle, princ, pass), code);
    free_handle(handle);
}

static void
cpw_test_succeed(char *user, krb5_principal princ, char *pass)
{
    cpw_test_fail(user, princ, pass, 0);
}

static void
test_chpass(void)
{
    krb5_principal princ = parse_princ("chpass-test");
    krb5_principal hist_princ = parse_princ("kadmin/history");
    kadm5_principal_ent_rec ent;
    void *handle;

    /* Specify a policy so that kadmin/history is created. */
    create_simple_princ(princ, "minlife-pol");

    /* Check kvno and enctypes after a password change. */
    handle = get_handle("admin");
    check(kadm5_chpass_principal(handle, princ, "newpassword"));
    check(kadm5_get_principal(handle, princ, &ent, KADM5_KEY_DATA));
    compare_key_data(&ent, default_supported_enctypes);
    assert(ent.key_data[0].key_data_kvno == 2);
    check(kadm5_free_principal_ent(handle, &ent));
    free_handle(handle);

    /* Fails for protected principal. */
    cpw_test_fail("admin", hist_princ, "pw", KADM5_PROTECT_PRINCIPAL);

    /* Fails over RPC if "change" ACL is not granted, or if we authenticated to
     * kadmin/changepw and are changing another principal's password. */
    if (rpc) {
        cpw_test_succeed("admin/modify", princ, "pw2");
        cpw_test_fail("admin/none", princ, "pw3", KADM5_AUTH_CHANGEPW);
        cpw_test_fail("$admin", princ, "pw3", KADM5_AUTH_CHANGEPW);
    }

    /* Fails with null handle or principal name. */
    cpw_test_fail(NULL, princ, "pw", KADM5_BAD_SERVER_HANDLE);
    cpw_test_fail("admin", NULL, "pw", EINVAL);

    delete_princ(princ);
    krb5_free_principal(context, princ);
    krb5_free_principal(context, hist_princ);
}

static void
cpol_test_fail(char *user, kadm5_policy_ent_t ent, uint32_t mask,
               krb5_error_code code)
{
    void *handle = get_handle(user);

    check_fail(kadm5_create_policy(handle, ent, mask | KADM5_POLICY), code);
    free_handle(handle);
}

static void
cpol_test_compare(char *user, kadm5_policy_ent_t ent, uint32_t mask)
{
    cpol_test_fail(user, ent, mask, 0);
    compare_policy(ent, mask);
    delete_policy(ent->policy);
}

static void
test_create_policy(void)
{
    void *handle;
    kadm5_policy_ent_rec ent;

    memset(&ent, 0, sizeof(ent));

    /* Fails with undefined mask bit. */
    ent.policy = "create-policy-test";
    cpol_test_fail("admin", &ent, 0x10000000, KADM5_BAD_MASK);

    /* Fails without KADM5_POLICY mask bit. */
    handle = get_handle("admin");
    check_fail(kadm5_create_policy(handle, &ent, 0), KADM5_BAD_MASK);
    free_handle(handle);

    /* pw_min_life = 0 and pw_min_life != 0 */
    cpol_test_compare("admin", &ent, KADM5_PW_MIN_LIFE);
    ent.pw_min_life = 32;
    cpol_test_compare("admin", &ent, KADM5_PW_MIN_LIFE);

    /* pw_max_life = 0 and pw_max_life != 0 */
    cpol_test_compare("admin", &ent, KADM5_PW_MAX_LIFE);
    ent.pw_max_life = 32;
    cpol_test_compare("admin", &ent, KADM5_PW_MAX_LIFE);

    /* pw_min_length = 0 (rejected) and pw_min_length != 0 */
    cpol_test_fail("admin", &ent, KADM5_PW_MIN_LENGTH, KADM5_BAD_LENGTH);
    ent.pw_min_length = 32;
    cpol_test_compare("admin", &ent, KADM5_PW_MIN_LENGTH);

    /* pw_min_classes = 0 (rejected), 1, 5, 6 (rejected) */
    cpol_test_fail("admin", &ent, KADM5_PW_MIN_CLASSES, KADM5_BAD_CLASS);
    ent.pw_min_classes = 1;
    cpol_test_compare("admin", &ent, KADM5_PW_MIN_CLASSES);
    ent.pw_min_classes = 5;
    cpol_test_compare("admin", &ent, KADM5_PW_MIN_CLASSES);
    ent.pw_min_classes = 6;
    cpol_test_fail("admin", &ent, KADM5_PW_MIN_CLASSES, KADM5_BAD_CLASS);

    /* pw_history_num = 0 (rejected), 1, 10 */
    cpol_test_fail("admin", &ent, KADM5_PW_HISTORY_NUM, KADM5_BAD_HISTORY);
    ent.pw_history_num = 1;
    cpol_test_compare("admin", &ent, KADM5_PW_HISTORY_NUM);
    ent.pw_history_num = 10;
    cpol_test_compare("admin", &ent, KADM5_PW_HISTORY_NUM);

    if (api >= KADM5_API_VERSION_3) {
        ent.pw_max_fail = 2;
        cpol_test_compare("admin", &ent, KADM5_PW_MAX_FAILURE);
        ent.pw_failcnt_interval = 90;
        cpol_test_compare("admin", &ent,
                          KADM5_PW_FAILURE_COUNT_INTERVAL);
        ent.pw_lockout_duration = 180;
        cpol_test_compare("admin", &ent, KADM5_PW_LOCKOUT_DURATION);
    }

    /* Fails over RPC if "add" ACL is not granted, or if we authenticated to
     * kadmin/changepw. */
    if (rpc) {
        cpol_test_fail("$admin", &ent, 0, KADM5_AUTH_ADD);
        cpol_test_fail("admin/none", &ent, 0, KADM5_AUTH_ADD);
        cpol_test_fail("admin/get", &ent, 0, KADM5_AUTH_ADD);
        cpol_test_fail("admin/modify", &ent, 0, KADM5_AUTH_ADD);
        cpol_test_fail("admin/delete", &ent, 0, KADM5_AUTH_ADD);
        cpol_test_compare("admin/add", &ent, 0);
    }

    /* Fails with existing policy name. */
    ent.policy = "test-pol";
    cpol_test_fail("admin", &ent, 0, KADM5_DUP);

    /* Fails with null or empty policy name, or invalid character in name. */
    ent.policy = NULL;
    cpol_test_fail("admin", &ent, 0, EINVAL);
    ent.policy = "";
    cpol_test_fail("admin", &ent, 0, KADM5_BAD_POLICY);
    ent.policy = "pol\7";
    cpol_test_fail("admin", &ent, 0, KADM5_BAD_POLICY);

    /* Fails with null handle or policy ent. */
    cpol_test_fail(NULL, &ent, 0, KADM5_BAD_SERVER_HANDLE);
    cpol_test_fail("admin", NULL, 0, EINVAL);
}

static void
cprinc_test_fail(char *user, kadm5_principal_ent_t ent, uint32_t mask,
                 char *pass, krb5_error_code code)
{
    void *handle = get_handle(user);

    check_fail(kadm5_create_principal(handle, ent, mask | KADM5_PRINCIPAL,
                                      pass), code);
    free_handle(handle);
}

static void
cprinc_test_compare(char *user, kadm5_principal_ent_t ent, uint32_t mask,
                    char *pass)
{
    cprinc_test_fail(user, ent, mask, pass, 0);
    compare_princ(ent, mask);
    delete_princ(ent->principal);
}

static void
test_create_principal(void)
{
    void *handle;
    kadm5_principal_ent_rec ent;
    krb5_principal princ = parse_princ("create-principal-test");
    krb5_principal user_princ = parse_princ("user");

    memset(&ent, 0, sizeof(ent));
    ent.principal = princ;

    /* Fails with undefined or prohibited mask bit. */
    cprinc_test_fail("admin", &ent, 0x100000, "", KADM5_BAD_MASK);
    cprinc_test_fail("admin", &ent, KADM5_LAST_PWD_CHANGE, "pw",
                     KADM5_BAD_MASK);
    cprinc_test_fail("admin", &ent, KADM5_MOD_TIME, "pw", KADM5_BAD_MASK);
    cprinc_test_fail("admin", &ent, KADM5_MOD_NAME, "pw", KADM5_BAD_MASK);
    cprinc_test_fail("admin", &ent, KADM5_MKVNO, "pw", KADM5_BAD_MASK);
    cprinc_test_fail("admin", &ent, KADM5_AUX_ATTRIBUTES, "pw",
                     KADM5_BAD_MASK);

    /* Fails without KADM5_PRINCIPAL mask bit. */
    handle = get_handle("admin");
    check_fail(kadm5_create_principal(handle, &ent, 0, "pw"), KADM5_BAD_MASK);
    free_handle(handle);

    /* Fails with empty password or password prohibited by policy. */
    cprinc_test_fail("admin", &ent, 0, "", KADM5_PASS_Q_TOOSHORT);
    ent.policy = "test-pol";
    cprinc_test_fail("admin", &ent, KADM5_POLICY, "tP", KADM5_PASS_Q_TOOSHORT);
    cprinc_test_fail("admin", &ent, KADM5_POLICY, "testpassword",
                     KADM5_PASS_Q_CLASS);
    cprinc_test_fail("admin", &ent, KADM5_POLICY, "Abyssinia",
                     KADM5_PASS_Q_DICT);

    cprinc_test_compare("admin", &ent, 0, "pw");
    ent.policy = "nonexistent-pol";
    cprinc_test_compare("admin", &ent, KADM5_POLICY, "pw");
    cprinc_test_compare("admin/rename", &ent, KADM5_POLICY, "pw");

    /* Test pw_expiration explicit specifications vs. policy pw_max_life. */
    ent.policy = "test-pol";
    cprinc_test_compare("admin", &ent, KADM5_POLICY, "NotinTheDictionary");
    cprinc_test_compare("admin", &ent, KADM5_PRINC_EXPIRE_TIME, "pw");
    cprinc_test_compare("admin", &ent, KADM5_PW_EXPIRATION, "pw");
    cprinc_test_compare("admin", &ent, KADM5_POLICY | KADM5_PW_EXPIRATION,
                        "NotinTheDictionary");
    ent.pw_expiration = 1234;
    cprinc_test_compare("admin", &ent, KADM5_PW_EXPIRATION, "pw");
    cprinc_test_compare("admin", &ent, KADM5_POLICY | KADM5_PW_EXPIRATION,
                        "NotinTheDictionary");
    ent.pw_expiration = 999999999;
    cprinc_test_compare("admin", &ent, KADM5_POLICY | KADM5_PW_EXPIRATION,
                        "NotinTheDictionary");
    ent.policy = "dict-only-pol";
    cprinc_test_compare("admin", &ent, KADM5_POLICY | KADM5_PW_EXPIRATION,
                        "pw");

    /* Fails over RPC if "add" ACL is not granted, or if we authenticated to
     * kadmin/changepw. */
    if (rpc) {
        cprinc_test_fail("$admin", &ent, 0, "pw", KADM5_AUTH_ADD);
        cprinc_test_fail("admin/none", &ent, 0, "pw", KADM5_AUTH_ADD);
        cprinc_test_fail("admin/get", &ent, 0, "pw", KADM5_AUTH_ADD);
        cprinc_test_fail("admin/modify", &ent, 0, "pw", KADM5_AUTH_ADD);
        cprinc_test_fail("admin/delete", &ent, 0, "pw", KADM5_AUTH_ADD);
    }

    /* Fails with existing policy name. */
    ent.principal = user_princ;
    cprinc_test_fail("admin", &ent, 0, "pw", KADM5_DUP);

    /* Fails with null handle or principal ent. */
    cprinc_test_fail(NULL, &ent, 0, "pw", KADM5_BAD_SERVER_HANDLE);
    cprinc_test_fail("admin", NULL, 0, "pw", EINVAL);

    krb5_free_principal(context, princ);
    krb5_free_principal(context, user_princ);
}

static void
dpol_test_fail(char *user, char *name, krb5_error_code code)
{
    void *handle = get_handle(user);

    check_fail(kadm5_delete_policy(handle, name), code);
    free_handle(handle);
}

static void
dpol_test_succeed(char *user, char *name)
{
    dpol_test_fail(user, name, 0);
}

static void
test_delete_policy(void)
{
    krb5_principal princ = parse_princ("delete-policy-test-princ");

    /* Fails with unknown policy. */
    dpol_test_fail("admin", "delete-policy-test", KADM5_UNK_POLICY);

    /* Fails with empty policy name. */
    dpol_test_fail("admin", "", KADM5_BAD_POLICY);

    /* Succeeds with "delete" ACL (or local authentication). */
    create_simple_policy("delete-policy-test");
    dpol_test_succeed("admin/delete", "delete-policy-test");

    /* Succeeds even if a principal references the policy, since we now allow
     * principals to reference nonexistent policies. */
    create_simple_policy("delete-policy-test");
    create_simple_princ(princ, "delete-policy-test");
    dpol_test_succeed("admin", "delete-policy-test");
    delete_princ(princ);

    /* Fails over RPC if "delete" ACL is not granted, or if we authenticated to
     * kadmin/changepw. */
    if (rpc) {
        dpol_test_fail("$admin", "test-pol", KADM5_AUTH_DELETE);
        dpol_test_fail("admin/none", "test-pol", KADM5_AUTH_DELETE);
        dpol_test_fail("admin/add", "test-pol", KADM5_AUTH_DELETE);
    }

    /* Fails with null handle or principal ent. */
    dpol_test_fail(NULL, "test-pol", KADM5_BAD_SERVER_HANDLE);
    dpol_test_fail("admin", NULL, EINVAL);

    krb5_free_principal(context, princ);
}

static void
dprinc_test_fail(char *user, krb5_principal princ, krb5_error_code code)
{
    void *handle = get_handle(user);

    check_fail(kadm5_delete_principal(handle, princ), code);
    free_handle(handle);
}

static void
dprinc_test_succeed(char *user, krb5_principal princ)
{
    dprinc_test_fail(user, princ, 0);
}

static void
test_delete_principal(void)
{
    krb5_principal princ = parse_princ("delete-principal-test");

    /* Fails with unknown principal. */
    dprinc_test_fail("admin", princ, KADM5_UNK_PRINC);

    /* Succeeds with "delete" ACL (or local authentication). */
    create_simple_princ(princ, NULL);
    dprinc_test_succeed("admin/delete", princ);

    /* Fails over RPC if "delete" ACL is not granted, or if we authenticated to
     * kadmin/changepw. */
    if (rpc) {
        dprinc_test_fail("$admin", princ, KADM5_AUTH_DELETE);
        dprinc_test_fail("admin/add", princ, KADM5_AUTH_DELETE);
        dprinc_test_fail("admin/modify", princ, KADM5_AUTH_DELETE);
        dprinc_test_fail("admin/get", princ, KADM5_AUTH_DELETE);
        dprinc_test_fail("admin/none", princ, KADM5_AUTH_DELETE);
    }

    /* Fails with null handle or principal ent. */
    dprinc_test_fail(NULL, princ, KADM5_BAD_SERVER_HANDLE);
    dprinc_test_fail("admin", NULL, EINVAL);

    krb5_free_principal(context, princ);
}

static void
gpol_test_succeed(char *user, char *name)
{
    void *handle = get_handle(user);
    kadm5_policy_ent_rec ent;

    check(kadm5_get_policy(handle, name, &ent));
    assert(strcmp(ent.policy, name) == 0);
    check(kadm5_free_policy_ent(handle, &ent));
    free_handle(handle);
}

static void
gpol_test_fail(char *user, char *name, krb5_error_code code)
{
    void *handle = get_handle(user);
    kadm5_policy_ent_rec ent;

    check_fail(kadm5_get_policy(handle, name, &ent), code);
    free_handle(handle);
}

static void
test_get_policy(void)
{
    /* Fails with unknown policy. */
    dpol_test_fail("admin", "unknown-policy", KADM5_UNK_POLICY);

    /* Fails with empty or null policy name or a null handle. */
    gpol_test_fail("admin", "", KADM5_BAD_POLICY);
    gpol_test_fail("admin", NULL, EINVAL);
    gpol_test_fail(NULL, "", KADM5_BAD_SERVER_HANDLE);

    /* Fails over RPC unless "get" ACL is granted or the principal's own policy
     * is retrieved. */
    if (rpc) {
        gpol_test_fail("admin/none", "test-pol", KADM5_AUTH_GET);
        gpol_test_fail("admin/add", "test-pol", KADM5_AUTH_GET);
        gpol_test_succeed("admin/get", "test-pol");
        gpol_test_succeed("user", "minlife-pol");
        gpol_test_succeed("$user", "minlife-pol");
    }
}

static void
gprinc_test_succeed(char *user, krb5_principal princ)
{
    void *handle = get_handle(user);
    kadm5_principal_ent_rec ent;

    check(kadm5_get_principal(handle, princ, &ent,
                              KADM5_PRINCIPAL_NORMAL_MASK));
    assert(krb5_principal_compare(context, ent.principal, princ));
    check(kadm5_free_principal_ent(handle, &ent));
    free_handle(handle);
}

static void
gprinc_test_fail(char *user, krb5_principal princ, krb5_error_code code)
{
    void *handle = get_handle(user);
    kadm5_principal_ent_rec ent;

    check_fail(kadm5_get_principal(handle, princ, &ent,
                                   KADM5_PRINCIPAL_NORMAL_MASK), code);
    free_handle(handle);
}

static void
test_get_principal(void)
{
    void *handle;
    kadm5_principal_ent_rec ent;
    krb5_principal princ = parse_princ("get-principal-test");
    krb5_principal admin_princ = parse_princ("admin");
    krb5_principal admin_none_princ = parse_princ("admin/none");
    int i;

    /* Fails with unknown principal. */
    gprinc_test_fail("admin", princ, KADM5_UNK_PRINC);

    create_simple_princ(princ, NULL);

    /* Succeeds with "get" ACL (or local authentication), or operating on
     * self. */
    gprinc_test_succeed("admin/none", admin_none_princ);
    gprinc_test_succeed("$admin", admin_princ);
    gprinc_test_succeed("admin/get", princ);

    /* Fails over RPC if "get" ACL is not granted, or if we authenticated to
     * kadmin/changepw and getting another principal entry. */
    if (rpc) {
        gprinc_test_fail("$admin", princ, KADM5_AUTH_GET);
        gprinc_test_fail("admin/none", princ, KADM5_AUTH_GET);
        gprinc_test_fail("admin/add", princ, KADM5_AUTH_GET);
        gprinc_test_fail("admin/modify", princ, KADM5_AUTH_GET);
        gprinc_test_fail("admin/delete", princ, KADM5_AUTH_GET);
    }

    /* Entry contains no key data or tl-data unless asked for. */
    handle = get_handle("admin");
    check(kadm5_get_principal(handle, princ, &ent,
                              KADM5_PRINCIPAL_NORMAL_MASK));
    assert(ent.n_tl_data == 0);
    assert(ent.n_key_data == 0);
    assert(ent.tl_data == NULL);
    check(kadm5_free_principal_ent(handle, &ent));

    /* Key data (without the actual keys over RPC) is provided if asked for. */
    check(kadm5_get_principal(handle, princ, &ent,
                              KADM5_PRINCIPAL_NORMAL_MASK | KADM5_KEY_DATA));
    assert(ent.n_key_data == 2);
    for (i = 0; i < ent.n_key_data; i++)
        assert(rpc == (ent.key_data[i].key_data_length[0] == 0));
    check(kadm5_free_principal_ent(handle, &ent));
    free_handle(handle);

    /* Fails with null handle or principal. */
    gprinc_test_fail(NULL, princ, KADM5_BAD_SERVER_HANDLE);
    gprinc_test_fail("admin", NULL, EINVAL);

    delete_princ(princ);
    krb5_free_principal(context, princ);
    krb5_free_principal(context, admin_princ);
    krb5_free_principal(context, admin_none_princ);
}

static void
test_init_destroy(void)
{
    krb5_context ctx;
    kadm5_ret_t ret;
    kadm5_config_params params;
    kadm5_principal_ent_rec ent, gent;
    krb5_principal princ = parse_princ("init-test");
    krb5_ccache cc;
    void *handle;
    char hostname[MAXHOSTNAMELEN];
    int r;

    memset(&params, 0, sizeof(params));
    memset(&ent, 0, sizeof(ent));
    ent.principal = princ;

    r = gethostname(hostname, sizeof(hostname));
    assert(r == 0);

    /* Destroy fails with no server handle. */
    check_fail(kadm5_destroy(NULL), KADM5_BAD_SERVER_HANDLE);

    /* Fails with bad structure version mask. */
    check_fail(kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE, NULL,
                          0x65432101, api, NULL, &handle),
               KADM5_BAD_STRUCT_VERSION);
    check_fail(kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE, NULL,
                          1, api, NULL, &handle), KADM5_BAD_STRUCT_VERSION);

    /* Fails with too-old or too-new structure version. */
    check_fail(kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE, NULL,
                          KADM5_STRUCT_VERSION_MASK, api, NULL, &handle),
               KADM5_OLD_STRUCT_VERSION);
    check_fail(kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE, NULL,
                          KADM5_STRUCT_VERSION_MASK | 0xca, api, NULL,
                          &handle), KADM5_NEW_STRUCT_VERSION);

    /* Fails with bad API version mask. */
    check_fail(kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE, NULL,
                          KADM5_STRUCT_VERSION, 0x65432100, NULL, &handle),
               KADM5_BAD_API_VERSION);
    check_fail(kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE, NULL,
                          KADM5_STRUCT_VERSION, 4, NULL, &handle),
               KADM5_BAD_API_VERSION);

    /* Fails with too-old or too-new API version.*/
    ret = kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE, NULL,
                     KADM5_STRUCT_VERSION, KADM5_API_VERSION_MASK, NULL,
                     &handle);
    assert(ret == (rpc ? KADM5_OLD_LIB_API_VERSION :
                   KADM5_OLD_SERVER_API_VERSION));
    ret = kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE, NULL,
                     KADM5_STRUCT_VERSION, KADM5_API_VERSION_MASK | 0xca, NULL,
                     &handle);
    assert(ret == (rpc ? KADM5_NEW_LIB_API_VERSION :
                   KADM5_NEW_SERVER_API_VERSION));

    /* Fails with structure and API version reversed. */
    check_fail(kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE, NULL,
                          api, KADM5_STRUCT_VERSION, NULL, &handle),
               KADM5_BAD_STRUCT_VERSION);

    /* Hardcoded default max lifetime is used when no handle or krb5.conf
     * setting is given. */
    handle = get_handle("admin");
    check(kadm5_create_principal(handle, &ent, KADM5_PRINCIPAL, "pw"));
    check(kadm5_get_principal(handle, princ, &gent,
                              KADM5_PRINCIPAL_NORMAL_MASK));
    assert(gent.max_life == KRB5_KDB_MAX_LIFE);
    check(kadm5_delete_principal(handle, princ));
    check(kadm5_free_principal_ent(handle, &gent));
    free_handle(handle);

    /* Fails with configured unknown realm.  Do these tests in separate krb5
     * contexts since the realm setting sticks to the context. */
    check(kadm5_init_krb5_context(&ctx));
    params.realm = "";
    params.mask = KADM5_CONFIG_REALM;
    ret = kadm5_init(ctx, "admin", "admin", KADM5_ADMIN_SERVICE, &params,
                     KADM5_STRUCT_VERSION, api, NULL, &handle);
    assert(ret == (rpc ? KADM5_MISSING_KRB5_CONF_PARAMS : ENOENT));
    krb5_free_context(ctx);

    check(kadm5_init_krb5_context(&ctx));
    params.realm = "@";
    ret = kadm5_init(ctx, "admin", "admin", KADM5_ADMIN_SERVICE, &params,
                     KADM5_STRUCT_VERSION, api, NULL, &handle);
    assert(ret == (rpc ? KADM5_MISSING_KRB5_CONF_PARAMS : ENOENT));
    krb5_free_context(ctx);

    check(kadm5_init_krb5_context(&ctx));
    params.realm = "BAD.REALM";
    ret = kadm5_init(ctx, "admin", "admin", KADM5_ADMIN_SERVICE, &params,
                     KADM5_STRUCT_VERSION, api, NULL, &handle);
    assert(ret == (rpc ? KADM5_MISSING_KRB5_CONF_PARAMS : ENOENT));
    krb5_free_context(ctx);

    /* Succeeds with explicit client realm and configured realm. */
    check(kadm5_init_krb5_context(&ctx));
    params.realm = "KRBTEST.COM";
    check(kadm5_init(ctx, "admin@KRBTEST.COM", "admin", KADM5_ADMIN_SERVICE,
                     &params, KADM5_STRUCT_VERSION, api, NULL, &handle));
    check(kadm5_destroy(handle));
    krb5_free_context(ctx);

    /* Succeeds with explicit client realm. */
    check(kadm5_init(context, "admin@KRBTEST.COM", "admin",
                     KADM5_ADMIN_SERVICE, NULL, KADM5_STRUCT_VERSION, api,
                     NULL, &handle));
    check(kadm5_destroy(handle));


    if (rpc) {
        check(krb5_cc_default(context, &cc));

        /* Succeeds with configured host and port. */
        params.admin_server = hostname;
        params.kadmind_port = 61001;
        params.mask = KADM5_CONFIG_ADMIN_SERVER | KADM5_CONFIG_KADMIND_PORT;
        check(kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE,
                         &params, KADM5_STRUCT_VERSION, api, NULL, &handle));
        check(kadm5_destroy(handle));

        /* Fails with wrong configured port. */
        params.kadmind_port = 4;
        check_fail(kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE,
                              &params, KADM5_STRUCT_VERSION, api, NULL,
                              &handle), KADM5_RPC_ERROR);

        /* Fails with non-resolving hostname. */
        params.admin_server = "does.not.exist";
        params.mask = KADM5_CONFIG_ADMIN_SERVER;
        check_fail(kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE,
                              &params, KADM5_STRUCT_VERSION, api, NULL,
                              &handle), KADM5_CANT_RESOLVE);

        /* Fails with uninitialized cache. */
        check_fail(kadm5_init_with_creds(context, "admin", cc,
                                         KADM5_ADMIN_SERVICE, NULL,
                                         KADM5_STRUCT_VERSION, api, NULL,
                                         &handle), KRB5_FCC_NOFILE);

        /* Succeeds with cache containing kadmin/admin cred. */
        kinit(cc, "admin", "admin", KADM5_ADMIN_SERVICE);
        check(kadm5_init_with_creds(context, "admin", cc, KADM5_ADMIN_SERVICE,
                                    NULL, KADM5_STRUCT_VERSION, api, NULL,
                                    &handle));
        check(kadm5_destroy(handle));

        /* Succeeds with cache containing kadmin/changepw cred. */
        kinit(cc, "admin", "admin", KADM5_CHANGEPW_SERVICE);
        check(kadm5_init_with_creds(context, "admin", cc,
                                    KADM5_CHANGEPW_SERVICE, NULL,
                                    KADM5_STRUCT_VERSION, api, NULL, &handle));
        check(kadm5_destroy(handle));

        /* Fails with cache containing only a TGT. */
        kinit(cc, "admin", "admin", NULL);
        check_fail(kadm5_init_with_creds(context, "admin", cc,
                                         KADM5_ADMIN_SERVICE, NULL,
                                         KADM5_STRUCT_VERSION, api, NULL,
                                         &handle), KRB5_CC_NOTFOUND);

        /* Fails authenticating to non-kadmin princ. */
        check_fail(kadm5_init(context, "admin", "admin", "user", NULL,
                              KADM5_STRUCT_VERSION, api, NULL, &handle),
                   KADM5_RPC_ERROR);

        /* Fails authenticating to nonexistent princ. */
        check_fail(kadm5_init(context, "admin", "admin", "noexist", NULL,
                              KADM5_STRUCT_VERSION, api, NULL, &handle),
                   KADM5_SECURE_PRINC_MISSING);

        /* Fails authenticating to client princ (which is non-kadmin). */
        check_fail(kadm5_init(context, "admin", "admin", "admin", NULL,
                              KADM5_STRUCT_VERSION, api, NULL, &handle),
                   KADM5_RPC_ERROR);

        /* Fails with wrong password. */
        check_fail(kadm5_init(context, "admin", "wrong", KADM5_ADMIN_SERVICE,
                              NULL, KADM5_STRUCT_VERSION, api, NULL, &handle),
                   KADM5_BAD_PASSWORD);

        /* Fails with null client name. */
        check_fail(kadm5_init(context, NULL, "admin", KADM5_ADMIN_SERVICE,
                              NULL, KADM5_STRUCT_VERSION, api, NULL, &handle),
                   EINVAL);

        /* Fails with nonexistent client name. */
        check_fail(kadm5_init(context, "noexist", "admin", KADM5_ADMIN_SERVICE,
                              NULL, KADM5_STRUCT_VERSION, api, NULL, &handle),
                   KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN);

        /* Fails with nonexistent client name with explicit realm. */
        check_fail(kadm5_init(context, "noexist@KRBTEST.COM", "admin",
                              KADM5_ADMIN_SERVICE, NULL, KADM5_STRUCT_VERSION,
                              api, NULL, &handle),
                   KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN);

        /* Fails with nonexistent client name with unknown realm. */
        check_fail(kadm5_init(context, "noexist@BAD.REALM", "admin",
                              KADM5_ADMIN_SERVICE, NULL, KADM5_STRUCT_VERSION,
                              api, NULL, &handle), KRB5_REALM_UNKNOWN);

        /* Fails with known name but unknown realm. */
        check_fail(kadm5_init(context, "admin@BAD.REALM", "admin",
                              KADM5_ADMIN_SERVICE, NULL, KADM5_STRUCT_VERSION,
                              api, NULL, &handle), KRB5_REALM_UNKNOWN);

        check(krb5_cc_destroy(context, cc));
    } else {
        /* Fails with nonexistent stash file. */
        params.stash_file = "does/not/exist";
        params.mask = KADM5_CONFIG_STASH_FILE;
        check_fail(kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE,
                              &params, KADM5_STRUCT_VERSION, api, NULL,
                              &handle), KRB5_KDB_CANTREAD_STORED);

        /* Uses configured defaults for principal creation. */
        params.max_life = 10;
        params.max_rlife = 20;
        params.expiration = 30;
        params.num_keysalts = 0;
        params.mask = KADM5_CONFIG_MAX_LIFE | KADM5_CONFIG_MAX_RLIFE |
            KADM5_CONFIG_EXPIRATION | KADM5_CONFIG_ENCTYPES;
        check(kadm5_init(context, "admin", "admin", KADM5_ADMIN_SERVICE,
                         &params, KADM5_STRUCT_VERSION, api, NULL, &handle));
        check(kadm5_create_principal(handle, &ent, KADM5_PRINCIPAL, "pw"));
        check(kadm5_get_principal(handle, princ, &gent,
                                  KADM5_PRINCIPAL_NORMAL_MASK |
                                  KADM5_KEY_DATA));
        assert(gent.max_life == 10);
        assert(gent.max_renewable_life == 20);
        assert(gent.princ_expire_time == 30);
        assert(gent.n_key_data == 0);
        check(kadm5_delete_principal(handle, princ));
        check(kadm5_free_principal_ent(handle, &gent));
        check(kadm5_destroy(handle));

        /* Succeeds with incorrect password using local auth. */
        check(kadm5_init(context, "admin", "wrong", KADM5_ADMIN_SERVICE, NULL,
                         KADM5_STRUCT_VERSION, api, NULL, &handle));
        check(kadm5_destroy(handle));

        /* Succeeds with null service using local auth. */
        check(kadm5_init(context, "admin", "admin", NULL, NULL,
                         KADM5_STRUCT_VERSION, api, NULL, &handle));
        check(kadm5_destroy(handle));

        /* Succeeds with nonexistent, non-kadmin service using local auth. */
        check(kadm5_init(context, "admin", "admin", "foobar", NULL,
                         KADM5_STRUCT_VERSION, api, NULL, &handle));
        check(kadm5_destroy(handle));
    }

    krb5_free_principal(context, princ);
}

static void
mpol_test_fail(char *user, kadm5_policy_ent_t ent, uint32_t mask,
               krb5_error_code code)
{
    void *handle = get_handle(user);

    check_fail(kadm5_modify_policy(handle, ent, mask), code);
    free_handle(handle);
}

static void
mpol_test_compare(void *handle, kadm5_policy_ent_t ent, uint32_t mask)
{
    mpol_test_fail(handle, ent, mask, 0);
    compare_policy(ent, mask);
}

static void
test_modify_policy(void)
{
    kadm5_policy_ent_rec ent;

    memset(&ent, 0, sizeof(ent));
    ent.policy = "modify-policy-test";
    create_simple_policy(ent.policy);

    /* pw_min_life = 0 and pw_min_life != 0 */
    mpol_test_compare("admin", &ent, KADM5_PW_MIN_LIFE);
    ent.pw_min_life = 32;
    mpol_test_compare("admin", &ent, KADM5_PW_MIN_LIFE);

    /* pw_max_life = 0 and pw_max_life != 0 */
    mpol_test_compare("admin", &ent, KADM5_PW_MAX_LIFE);
    ent.pw_max_life = 32;
    mpol_test_compare("admin", &ent, KADM5_PW_MAX_LIFE);

    /* pw_min_length = 0 (rejected) and pw_min_length != 0 */
    mpol_test_fail("admin", &ent, KADM5_PW_MIN_LENGTH, KADM5_BAD_LENGTH);
    ent.pw_min_length = 8;
    mpol_test_compare("admin", &ent, KADM5_PW_MIN_LENGTH);

    /* pw_min_classes = 0 (rejected), 1, 5, 6 (rejected) */
    mpol_test_fail("admin", &ent, KADM5_PW_MIN_CLASSES, KADM5_BAD_CLASS);
    ent.pw_min_classes = 1;
    mpol_test_compare("admin", &ent, KADM5_PW_MIN_CLASSES);
    ent.pw_min_classes = 5;
    mpol_test_compare("admin", &ent, KADM5_PW_MIN_CLASSES);
    ent.pw_min_classes = 6;
    mpol_test_fail("admin", &ent, KADM5_PW_MIN_CLASSES, KADM5_BAD_CLASS);

    /* pw_history_num = 0 (rejected), 1, 10 */
    mpol_test_fail("admin", &ent, KADM5_PW_HISTORY_NUM, KADM5_BAD_HISTORY);
    ent.pw_history_num = 1;
    mpol_test_compare("admin", &ent, KADM5_PW_HISTORY_NUM);
    ent.pw_history_num = 10;
    mpol_test_compare("admin", &ent, KADM5_PW_HISTORY_NUM);

    if (api >= KADM5_API_VERSION_3) {
        ent.pw_max_fail = 2;
        mpol_test_compare("admin", &ent, KADM5_PW_MAX_FAILURE);
        ent.pw_failcnt_interval = 90;
        mpol_test_compare("admin", &ent, KADM5_PW_FAILURE_COUNT_INTERVAL);
        ent.pw_lockout_duration = 180;
        mpol_test_compare("admin", &ent, KADM5_PW_LOCKOUT_DURATION);
    }

    /* Fails over RPC if "modify" ACL is not granted, or if we authenticated to
     * kadmin/changepw. */
    if (rpc) {
        mpol_test_fail("$admin", &ent, KADM5_PW_MAX_LIFE, KADM5_AUTH_MODIFY);
        mpol_test_fail("admin/none", &ent, KADM5_PW_MAX_LIFE,
                       KADM5_AUTH_MODIFY);
        mpol_test_fail("admin/get", &ent, KADM5_PW_MAX_LIFE,
                       KADM5_AUTH_MODIFY);
        mpol_test_compare("admin/modify", &ent, KADM5_PW_MAX_LIFE);
    }

    delete_policy(ent.policy);

    /* Fails with empty or null policy name. */
    ent.policy = NULL;
    mpol_test_fail("admin", &ent, KADM5_PW_MAX_LIFE, EINVAL);
    ent.policy = "";
    mpol_test_fail("admin", &ent, KADM5_PW_MAX_LIFE, KADM5_BAD_POLICY);

    /* Fails with null handle or policy ent. */
    mpol_test_fail(NULL, &ent, KADM5_PW_MAX_LIFE, KADM5_BAD_SERVER_HANDLE);
    mpol_test_fail("admin", NULL, KADM5_PW_MAX_LIFE, EINVAL);
}

static void
mprinc_test_fail(char *user, kadm5_principal_ent_t ent, uint32_t mask,
                 krb5_error_code code)
{
    void *handle = get_handle(user);

    check_fail(kadm5_modify_principal(handle, ent, mask), code);
    free_handle(handle);
}

static void
mprinc_test_compare(char *user, kadm5_principal_ent_t ent, uint32_t mask)
{
    mprinc_test_fail(user, ent, mask, 0);
    compare_princ(ent, mask);
}

static void
test_modify_principal(void)
{
    void *handle;
    krb5_principal princ = parse_princ("modify-principal-test");
    kadm5_principal_ent_rec ent;
    krb5_tl_data tl = { NULL, 1, 1, (uint8_t *)"x" };
    krb5_tl_data tl2 = { NULL, 999, 6, (uint8_t *)"foobar" };

    memset(&ent, 0, sizeof(ent));
    ent.principal = princ;

    /* Fails with unknown principal. */
    mprinc_test_fail("admin", &ent, KADM5_KVNO, KADM5_UNK_PRINC);

    create_simple_princ(princ, NULL);

    /* Fails with prohibited mask bit or tl-data type. */
    mprinc_test_fail("admin", &ent, KADM5_AUX_ATTRIBUTES, KADM5_BAD_MASK);
    mprinc_test_fail("admin", &ent, KADM5_KEY_DATA, KADM5_BAD_MASK);
    mprinc_test_fail("admin", &ent, KADM5_LAST_FAILED, KADM5_BAD_MASK);
    mprinc_test_fail("admin", &ent, KADM5_LAST_SUCCESS, KADM5_BAD_MASK);
    mprinc_test_fail("admin", &ent, KADM5_LAST_PWD_CHANGE, KADM5_BAD_MASK);
    mprinc_test_fail("admin", &ent, KADM5_MKVNO, KADM5_BAD_MASK);
    mprinc_test_fail("admin", &ent, KADM5_MOD_NAME, KADM5_BAD_MASK);
    mprinc_test_fail("admin", &ent, KADM5_MOD_TIME, KADM5_BAD_MASK);
    mprinc_test_fail("admin", &ent, KADM5_PRINCIPAL, KADM5_BAD_MASK);

    /* Fails with tl-data type below 256. */
    ent.n_tl_data = 1;
    ent.tl_data = &tl;
    mprinc_test_fail("admin", &ent, KADM5_TL_DATA, KADM5_BAD_TL_TYPE);

    /* Fails with fail_auth_count other than zero. */
    ent.fail_auth_count = 1234;
    mprinc_test_fail("admin", &ent, KADM5_FAIL_AUTH_COUNT,
                     KADM5_BAD_SERVER_PARAMS);
    ent.fail_auth_count = 0;

    /* Succeeds with zero values of various fields. */
    mprinc_test_compare("admin", &ent, KADM5_PW_EXPIRATION);
    mprinc_test_compare("admin", &ent, KADM5_MAX_LIFE);
    mprinc_test_compare("admin", &ent, KADM5_MAX_RLIFE);
    mprinc_test_compare("admin", &ent, KADM5_FAIL_AUTH_COUNT);
    mprinc_test_compare("admin/modify", &ent, KADM5_PRINC_EXPIRE_TIME);
    mprinc_test_compare("admin", &ent, KADM5_POLICY_CLR);

    /* Setting a policy causes a pw_expiration computation.  Explicit
     * PW_EXPIRATION overrides the policy. */
    ent.pw_expiration = 1234;
    mprinc_test_compare("admin", &ent, KADM5_PW_EXPIRATION);
    ent.policy = "dict-only-pol";
    mprinc_test_compare("admin", &ent, KADM5_POLICY);
    ent.policy = "test-pol";
    mprinc_test_compare("admin", &ent, KADM5_POLICY);
    ent.pw_expiration = 999999999;
    mprinc_test_compare("admin", &ent, KADM5_PW_EXPIRATION);
    mprinc_test_compare("admin", &ent, KADM5_POLICY_CLR);

    /* Succeeds with non-zero values of various fields. */
    ent.princ_expire_time = 1234;
    mprinc_test_compare("admin", &ent, KADM5_PRINC_EXPIRE_TIME);
    ent.attributes = KRB5_KDB_DISALLOW_ALL_TIX;
    mprinc_test_compare("admin", &ent, KADM5_ATTRIBUTES);
    ent.attributes = KRB5_KDB_REQUIRES_PWCHANGE;
    mprinc_test_compare("admin", &ent, KADM5_ATTRIBUTES);
    ent.attributes = KRB5_KDB_DISALLOW_TGT_BASED;
    mprinc_test_compare("admin", &ent, KADM5_ATTRIBUTES);
    ent.max_life = 3456;
    mprinc_test_compare("admin", &ent, KADM5_MAX_LIFE);
    ent.kvno = 7;
    mprinc_test_compare("admin", &ent, KADM5_KVNO);

    /* Fails over RPC if "modify" ACL is not granted, or if we authenticated to
     * kadmin/changepw. */
    if (rpc) {
        mprinc_test_fail("$admin", &ent, KADM5_KVNO, KADM5_AUTH_MODIFY);
        mprinc_test_fail("admin/none", &ent, KADM5_KVNO, KADM5_AUTH_MODIFY);
        mprinc_test_fail("admin/get", &ent, KADM5_KVNO, KADM5_AUTH_MODIFY);
        mprinc_test_fail("admin/add", &ent, KADM5_KVNO, KADM5_AUTH_MODIFY);
        mprinc_test_fail("admin/delete", &ent, KADM5_KVNO, KADM5_AUTH_MODIFY);
    }

    /* tl-data of type > 255 is accepted. */
    handle = get_handle("admin");
    ent.max_renewable_life = 88;
    ent.tl_data = &tl2;
    check(kadm5_modify_principal(handle, &ent,
                                 KADM5_MAX_RLIFE | KADM5_TL_DATA));
    memset(&ent, 0, sizeof(ent));
    check(kadm5_get_principal(handle, princ, &ent,
                              KADM5_PRINCIPAL_NORMAL_MASK | KADM5_TL_DATA));
    assert(ent.max_renewable_life == 88);
    assert(ent.n_tl_data == 1);
    assert(ent.tl_data->tl_data_type == tl2.tl_data_type);
    assert(ent.tl_data->tl_data_length == tl2.tl_data_length);
    assert(memcmp(ent.tl_data->tl_data_contents, tl2.tl_data_contents,
                  tl2.tl_data_length) == 0);
    check(kadm5_free_principal_ent(handle, &ent));
    free_handle(handle);

    /* Fails with null handle or principal ent. */
    mprinc_test_fail(NULL, &ent, KADM5_KVNO, KADM5_BAD_SERVER_HANDLE);
    mprinc_test_fail("admin", NULL, KADM5_KVNO, EINVAL);

    delete_princ(princ);
    krb5_free_principal(context, princ);
}

static void
rnd_test_fail(char *user, krb5_principal princ, krb5_error_code code)
{
    void *handle = get_handle(user);

    check_fail(kadm5_randkey_principal(handle, princ, NULL, NULL), code);
    free_handle(handle);
}

static void
rnd_test_succeed(char *user, krb5_principal princ)
{
    rnd_test_fail(user, princ, 0);
}

static void
test_randkey(void)
{
    void *handle;
    krb5_principal princ = parse_princ("randkey-principal-test");
    krb5_principal user_princ = parse_princ("user");
    krb5_principal admin_princ = parse_princ("admin");
    kadm5_principal_ent_rec ent;
    krb5_keyblock *keys;
    int n_keys, i;

    create_simple_princ(princ, NULL);

    /* Check kvno and enctypes after randkey. */
    handle = get_handle("admin");
    check(kadm5_randkey_principal(handle, princ, &keys, &n_keys));
    check(kadm5_get_principal(handle, princ, &ent, KADM5_KEY_DATA));
    compare_key_data(&ent, default_supported_enctypes);
    assert(ent.key_data[0].key_data_kvno == 2);
    assert(n_keys == ent.n_key_data);
    for (i = 0; i < n_keys; i++)
        krb5_free_keyblock_contents(context, &keys[i]);
    free(keys);
    check(kadm5_free_principal_ent(handle, &ent));
    free_handle(handle);

    /*
     * Fails over RPC if "change" ACL is not granted, or if we authenticated to
     * kadmin/changepw and are changing another principal's password, or for
     * self-service if the policy minimum life has not elapsed since the last
     * key change.
     */
    if (rpc) {
        rnd_test_fail("$admin", user_princ, KADM5_AUTH_CHANGEPW);
        rnd_test_fail("admin/none", user_princ, KADM5_AUTH_CHANGEPW);
        rnd_test_fail("admin/delete", user_princ, KADM5_AUTH_CHANGEPW);
        rnd_test_succeed("admin/modify", user_princ);
        cpw_test_succeed("admin", user_princ, USER_PASSWORD);
        rnd_test_fail("user", user_princ, KADM5_PASS_TOOSOON);
        rnd_test_fail("$user", user_princ, KADM5_PASS_TOOSOON);
    }

    /* Succeeds with change privilege in spite of policy minimum life. */
    rnd_test_succeed("admin/modify", user_princ);
    cpw_test_succeed("admin", user_princ, USER_PASSWORD);

    /* Succeeds for self-service when authenticating to kadmin/changepw. */
    handle = get_handle("$admin");
    check(kadm5_randkey_principal(handle, admin_princ, NULL, NULL));
    check(kadm5_chpass_principal(handle, admin_princ, ADMIN_PASSWORD));
    free_handle(handle);

    /* Fails with null handle or principal name. */
    rnd_test_fail(NULL, princ, KADM5_BAD_SERVER_HANDLE);
    rnd_test_fail("admin", NULL, EINVAL);

    delete_princ(princ);
    krb5_free_principal(context, princ);
    krb5_free_principal(context, user_princ);
    krb5_free_principal(context, admin_princ);
}

int
main(int argc, char **argv)
{
    assert(argc == 2);
    rpc = (strcmp(argv[1], "clnt") == 0);

    check(kadm5_init_krb5_context(&context));

    api = KADM5_API_VERSION_2;
    test_create_policy();
    test_get_policy();
    test_modify_policy();

    api = KADM5_API_VERSION_4;
    test_chpass();
    test_create_policy();
    test_create_principal();
    test_delete_policy();
    test_delete_principal();
    test_get_policy();
    test_get_principal();
    test_init_destroy();
    test_modify_policy();
    test_modify_principal();
    test_randkey();

    krb5_free_context(context);

    return 0;
}