/*
    SSSD

    Kerberos 5 Backend Module -- Utilities

    Authors:
        Sumit Bose <sbose@redhat.com>

    Copyright (C) 2009 Red Hat

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#include <string.h>
#include <stdlib.h>
#include <libgen.h>

#include "providers/krb5/krb5_utils.h"
#include "providers/krb5/krb5_ccache.h"
#include "providers/krb5/krb5_auth.h"
#include "src/util/find_uid.h"
#include "util/util.h"

errno_t find_or_guess_upn(TALLOC_CTX *mem_ctx, struct ldb_message *msg,
                          struct krb5_ctx *krb5_ctx,
                          struct sss_domain_info *dom, const char *user,
                          const char *user_dom, char **_upn)
{
    const char *upn = NULL;
    int ret;

    if (krb5_ctx == NULL || dom == NULL || user == NULL || _upn == NULL) {
        return EINVAL;
    }

    if (msg != NULL) {
        upn = ldb_msg_find_attr_as_string(msg, SYSDB_CANONICAL_UPN, NULL);
        if (upn != NULL) {
            ret = EOK;
            goto done;
        }

        upn = ldb_msg_find_attr_as_string(msg, SYSDB_UPN, NULL);
        if (upn != NULL) {
            ret = EOK;
            goto done;
        }
    }

    ret = krb5_get_simple_upn(mem_ctx, krb5_ctx, dom, user,
                              user_dom, _upn);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "krb5_get_simple_upn failed.\n");
        return ret;
    }

done:
    if (ret == EOK && upn != NULL) {
        *_upn = talloc_strdup(mem_ctx, upn);
        if (*_upn == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, "talloc_strdup failed.\n");
            return ENOMEM;
        }
    }

    return ret;
}

errno_t check_if_cached_upn_needs_update(struct sysdb_ctx *sysdb,
                                         struct sss_domain_info *domain,
                                         const char *user,
                                         const char *upn)
{
    TALLOC_CTX *tmp_ctx;
    int ret;
    int sret;
    const char *attrs[] = {SYSDB_UPN, SYSDB_CANONICAL_UPN, NULL};
    struct sysdb_attrs *new_attrs;
    struct ldb_result *res;
    bool in_transaction = false;
    const char *cached_upn;
    const char *cached_canonical_upn;

    if (sysdb == NULL || user == NULL || upn == NULL) {
        return EINVAL;
    }

    tmp_ctx = talloc_new(NULL);
    if (tmp_ctx == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "talloc_new failed.\n");
        return ENOMEM;
    }

    ret = sysdb_get_user_attr(tmp_ctx, domain, user, attrs, &res);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "sysdb_get_user_attr failed.\n");
        goto done;
    }

    if (res->count != 1) {
        DEBUG(SSSDBG_OP_FAILURE, "[%d] user objects for name [%s] found, " \
                                  "expected 1.\n", res->count, user);
        ret = EINVAL;
        goto done;
    }

    cached_upn = ldb_msg_find_attr_as_string(res->msgs[0], SYSDB_UPN, NULL);

    if (cached_upn != NULL && strcmp(cached_upn, upn) == 0) {
        DEBUG(SSSDBG_TRACE_ALL, "Cached UPN and new one match, "
                                 "nothing to do.\n");
        ret = EOK;
        goto done;
    }

    cached_canonical_upn = ldb_msg_find_attr_as_string(res->msgs[0],
                                                       SYSDB_CANONICAL_UPN,
                                                       NULL);

    if (cached_canonical_upn != NULL
            && strcmp(cached_canonical_upn, upn) == 0) {
        DEBUG(SSSDBG_TRACE_ALL, "Cached canonical UPN and new one match, "
                                 "nothing to do.\n");
        ret = EOK;
        goto done;
    }

    DEBUG(SSSDBG_TRACE_LIBS, "Replacing canonical UPN [%s] with [%s] " \
                              "for user [%s].\n",
                              cached_canonical_upn == NULL ?
                                                 "empty" : cached_canonical_upn,
                              upn, user);

    new_attrs = sysdb_new_attrs(tmp_ctx);
    if (new_attrs == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "sysdb_new_attrs failed.\n");
        ret = ENOMEM;
        goto done;
    }

    ret = sysdb_attrs_add_string(new_attrs, SYSDB_CANONICAL_UPN, upn);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "sysdb_attrs_add_string failed.\n");
        goto done;
    }

    ret = sysdb_transaction_start(sysdb);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Error %d starting transaction (%s)\n", ret, strerror(ret));
        goto done;
    }
    in_transaction = true;

    ret = sysdb_set_entry_attr(sysdb, res->msgs[0]->dn, new_attrs,
                               cached_canonical_upn == NULL ? SYSDB_MOD_ADD :
                                                              SYSDB_MOD_REP);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "sysdb_set_entry_attr failed [%d][%s].\n",
                                  ret, strerror(ret));
        goto done;
    }

    ret = sysdb_transaction_commit(sysdb);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Failed to commit transaction!\n");
        goto done;
    }
    in_transaction = false;

    ret = EOK;

done:
    if (in_transaction) {
        sret = sysdb_transaction_cancel(sysdb);
        if (sret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE, "Failed to cancel transaction\n");
        }
    }

    talloc_free(tmp_ctx);

    return ret;
}

#define S_EXP_UID "{uid}"
#define L_EXP_UID (sizeof(S_EXP_UID) - 1)
#define S_EXP_USERID "{USERID}"
#define L_EXP_USERID (sizeof(S_EXP_USERID) - 1)
#define S_EXP_EUID "{euid}"
#define L_EXP_EUID (sizeof(S_EXP_EUID) - 1)
#define S_EXP_USERNAME "{username}"
#define L_EXP_USERNAME (sizeof(S_EXP_USERNAME) - 1)

static errno_t
check_ccache_re(const char *filename, pcre *illegal_re)
{
    errno_t ret;

    ret = pcre_exec(illegal_re, NULL, filename, strlen(filename),
                    0, 0, NULL, 0);
    if (ret == 0) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Illegal pattern in ccache directory name [%s].\n", filename);
        return EINVAL;
    } else if (ret == PCRE_ERROR_NOMATCH) {
        DEBUG(SSSDBG_TRACE_LIBS,
              "Ccache directory name [%s] does not contain "
               "illegal patterns.\n", filename);
        return EOK;
    }

    DEBUG(SSSDBG_CRIT_FAILURE, "pcre_exec failed [%d].\n", ret);
    return EFAULT;
}

char *expand_ccname_template(TALLOC_CTX *mem_ctx, struct krb5child_req *kr,
                             const char *template, pcre *illegal_re,
                             bool file_mode, bool case_sensitive)
{
    char *copy;
    char *p;
    char *n;
    char *result = NULL;
    char *dummy;
    char *name;
    char *res = NULL;
    const char *cache_dir_tmpl;
    TALLOC_CTX *tmp_ctx = NULL;
    char action;
    bool rerun;
    int ret;

    if (template == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Missing template.\n");
        return NULL;
    }

    tmp_ctx = talloc_new(NULL);
    if (!tmp_ctx) return NULL;

    copy = talloc_strdup(tmp_ctx, template);
    if (copy == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed.\n");
        goto done;
    }

    result = talloc_strdup(tmp_ctx, "");
    if (result == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_strdup failed.\n");
        goto done;
    }

    p = copy;
    while ( (n = strchr(p, '%')) != NULL) {
        *n = '\0';
        n++;
        if ( *n == '\0' ) {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "format error, single %% at the end of the template.\n");
            goto done;
        }

        rerun = true;
        action = *n;
        while (rerun) {
            rerun = false;
            switch (action) {
            case 'u':
                if (kr->pd->user == NULL) {
                    DEBUG(SSSDBG_CRIT_FAILURE,
                          "Cannot expand user name template "
                          "because user name is empty.\n");
                    goto done;
                }

                name = sss_output_name(tmp_ctx, kr->pd->user, case_sensitive, 0);
                if (name == NULL) {
                    DEBUG(SSSDBG_CRIT_FAILURE,
                          "sss_get_cased_name failed\n");
                    goto done;
                }

                result = talloc_asprintf_append(result, "%s%s", p,
                                                name);
                break;
            case 'U':
                if (kr->uid <= 0) {
                    DEBUG(SSSDBG_CRIT_FAILURE, "Cannot expand uid template "
                              "because uid is invalid.\n");
                    goto done;
                }
                result = talloc_asprintf_append(result, "%s%"SPRIuid, p,
                                                kr->uid);
                break;
            case 'p':
                if (kr->upn == NULL) {
                    DEBUG(SSSDBG_CRIT_FAILURE,
                          "Cannot expand user principal name template "
                              "because upn is empty.\n");
                    goto done;
                }
                result = talloc_asprintf_append(result, "%s%s", p, kr->upn);
                break;
            case '%':
                result = talloc_asprintf_append(result, "%s%%", p);
                break;
            case 'r':
                dummy = dp_opt_get_string(kr->krb5_ctx->opts, KRB5_REALM);
                if (dummy == NULL) {
                    DEBUG(SSSDBG_CRIT_FAILURE, "Missing kerberos realm.\n");
                    goto done;
                }
                result = talloc_asprintf_append(result, "%s%s", p, dummy);
                break;
            case 'h':
                if (kr->homedir == NULL) {
                    DEBUG(SSSDBG_CRIT_FAILURE,
                          "Cannot expand home directory template "
                              "because the path is not available.\n");
                    goto done;
                }
                result = talloc_asprintf_append(result, "%s%s", p, kr->homedir);
                break;
            case 'd':
                if (file_mode) {
                    cache_dir_tmpl = dp_opt_get_string(kr->krb5_ctx->opts,
                                                       KRB5_CCACHEDIR);
                    if (cache_dir_tmpl == NULL) {
                        DEBUG(SSSDBG_CRIT_FAILURE,
                              "Missing credential cache directory.\n");
                        goto done;
                    }

                    dummy = expand_ccname_template(tmp_ctx, kr, cache_dir_tmpl,
                                                   illegal_re, false, case_sensitive);
                    if (dummy == NULL) {
                        DEBUG(SSSDBG_CRIT_FAILURE,
                              "Expanding credential cache directory "
                                  "template failed.\n");
                        goto done;
                    }
                    result = talloc_asprintf_append(result, "%s%s", p, dummy);
                    talloc_zfree(dummy);
                } else {
                    DEBUG(SSSDBG_CRIT_FAILURE,
                          "'%%d' is not allowed in this template.\n");
                    goto done;
                }
                break;
            case 'P':
                if (!file_mode) {
                    DEBUG(SSSDBG_CRIT_FAILURE,
                          "'%%P' is not allowed in this template.\n");
                    goto done;
                }
                if (kr->pd->cli_pid == 0) {
                    DEBUG(SSSDBG_CRIT_FAILURE, "Cannot expand PID template "
                              "because PID is not available.\n");
                    goto done;
                }
                result = talloc_asprintf_append(result, "%s%d", p,
                                                kr->pd->cli_pid);
                break;

            /* Additional syntax from krb5.conf default_ccache_name */
            case '{':
                if (strncmp(n , S_EXP_UID, L_EXP_UID) == 0) {
                    action = 'U';
                    n += L_EXP_UID - 1;
                    rerun = true;
                    continue;
                } else if (strncmp(n , S_EXP_USERID, L_EXP_USERID) == 0) {
                    action = 'U';
                    n += L_EXP_USERID - 1;
                    rerun = true;
                    continue;
                } else if (strncmp(n , S_EXP_EUID, L_EXP_EUID) == 0) {
                    /* SSSD does not distinguish between uid and euid,
                     * so we treat both the same way */
                    action = 'U';
                    n += L_EXP_EUID - 1;
                    rerun = true;
                    continue;
                } else if (strncmp(n , S_EXP_USERNAME, L_EXP_USERNAME) == 0) {
                    action = 'u';
                    n += L_EXP_USERNAME - 1;
                    rerun = true;
                    continue;
                } else {
                    /* ignore any expansion variable we do not understand and
                     * let libkrb5 hndle it or fail */
                    name = n;
                    n = strchr(name, '}');
                    if (!n) {
                        DEBUG(SSSDBG_CRIT_FAILURE,
                              "Invalid substitution sequence in cache "
                              "template. Missing closing '}' in [%s].\n",
                              template);
                        goto done;
                    }
                    result = talloc_asprintf_append(result, "%s%%%.*s", p,
                                                    (int)(n - name + 1), name);
                }
                break;
            default:
                DEBUG(SSSDBG_CRIT_FAILURE,
                      "format error, unknown template [%%%c].\n", *n);
                goto done;
            }
        }

        if (result == NULL) {
            DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf_append failed.\n");
            goto done;
        }

        p = n + 1;
    }

    result = talloc_asprintf_append(result, "%s", p);
    if (result == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_asprintf_append failed.\n");
        goto done;
    }

    if (illegal_re != NULL) {
        ret = check_ccache_re(result, illegal_re);
        if (ret != EOK) {
            goto done;
        }
    }

    res = talloc_move(mem_ctx, &result);
