root/usr/src/lib/krb5/plugins/kdb/ldap/libkdb_ldap/kdb_ldap_conn.c
/*
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */


/*
 * lib/kdb/kdb_ldap/kdb_ldap_conn.c
 *
 * Copyright (c) 2004-2005, Novell, Inc.
 * 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.
 *   * The copyright holder's name is not used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * 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 OWNER 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 "autoconf.h"
#if HAVE_UNISTD_H
#include <unistd.h>
#endif

#include "ldap_main.h"
#include "ldap_service_stash.h"
#include <kdb5.h>
#include <libintl.h>

static krb5_error_code
krb5_validate_ldap_context(krb5_context context, krb5_ldap_context *ldap_context)
{
    krb5_error_code             st=0;
    unsigned char               *password=NULL;

    if (ldap_context->bind_dn == NULL) {
        st = EINVAL;
        /* Solaris Kerberos: Keep error messages consistent */
        krb5_set_error_message(context, st, gettext("LDAP bind dn value missing"));
        goto err_out;
    }

    if (ldap_context->bind_pwd == NULL && ldap_context->service_password_file == NULL) {
        st = EINVAL;
        /* Solaris Kerberos: Keep error messages consistent */
        krb5_set_error_message(context, st, gettext("LDAP bind password value missing"));
        goto err_out;
    }

    if (ldap_context->bind_pwd == NULL && ldap_context->service_password_file !=
        NULL && ldap_context->service_cert_path == NULL) {
        if ((st=krb5_ldap_readpassword(context, ldap_context, &password)) != 0) {
            prepend_err_str(context, gettext("Error reading password from stash: "), st, st);
            goto err_out;
        }

        /* Check if the returned 'password' is actually the path of a certificate */
        if (!strncmp("{FILE}", (char *)password, 6)) {
            /* 'password' format: <path>\0<password> */
            ldap_context->service_cert_path = strdup((char *)password + strlen("{FILE}"));
            if (ldap_context->service_cert_path == NULL) {
                st = ENOMEM;
                krb5_set_error_message(context, st, gettext("Error: memory allocation failed"));
                goto err_out;
            }
            if (password[strlen((char *)password) + 1] == '\0')
                ldap_context->service_cert_pass = NULL;
            else {
                ldap_context->service_cert_pass = strdup((char *)password +
                                                         strlen((char *)password) + 1);
                if (ldap_context->service_cert_pass == NULL) {
                    st = ENOMEM;
                    krb5_set_error_message(context, st, gettext("Error: memory allocation failed"));
                    goto err_out;
                }
            }
            free(password);
        } else {
            ldap_context->bind_pwd = (char *)password;
            if (ldap_context->bind_pwd == NULL) {
                st = EINVAL;
                krb5_set_error_message(context, st, gettext("Error reading password from stash"));
                goto err_out;
            }
        }
    }

    /* NULL password not allowed */
    if (ldap_context->bind_pwd != NULL && strlen(ldap_context->bind_pwd) == 0) {
        st = EINVAL;
        krb5_set_error_message(context, st, gettext("Service password length is zero"));
        goto err_out;
    }

err_out:
    return st;
}

/*
 * Internal Functions called by init functions.
 */

static krb5_error_code
krb5_ldap_bind(ldap_context, ldap_server_handle)
    krb5_ldap_context           *ldap_context;
    krb5_ldap_server_handle     *ldap_server_handle;
{
    krb5_error_code             st=0;
    struct berval               bv={0, NULL}, *servercreds=NULL;

    if (ldap_context->service_cert_path != NULL) {
        /* Certificate based bind (SASL EXTERNAL mechanism) */

        st = ldap_sasl_bind_s(ldap_server_handle->ldap_handle,
                              NULL,        /* Authenticating dn */
                              LDAP_SASL_EXTERNAL,  /* Method used for authentication */
                              &bv,
                              NULL,
                              NULL,
                              &servercreds);

        while (st == LDAP_SASL_BIND_IN_PROGRESS) {
            st = ldap_sasl_bind_s(ldap_server_handle->ldap_handle,
                                  NULL,
                                  LDAP_SASL_EXTERNAL,
                                  servercreds,
                                  NULL,
                                  NULL,
                                  &servercreds);
        }
    } else {
        /* password based simple bind */
        bv.bv_val = ldap_context->bind_pwd;
        bv.bv_len = strlen(ldap_context->bind_pwd);
        st = ldap_sasl_bind_s(ldap_server_handle->ldap_handle,
                                ldap_context->bind_dn,
                                LDAP_SASL_SIMPLE, &bv, NULL,
                                NULL, NULL);
    }
    return st;
}

