/* -*- mode: c; c-basic-offset: 4; indent-tabs-mode: nil -*- */
/* plugins/kdb/ldap/libkdb_ldap/kdb_ldap.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 <ctype.h>
#include "kdb_ldap.h"
#include "ldap_misc.h"
#include <kdb5.h>
#include <kadm5/admin.h>

#if !defined(isblank) && defined(HAVE_ISBLANK)
#if defined(NEED_ISBLANK_PROTO)
extern int isblank();
#endif
#else /* isblank missing */
#if !defined(isblank)
#define isblank isspace
#endif
#endif

krb5_error_code
krb5_ldap_get_db_opt(char *input, char **opt, char **val)
{
    char *pos = strchr(input, '=');

    *val = NULL;
    if (pos == NULL) {
        *opt = strdup(input);
        if (*opt == NULL) {
            return ENOMEM;
        }
    } else {
        int len = pos - input;
        *opt = malloc((unsigned) len + 1);
        if (!*opt) {
            return ENOMEM;
        }
        memcpy(*opt, input, (unsigned) len);
        /* ignore trailing blanks */
        while (isblank((*opt)[len-1]))
            --len;
        (*opt)[len] = '\0';

        pos += 1; /* move past '=' */
        while (isblank(*pos))  /* ignore leading blanks */
            pos += 1;
        if (*pos != '\0') {
            *val = strdup (pos);
            if (!*val) {
                free (*opt);
                return ENOMEM;
            }
        }
    }
    return (0);

}


/*
 * ldap get age
 */
krb5_error_code
krb5_ldap_get_age(context, db_name, age)
    krb5_context context;
    char *db_name;
    time_t *age;
{
    time (age);
    return 0;
}

/*
 * read startup information - kerberos and realm container
 */
krb5_error_code
krb5_ldap_read_startup_information(krb5_context context)
{
    krb5_error_code      retval = 0;
    kdb5_dal_handle      *dal_handle=NULL;
    krb5_ldap_context    *ldap_context=NULL;
    int                  mask = 0;

    SETUP_CONTEXT();
    if ((retval=krb5_ldap_read_krbcontainer_params(context, &(ldap_context->krbcontainer)))) {
        prepend_err_str(context, _("Unable to read Kerberos container"),
                        retval, retval);
        goto cleanup;
    }

    if ((retval=krb5_ldap_read_realm_params(context, context->default_realm, &(ldap_context->lrparams), &mask))) {
        prepend_err_str(context, _("Unable to read Realm"), retval, retval);
        goto cleanup;
    }

    if (((mask & LDAP_REALM_MAXTICKETLIFE) == 0) || ((mask & LDAP_REALM_MAXRENEWLIFE) == 0)
        || ((mask & LDAP_REALM_KRBTICKETFLAGS) == 0)) {
        kadm5_config_params  params_in, params_out;

        memset(&params_in, 0, sizeof(params_in));
        memset(&params_out, 0, sizeof(params_out));

        retval = kadm5_get_config_params(context, 1, &params_in, &params_out);
        if (retval) {
            if ((mask & LDAP_REALM_MAXTICKETLIFE) == 0) {
                ldap_context->lrparams->max_life = 24 * 60 * 60; /* 1 day */
            }
            if ((mask & LDAP_REALM_MAXRENEWLIFE) == 0) {
                ldap_context->lrparams->max_renewable_life = 0;
            }
            if ((mask & LDAP_REALM_KRBTICKETFLAGS) == 0) {
                ldap_context->lrparams->tktflags = KRB5_KDB_DEF_FLAGS;
            }
            retval = 0;
            goto cleanup;
        }

        if ((mask & LDAP_REALM_MAXTICKETLIFE) == 0) {
            if (params_out.mask & KADM5_CONFIG_MAX_LIFE)
                ldap_context->lrparams->max_life = params_out.max_life;
        }

        if ((mask & LDAP_REALM_MAXRENEWLIFE) == 0) {
            if (params_out.mask & KADM5_CONFIG_MAX_RLIFE)
                ldap_context->lrparams->max_renewable_life = params_out.max_rlife;
        }

        if ((mask & LDAP_REALM_KRBTICKETFLAGS) == 0) {
            if (params_out.mask & KADM5_CONFIG_FLAGS)
                ldap_context->lrparams->tktflags = params_out.flags;
        }

        kadm5_free_config_params(context, &params_out);
    }

cleanup:
    return retval;
}