done:
    talloc_zfree(tmp_ctx);
    return res;
}

errno_t get_domain_or_subdomain(struct be_ctx *be_ctx,
                                char *domain_name,
                                struct sss_domain_info **dom)
{

    if (domain_name != NULL &&
        strcasecmp(domain_name, be_ctx->domain->name) != 0) {
        *dom = find_domain_by_name(be_ctx->domain, domain_name, true);
        if (*dom == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, "find_domain_by_name failed.\n");
            return ENOMEM;
        }
    } else {
        *dom = be_ctx->domain;
    }

    return EOK;
}

static errno_t split_tuple(TALLOC_CTX *mem_ctx, const char *tuple,
                           const char **_first, const char **_second)
{
    errno_t ret;
    char **list;
    int n;

    ret = split_on_separator(mem_ctx, tuple, ':', true, true, &list, &n);

    if (ret != EOK) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              "split_on_separator failed - %s:[%d]\n",
              sss_strerror(ret), ret);
        goto done;
    } else if (n != 2) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              "split_on_separator failed - Expected format is: "
              "'username:primary' but got: '%s'.\n", tuple);
        ret = EINVAL;
        goto done;
    }

    *_first = list[0];
    *_second = list[1];

done:
    return ret;
}

static errno_t
fill_name_to_primary_map(TALLOC_CTX *mem_ctx, char **map,
                         struct map_id_name_to_krb_primary *name_to_primary,
                         size_t size)
{
    int i;
    errno_t ret;

    for (i = 0; i < size; i++) {
        ret = split_tuple(mem_ctx, map[i],
                          &name_to_primary[i].id_name,
                          &name_to_primary[i].krb_primary);
        if (ret != EOK) {
            DEBUG(SSSDBG_MINOR_FAILURE,
                  "split_tuple failed - %s:[%d]\n", sss_strerror(ret), ret);
            goto done;
        }
    }

    ret = EOK;

done:
    return ret;
}