static krb5_error_code
krb5_ldap_initialize(ldap_context, server_info)
    krb5_ldap_context *ldap_context;
    krb5_ldap_server_info *server_info;
{
    krb5_error_code             st=0;
    krb5_ldap_server_handle     *ldap_server_handle=NULL;
    char                        *errstr = NULL;


    ldap_server_handle = calloc(1, sizeof(krb5_ldap_server_handle));
    if (ldap_server_handle == NULL) {
        st = ENOMEM;
        goto err_out;
    }
    else {
        /*
         * Solaris Kerbreros: need ldap_handle to be NULL so calls to
         * ldap_initialize won't leak handles
         */
        ldap_server_handle->ldap_handle = NULL;
    }

    if (strncasecmp(server_info->server_name, "ldapi:", 6) == 0) {
        /*
         * Solaris Kerberos: ldapi is not supported on Solaris at this time.
         * return an error.
         */
        if (ldap_context->kcontext)
            krb5_set_error_message (ldap_context->kcontext, KRB5_KDB_ACCESS_ERROR,
                gettext("ldapi is not supported"));
        st = KRB5_KDB_ACCESS_ERROR;
        goto err_out;
    } else {
        /*
         * Solaris Kerbreros: need to use SSL to protect LDAP simple and
         * External binds.
         */
        if (ldap_context->root_certificate_file == NULL) {
            if (ldap_context->kcontext)
                krb5_set_error_message (ldap_context->kcontext, KRB5_KDB_ACCESS_ERROR,
                    gettext("ldap_cert_path not set, can not create SSL connection"));
            st = KRB5_KDB_ACCESS_ERROR;
            goto err_out;
        }

        /* setup for SSL */
        if ((st = ldapssl_client_init(ldap_context->root_certificate_file, NULL)) < 0) {
            if (ldap_context->kcontext)
                krb5_set_error_message (ldap_context->kcontext, KRB5_KDB_ACCESS_ERROR, "%s",
                    ldapssl_err2string(st));
            st = KRB5_KDB_ACCESS_ERROR;
            goto err_out;
        }

        /* ldap init, use SSL */
        if ((st = ldap_initialize(&ldap_server_handle->ldap_handle,
                                    server_info->server_name, SSL_ON, &errstr)) != LDAP_SUCCESS) {
            if (ldap_context->kcontext) {
                krb5_set_error_message (ldap_context->kcontext, KRB5_KDB_ACCESS_ERROR, "%s",
                    errstr);
            }
            st = KRB5_KDB_ACCESS_ERROR;
            goto err_out;
        }

        if (ldap_context->service_cert_path != NULL) {
            /*
             * Solaris Kerbreros: for LDAP_SASL_EXTERNAL bind which requires the
             * client offer its cert to the server.
             */
            if ((st = ldapssl_enable_clientauth(ldap_server_handle->ldap_handle,
                                        NULL, ldap_context->service_cert_pass,
                                        "XXX WAF need cert nickname/label")) < 0) {
                if (ldap_context->kcontext) {
                    krb5_set_error_message (ldap_context->kcontext,
                                            KRB5_KDB_ACCESS_ERROR, "%s",
                                            ldap_err2string(st));
                }
                st = KRB5_KDB_ACCESS_ERROR;
                goto err_out;
            }
        }
    }

    if ((st=krb5_ldap_bind(ldap_context, ldap_server_handle)) == 0) {
        ldap_server_handle->server_info_update_pending = FALSE;
        server_info->server_status = ON;
        krb5_update_ldap_handle(ldap_server_handle, server_info);
    } else {
        if (ldap_context->kcontext)
            /* Solaris Kerberos: Better error message */
            krb5_set_error_message (ldap_context->kcontext,
                                    KRB5_KDB_ACCESS_ERROR,
                                    gettext("Failed to bind to ldap server \"%s\": %s"),
                                    server_info->server_name, ldap_err2string(st));
        st = KRB5_KDB_ACCESS_ERROR;
        server_info->server_status = OFF;
        time(&server_info->downtime);
        (void)ldap_unbind_s(ldap_server_handle->ldap_handle);
        free(ldap_server_handle);
    }

err_out:
    return st;
}

/*
 * initialization for data base routines.
 */