/*
 * Interrogate the root DSE (zero length DN) for an attribute
 * value assertion.
 */
static int
has_rootdse_ava(krb5_context context, char *ldap_server, char *attribute,
                char *value)
{
    int               i=0, flag=0, ret=0, retval=0;
    char              *attrs[2], **values=NULL;
    LDAP              *ld=NULL;
    LDAPMessage       *msg=NULL, *res=NULL;
    struct berval     cred;

    attrs[0] = attribute;
    attrs[1] = NULL;

    retval = ldap_initialize(&ld, ldap_server);
    if (retval != LDAP_SUCCESS) {
        ret = 2; /* Don't know */
        goto cleanup;
    }

    cred.bv_val = "";
    cred.bv_len = 0;

    /* Anonymous bind */
    retval = ldap_sasl_bind_s(ld, "", NULL, &cred, NULL, NULL, NULL);
    if (retval != LDAP_SUCCESS) {
        ret = 2; /* Don't know */
        goto cleanup;
    }

    retval = ldap_search_ext_s(ld, "", LDAP_SCOPE_BASE, NULL, attrs, 0, NULL, NULL, NULL, 0, &res);
    if (retval != LDAP_SUCCESS) {
        ret = 2; /* Don't know */
        goto cleanup;
    }

    msg = ldap_first_message(ld, res);
    if (msg == NULL) {
        ret = 2; /* Don't know */
        goto cleanup;
    }

    values = ldap_get_values(ld, msg, attribute);
    if (values == NULL) {
        ret = 1; /* Not supported */
        goto cleanup;
    }

    for (i = 0; values[i] != NULL; i++) {
        if (strcmp(values[i], value) == 0) {
            flag = 1;
            break;
        }
    }

    if (flag != 1) {
        ret = 1; /* Not supported */
        goto cleanup;
    }

cleanup:

    if (values != NULL)
        ldap_value_free(values);

    if (res != NULL)
        ldap_msgfree(res);

    if (ld != NULL)
        ldap_unbind_ext_s(ld, NULL, NULL);

    return ret;
}

#define ERR_MSG1 _("Unable to check if SASL EXTERNAL mechanism is supported by LDAP server. Proceeding anyway ...")
#define ERR_MSG2 _("SASL EXTERNAL mechanism not supported by LDAP server. Can't perform certificate-based bind.")

/* Function to check if a LDAP server supports the SASL external mechanism
 *Return values:
 *   0 => supports
 *   1 => does not support
 *   2 => don't know
 */
int
has_sasl_external_mech(krb5_context context, char *ldap_server)
{
    int ret;

    ret = has_rootdse_ava(context, ldap_server,
                          "supportedSASLMechanisms", "EXTERNAL");
    switch (ret) {
    case 1: /* not supported */
        krb5_set_error_message(context, 1, "%s", ERR_MSG2);
        break;
    case 2: /* don't know */
        krb5_set_error_message(context, 1, "%s", ERR_MSG1);
        break;
    default:
        break;
    }

    return ret;
}

int
has_modify_increment(context, ldap_server)
    krb5_context     context;
    char             *ldap_server;
{
    return has_rootdse_ava(context, ldap_server,
                           "supportedFeatures", "1.3.6.1.1.14");
}

void *
krb5_ldap_alloc(krb5_context context, void *ptr, size_t size)
{
    return realloc(ptr, size);
}

void
krb5_ldap_free(krb5_context context, void *ptr)
{
    free(ptr);
}