errno_t
parse_krb5_map_user(TALLOC_CTX *mem_ctx,
                    const char *krb5_map_user,
                    const char *dom_name,
                    struct map_id_name_to_krb_primary **_name_to_primary)
{
    int size;
    char **map;
    errno_t ret;
    TALLOC_CTX *tmp_ctx;
    struct map_id_name_to_krb_primary *name_to_primary;

    tmp_ctx = talloc_new(NULL);
    if (tmp_ctx == NULL) {
        ret = ENOMEM;
        goto done;
    }

    if (krb5_map_user == NULL || strlen(krb5_map_user) == 0) {
        DEBUG(SSSDBG_CONF_SETTINGS, "krb5_map_user is empty!\n");
        size = 0;
    } else {
        ret = split_on_separator(tmp_ctx, krb5_map_user, ',', true, true,
                                 &map, &size);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Failed to parse krb5_map_user!\n");
            goto done;
        }
    }

    name_to_primary = talloc_zero_array(tmp_ctx,
                                        struct map_id_name_to_krb_primary,
                                        size + 1);
    if (name_to_primary == NULL) {
        ret = ENOMEM;
        goto done;
    }
    /* sentinel */
    name_to_primary[size].id_name = NULL;
    name_to_primary[size].krb_primary = NULL;

    if (size > 0) {
        ret = fill_name_to_primary_map(name_to_primary, map, name_to_primary,
                                       size);
        if (ret != EOK) {
            DEBUG(SSSDBG_MINOR_FAILURE,
                  "fill_name_to_primary_map failed: %s:[%d]\n",
                  sss_strerror(ret), ret);
            goto done;
        }
    }

    /* conversion names to fully-qualified names */
    for (int i = 0; i < size; i++) {
        name_to_primary[i].id_name = sss_create_internal_fqname(
                                                     name_to_primary,
                                                     name_to_primary[i].id_name,
                                                     dom_name);
        if (name_to_primary[i].id_name == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, "sss_create_internal_fqname failed\n");
            ret = ENOMEM;
            goto done;
        }

        name_to_primary[i].krb_primary = sss_create_internal_fqname(
                                                 name_to_primary,
                                                 name_to_primary[i].krb_primary,
                                                 dom_name);
        if (name_to_primary[i].krb_primary == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, "sss_create_internal_fqname failed\n");
            ret = ENOMEM;
            goto done;
        }
    }
    ret = EOK;

done:
    if (ret == EOK) {
        *_name_to_primary = talloc_steal(mem_ctx, name_to_primary);
    }
    talloc_free(tmp_ctx);
    return ret;
}