krb5_error_code
krb5_ldap_db_init(krb5_context context, krb5_ldap_context *ldap_context)
{
    krb5_error_code             st=0;
    krb5_boolean                sasl_mech_supported=TRUE;
    int                         cnt=0, version=LDAP_VERSION3;
#ifdef LDAP_OPT_NETWORK_TIMEOUT
    struct timeval              local_timelimit = {10,0};
#elif defined LDAP_X_OPT_CONNECT_TIMEOUT
    int                         local_timelimit = 1000; /* Solaris Kerberos: 1 second */
#endif

    if ((st=krb5_validate_ldap_context(context, ldap_context)) != 0)
        goto err_out;

    ldap_set_option(NULL, LDAP_OPT_PROTOCOL_VERSION, &version);
#ifdef LDAP_OPT_NETWORK_TIMEOUT
    ldap_set_option(NULL, LDAP_OPT_NETWORK_TIMEOUT, &local_timelimit);
#elif defined LDAP_X_OPT_CONNECT_TIMEOUT
    ldap_set_option(NULL, LDAP_X_OPT_CONNECT_TIMEOUT, &local_timelimit);
#endif

    HNDL_LOCK(ldap_context);
    while (ldap_context->server_info_list[cnt] != NULL) {
        krb5_ldap_server_info *server_info=NULL;

        server_info = ldap_context->server_info_list[cnt];

        if (server_info->server_status == NOTSET) {
            int conns=0;

            /*
             * Check if the server has to perform certificate-based authentication
             */
            if (ldap_context->service_cert_path != NULL) {
                /* Find out if the server supports SASL EXTERNAL mechanism */
                if (has_sasl_external_mech(context, server_info->server_name) == 1) {
                    cnt++;
                    sasl_mech_supported = FALSE;
                    continue; /* Check the next LDAP server */
                }
                sasl_mech_supported = TRUE;
            }

            krb5_clear_error_message(context);

            for (conns=0; conns < ldap_context->max_server_conns; ++conns) {
                if ((st=krb5_ldap_initialize(ldap_context, server_info)) != 0)
                    break;
            } /* for (conn= ... */

            if (server_info->server_status == ON)
                break;  /* server init successful, so break */
        }
        ++cnt;
    }
    HNDL_UNLOCK(ldap_context);

err_out:
    if (sasl_mech_supported == FALSE) {
        st = KRB5_KDB_ACCESS_ERROR;
        krb5_set_error_message (context, st,
                                gettext("Certificate based authentication requested but "
                                "not supported by LDAP servers"));
    }
    return (st);
}


/*
 * get a single handle. Do not lock the mutex
 */

krb5_error_code
krb5_ldap_db_single_init(krb5_ldap_context *ldap_context)
{
    krb5_error_code             st=0;
    int                         cnt=0;
    krb5_ldap_server_info       *server_info=NULL;

    while (ldap_context->server_info_list[cnt] != NULL) {
        server_info = ldap_context->server_info_list[cnt];
        if ((server_info->server_status == NOTSET || server_info->server_status == ON)) {
            if (server_info->num_conns < ldap_context->max_server_conns-1) {
                st = krb5_ldap_initialize(ldap_context, server_info);
                if (st == LDAP_SUCCESS)
                    goto cleanup;
            }
        }
        ++cnt;
    }

    /* If we are here, try to connect to all the servers */

    cnt = 0;
    while (ldap_context->server_info_list[cnt] != NULL) {
        server_info = ldap_context->server_info_list[cnt];
        st = krb5_ldap_initialize(ldap_context, server_info);
        if (st == LDAP_SUCCESS)
            goto cleanup;
        ++cnt;
    }
cleanup:
    return (st);
}

krb5_error_code
krb5_ldap_rebind(ldap_context, ldap_server_handle)
    krb5_ldap_context           *ldap_context;
    krb5_ldap_server_handle     **ldap_server_handle;
{
    krb5_ldap_server_handle     *handle = *ldap_server_handle;
    int                         use_ssl;

    /*
     * Solaris Kerberos: use SSL unless ldapi (unix domain sockets is specified)
     */
    if (strncasecmp(handle->server_info->server_name, "ldapi:", 6) == 0)
        use_ssl = SSL_OFF;
    else
        use_ssl = SSL_ON;

    if ((ldap_initialize(&handle->ldap_handle, handle->server_info->server_name,
                use_ssl, NULL) != LDAP_SUCCESS)
        || (krb5_ldap_bind(ldap_context, handle) != LDAP_SUCCESS))
        return krb5_ldap_request_next_handle_from_pool(ldap_context, ldap_server_handle);
    return LDAP_SUCCESS;
}

/*
 *     DAL API functions
 */
krb5_error_code krb5_ldap_lib_init()
{
    return 0;
}

krb5_error_code krb5_ldap_lib_cleanup()
{
    /* right now, no cleanup required */
    return 0;
}

krb5_error_code
krb5_ldap_free_ldap_context(krb5_ldap_context *ldap_context)
{
    if (ldap_context == NULL)
        return 0;

    krb5_ldap_free_krbcontainer_params(ldap_context->krbcontainer);
    ldap_context->krbcontainer = NULL;

    krb5_ldap_free_realm_params(ldap_context->lrparams);
    ldap_context->lrparams = NULL;

    krb5_ldap_free_server_params(ldap_context);

    return 0;
}

krb5_error_code
krb5_ldap_close(krb5_context context)
{
    kdb5_dal_handle  *dal_handle=NULL;
    krb5_ldap_context *ldap_context=NULL;

    if (context == NULL ||
        context->db_context == NULL ||
        ((kdb5_dal_handle *)context->db_context)->db_context == NULL)
        return 0;

    dal_handle = (kdb5_dal_handle *) context->db_context;
    ldap_context = (krb5_ldap_context *) dal_handle->db_context;
    dal_handle->db_context = NULL;

    krb5_ldap_free_ldap_context(ldap_context);

    return 0;
}