krb5_error_code
krb5_ldap_open(krb5_context context, char *conf_section, char **db_args,
               int mode)
{
    krb5_error_code status  = 0;
    char **t_ptr = db_args;
    krb5_ldap_context *ldap_context=NULL;
    int srv_cnt = 0;
    kdb5_dal_handle *dal_handle=NULL;

    /* Clear the global error string */
    krb5_clear_error_message(context);

    ldap_context = calloc(1, sizeof(krb5_ldap_context));
    if (ldap_context == NULL) {
        status = ENOMEM;
        goto clean_n_exit;
    }

    ldap_context->kcontext = context;

    while (t_ptr && *t_ptr) {
        char *opt = NULL, *val = NULL;

        if ((status = krb5_ldap_get_db_opt(*t_ptr, &opt, &val)) != 0) {
            goto clean_n_exit;
        }
        if (opt && !strcmp(opt, "binddn")) {
            if (ldap_context->bind_dn) {
                free (opt);
                free (val);
                status = EINVAL;
                krb5_set_error_message(context, status, _("'binddn' missing"));
                goto clean_n_exit;
            }
            if (val == NULL) {
                status = EINVAL;
                krb5_set_error_message(context, status,
                                       _("'binddn' value missing"));
                free(opt);
                goto clean_n_exit;
            }
            ldap_context->bind_dn = strdup(val);
            if (ldap_context->bind_dn == NULL) {
                free (opt);
                free (val);
                status = ENOMEM;
                goto clean_n_exit;
            }
        } else if (opt && !strcmp(opt, "nconns")) {
            if (ldap_context->max_server_conns) {
                free (opt);
                free (val);
                status = EINVAL;
                krb5_set_error_message(context, status, _("'nconns' missing"));
                goto clean_n_exit;
            }
            if (val == NULL) {
                status = EINVAL;
                krb5_set_error_message(context, status,
                                       _("'nconns' value missing"));
                free(opt);
                goto clean_n_exit;
            }
            ldap_context->max_server_conns = atoi(val) ? atoi(val) : DEFAULT_CONNS_PER_SERVER;
        } else if (opt && !strcmp(opt, "bindpwd")) {
            if (ldap_context->bind_pwd) {
                free (opt);
                free (val);
                status = EINVAL;
                krb5_set_error_message(context, status,
                                       _("'bindpwd' missing"));
                goto clean_n_exit;
            }
            if (val == NULL) {
                status = EINVAL;
                krb5_set_error_message(context, status,
                                       _("'bindpwd' value missing"));
                free(opt);
                goto clean_n_exit;
            }
            ldap_context->bind_pwd = strdup(val);
            if (ldap_context->bind_pwd == NULL) {
                free (opt);
                free (val);
                status = ENOMEM;
                goto clean_n_exit;
            }
        } else if (opt && !strcmp(opt, "host")) {
            if (val == NULL) {
                status = EINVAL;
                krb5_set_error_message(context, status,
                                       _("'host' value missing"));
                free(opt);
                goto clean_n_exit;
            }
            if (ldap_context->server_info_list == NULL)
                ldap_context->server_info_list = (krb5_ldap_server_info **) calloc (SERV_COUNT+1, sizeof (krb5_ldap_server_info *)) ;

            if (ldap_context->server_info_list == NULL) {
                free (opt);
                free (val);
                status = ENOMEM;
                goto clean_n_exit;
            }

            ldap_context->server_info_list[srv_cnt] = (krb5_ldap_server_info *) calloc (1, sizeof (krb5_ldap_server_info));
            if (ldap_context->server_info_list[srv_cnt] == NULL) {
                free (opt);
                free (val);
                status = ENOMEM;
                goto clean_n_exit;
            }

            ldap_context->server_info_list[srv_cnt]->server_status = NOTSET;

            ldap_context->server_info_list[srv_cnt]->server_name = strdup(val);
            if (ldap_context->server_info_list[srv_cnt]->server_name == NULL) {
                free (opt);
                free (val);
                status = ENOMEM;
                goto clean_n_exit;
            }

            srv_cnt++;
#ifdef HAVE_EDIRECTORY
        } else if (opt && !strcmp(opt, "cert")) {
            if (val == NULL) {
                status = EINVAL;
                krb5_set_error_message(context, status,
                                       _("'cert' value missing"));
                free(opt);
                goto clean_n_exit;
            }

            if (ldap_context->root_certificate_file == NULL) {
                ldap_context->root_certificate_file = strdup(val);
                if (ldap_context->root_certificate_file == NULL) {
                    free (opt);
                    free (val);
                    status = ENOMEM;
                    goto clean_n_exit;
                }
            } else {
                char *newstr;

                if (asprintf(&newstr, "%s %s",
                             ldap_context->root_certificate_file, val) < 0) {
                    free (opt);
                    free (val);
                    status = ENOMEM;
                    goto clean_n_exit;
                }
                free(ldap_context->root_certificate_file);
                ldap_context->root_certificate_file = newstr;
            }
#endif
        } else {
            /* ignore hash argument. Might have been passed from create */
            status = EINVAL;
            if (opt && !strcmp(opt, "temporary")) {
                /*
                 * temporary is passed in when kdb5_util load without -update is done.
                 * This is unsupported by the LDAP plugin.
                 */
                krb5_set_error_message(context, status,
                                       _("open of LDAP directory aborted, "
                                         "plugin requires -update argument"));
            } else {
                krb5_set_error_message (context, status,
                                        _("unknown option \'%s\'"),
                                        opt?opt:val);
            }
            free(opt);
            free(val);
            goto clean_n_exit;
        }

        free(opt);
        free(val);
        t_ptr++;
    }

    dal_handle = context->dal_handle;
    dal_handle->db_context = ldap_context;
    status = krb5_ldap_read_server_params(context, conf_section, mode & 0x0300);
    if (status) {
        if (ldap_context)
            krb5_ldap_free_ldap_context(ldap_context);
        ldap_context = NULL;
        dal_handle->db_context = NULL;
        prepend_err_str(context, _("Error reading LDAP server params: "),
                        status, status);
        goto clean_n_exit;
    }
    if ((status=krb5_ldap_db_init(context, ldap_context)) != 0) {
        goto clean_n_exit;
    }

    if ((status=krb5_ldap_read_startup_information(context)) != 0) {
        goto clean_n_exit;
    }

clean_n_exit:
    /* may be clearing up is not required  db_fini might do it for us, check out */
    if (status) {
        krb5_ldap_close(context);
    }
    return status;
}

#include "ldap_err.h"
int
set_ldap_error(krb5_context ctx, int st, int op)
{
    int translated_st = translate_ldap_error(st, op);
    krb5_set_error_message(ctx, translated_st, "%s", ldap_err2string(st));
    return translated_st;
}

void
prepend_err_str(krb5_context ctx, const char *str, krb5_error_code err,
                krb5_error_code oerr)
{
    const char *omsg;
    if (oerr == 0) oerr = err;
    omsg = krb5_get_error_message (ctx, err);
    krb5_set_error_message (ctx, err, "%s %s", str, omsg);
}

extern krb5int_access accessor;
MAKE_INIT_FUNCTION(kldap_init_fn);

int
kldap_init_fn(void)
{
    /* Global (per-module) initialization.  */
    return krb5int_accessor (&accessor, KRB5INT_ACCESS_VERSION);
}

int
kldap_ensure_initialized(void)
{
    return CALL_INIT_FUNCTION (kldap_init_fn);
}

krb5_error_code
krb5_ldap_check_policy_as(krb5_context kcontext, krb5_kdc_req *request,
                          krb5_db_entry *client, krb5_db_entry *server,
                          krb5_timestamp kdc_time, const char **status,
                          krb5_pa_data ***e_data)
{
    krb5_error_code retval;

    retval = krb5_ldap_lockout_check_policy(kcontext, client, kdc_time);
    if (retval == KRB5KDC_ERR_CLIENT_REVOKED)
        *status = "LOCKED_OUT";
    return retval;
}

void
krb5_ldap_audit_as_req(krb5_context kcontext, krb5_kdc_req *request,
                       krb5_db_entry *client, krb5_db_entry *server,
                       krb5_timestamp authtime, krb5_error_code error_code)
{
    (void) krb5_ldap_lockout_audit(kcontext, client, authtime, error_code);
}

krb5_error_code
krb5_ldap_check_allowed_to_delegate(krb5_context context,
                                    krb5_const_principal client,
                                    const krb5_db_entry *server,
                                    krb5_const_principal proxy)
{
    krb5_error_code code;
    krb5_tl_data *tlp;

    code = KRB5KDC_ERR_POLICY;

    for (tlp = server->tl_data; tlp != NULL; tlp = tlp->tl_data_next) {
        krb5_principal acl;

        if (tlp->tl_data_type != KRB5_TL_CONSTRAINED_DELEGATION_ACL)
            continue;

        if (krb5_parse_name(context, (char *)tlp->tl_data_contents, &acl) != 0)
            continue;

        if (krb5_principal_compare(context, proxy, acl)) {
            code = 0;
            krb5_free_principal(context, acl);
            break;
        }
        krb5_free_principal(context, acl);
    }

    return code;
}
