/*
    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_auth.h"
#include "src/util/find_uid.h"
#include "util/util.h"

char *expand_ccname_template(TALLOC_CTX *mem_ctx, struct krb5child_req *kr,
                             const char *template, bool file_mode,
                             bool case_sensitive, bool *private_path)
{
    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;

    *private_path = false;

    if (template == NULL) {
        DEBUG(1, ("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(1, ("talloc_strdup failed.\n"));
        goto done;
    }

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

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

        switch( *n ) {
            case 'u':
                if (kr->pd->user == NULL) {
                    DEBUG(1, ("Cannot expand user name template "
                              "because user name is empty.\n"));
                    goto done;
                }
                name = sss_get_cased_name(tmp_ctx, kr->pd->user,
                                          case_sensitive);
                if (!name) {
                    DEBUG(SSSDBG_CRIT_FAILURE,
                          ("sss_get_cased_name failed\n"));
                    goto done;
                }

                result = talloc_asprintf_append(result, "%s%s", p,
                                                name);
                if (!file_mode) *private_path = true;
                break;
            case 'U':
                if (kr->uid <= 0) {
                    DEBUG(1, ("Cannot expand uid template "
                              "because uid is invalid.\n"));
                    goto done;
                }
                result = talloc_asprintf_append(result, "%s%d", p,
                                                kr->uid);
                if (!file_mode) *private_path = true;
                break;
            case 'p':
                if (kr->upn == NULL) {
                    DEBUG(1, ("Cannot expand user principal name template "
                              "because upn is empty.\n"));
                    goto done;
                }
                result = talloc_asprintf_append(result, "%s%s", p, kr->upn);
                if (!file_mode) *private_path = true;
                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(1, ("Missing kerberos realm.\n"));
                    goto done;
                }
                result = talloc_asprintf_append(result, "%s%s", p, dummy);
                break;
            case 'h':
                if (kr->homedir == NULL) {
                    DEBUG(1, ("Cannot expand home directory template "
                              "because the path is not available.\n"));
                    goto done;
                }
                result = talloc_asprintf_append(result, "%s%s", p, kr->homedir);
                if (!file_mode) *private_path = true;
                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(1, ("Missing credential cache directory.\n"));
                        goto done;
                    }

                    dummy = expand_ccname_template(tmp_ctx, kr, cache_dir_tmpl,
                                                   false, case_sensitive,
                                                   private_path);
                    if (dummy == NULL) {
                        DEBUG(1, ("Expanding credential cache directory "
                                  "template failed.\n"));
                        goto done;
                    }
                    result = talloc_asprintf_append(result, "%s%s", p, dummy);
                    talloc_zfree(dummy);
                } else {
                    DEBUG(1, ("'%%d' is not allowed in this template.\n"));
                    goto done;
                }
                break;
            case 'P':
                if (!file_mode) {
                    DEBUG(1, ("'%%P' is not allowed in this template.\n"));
                    goto done;
                }
                if (kr->pd->cli_pid == 0) {
                    DEBUG(1, ("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;
            default:
                DEBUG(1, ("format error, unknown template [%%%c].\n", *n));
                goto done;
        }

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

        p = n + 1;
    }

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

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

static errno_t check_parent_stat(bool private_path, struct stat *parent_stat,
                                 uid_t uid, gid_t gid)
{
    if (private_path) {
        if (!((parent_stat->st_uid == 0 && parent_stat->st_gid == 0) ||
               parent_stat->st_uid == uid)) {
            DEBUG(1, ("Private directory can only be created below a "
                      "directory belonging to root or to [%d][%d].\n",
                      uid, gid));
            return EINVAL;
        }

        if (parent_stat->st_uid == uid) {
            if (!(parent_stat->st_mode & S_IXUSR)) {
                DEBUG(1, ("Parent directory does have the search bit set for "
                          "the owner.\n"));
                return EINVAL;
            }
        } else {
            if (!(parent_stat->st_mode & S_IXOTH)) {
                DEBUG(1, ("Parent directory does have the search bit set for "
                        "others.\n"));
                return EINVAL;
            }
        }
    } else {
        if (parent_stat->st_uid != 0 || parent_stat->st_gid != 0) {
            DEBUG(1, ("Public directory cannot be created below a user "
                      "directory.\n"));
            return EINVAL;
        }

        if (!(parent_stat->st_mode & S_IXOTH)) {
            DEBUG(1, ("Parent directory does have the search bit set for "
                      "others.\n"));
            return EINVAL;
        }
    }

    return EOK;
}

struct string_list {
    struct string_list *next;
    struct string_list *prev;
    char *s;
};

static errno_t find_ccdir_parent_data(TALLOC_CTX *mem_ctx,
                                      const char *ccdirname,
                                      struct stat *parent_stat,
                                      struct string_list **missing_parents)
{
    int ret = EFAULT;
    char *parent = NULL;
    char *end;
    struct string_list *li;

    ret = stat(ccdirname, parent_stat);
    if (ret == EOK) {
        if ( !S_ISDIR(parent_stat->st_mode) ) {
            DEBUG(SSSDBG_MINOR_FAILURE,
                  ("[%s] is not a directory.\n", ccdirname));
            return EINVAL;
        }
        return EOK;
    } else {
        if (errno != ENOENT) {
            ret = errno;
            DEBUG(SSSDBG_MINOR_FAILURE,
                  ("stat for [%s] failed: [%d][%s].\n", ccdirname, ret,
                   strerror(ret)));
            return ret;
        }
    }

    li = talloc_zero(mem_ctx, struct string_list);
    if (li == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              ("talloc_zero failed.\n"));
        return ENOMEM;
    }

    li->s = talloc_strdup(li, ccdirname);
    if (li->s == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              ("talloc_strdup failed.\n"));
        return ENOMEM;
    }

    DLIST_ADD(*missing_parents, li);

    parent = talloc_strdup(mem_ctx, ccdirname);
    if (parent == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              ("talloc_strdup failed.\n"));
        return ENOMEM;
    }

    /* We'll remove all trailing slashes from the back so that
     * we only pass /some/path to find_ccdir_parent_data, not
     * /some/path */
    do {
        end = strrchr(parent, '/');
        if (end == NULL || end == parent) {
            DEBUG(SSSDBG_MINOR_FAILURE,
                  ("Cannot find parent directory of [%s], / is not allowed.\n",
                   ccdirname));
            ret = EINVAL;
            goto done;
        }
        *end = '\0';
    } while (*(end+1) == '\0');

    ret = find_ccdir_parent_data(mem_ctx, parent, parent_stat, missing_parents);

done:
    talloc_free(parent);
    return ret;
}

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;
}

errno_t
create_ccache_dir(const char *ccdirname, pcre *illegal_re,
                  uid_t uid, gid_t gid, bool private_path)
{
    int ret = EFAULT;
    struct stat parent_stat;
    struct string_list *missing_parents = NULL;
    struct string_list *li = NULL;
    mode_t old_umask;
    mode_t new_dir_mode;
    TALLOC_CTX *tmp_ctx = NULL;

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

    if (*ccdirname != '/') {
        DEBUG(SSSDBG_MINOR_FAILURE,
              ("Only absolute paths are allowed, not [%s] .\n", ccdirname));
        ret = EINVAL;
        goto done;
    }

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

    ret = find_ccdir_parent_data(tmp_ctx, ccdirname, &parent_stat,
                                 &missing_parents);
    if (ret != EOK) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              ("find_ccdir_parent_data failed.\n"));
        goto done;
    }

    ret = check_parent_stat(private_path, &parent_stat, uid, gid);
    if (ret != EOK) {
        DEBUG(SSSDBG_MINOR_FAILURE,
              ("check_parent_stat failed for %s directory [%s].\n",
               private_path ? "private" : "public", ccdirname));
        goto done;
    }

    DLIST_FOR_EACH(li, missing_parents) {
        DEBUG(SSSDBG_TRACE_INTERNAL,
              ("Creating directory [%s].\n", li->s));
        if (li->next == NULL) {
            new_dir_mode = private_path ? 0700 : 01777;
        } else {
            if (private_path &&
                parent_stat.st_uid == uid && parent_stat.st_gid == gid) {
                new_dir_mode = 0700;
            } else {
                new_dir_mode = 0755;
            }
        }

        old_umask = umask(0000);
        ret = mkdir(li->s, new_dir_mode);
        umask(old_umask);
        if (ret != EOK) {
            ret = errno;
            DEBUG(SSSDBG_MINOR_FAILURE,
                  ("mkdir [%s] failed: [%d][%s].\n", li->s, ret,
                   strerror(ret)));
            goto done;
        }
        if (private_path &&
            ((parent_stat.st_uid == uid && parent_stat.st_gid == gid) ||
             li->next == NULL)) {
            ret = chown(li->s, uid, gid);
            if (ret != EOK) {
                ret = errno;
                DEBUG(SSSDBG_MINOR_FAILURE,
                      ("chown failed [%d][%s].\n", ret, strerror(ret)));
                goto done;
            }
        }
    }

    ret = EOK;

done:
    talloc_free(tmp_ctx);
    return ret;
}

errno_t get_ccache_file_data(const char *ccache_file, const char *client_name,
                             struct tgt_times *tgtt)
{
    krb5_error_code kerr;
    krb5_context ctx = NULL;
    krb5_ccache cc = NULL;
    krb5_principal client_princ = NULL;
    krb5_principal server_princ = NULL;
    char *server_name;
    krb5_creds mcred;
    krb5_creds cred;
    const char *realm_name;
    int realm_length;

    kerr = krb5_init_context(&ctx);
    if (kerr != 0) {
        DEBUG(1, ("krb5_init_context failed.\n"));
        goto done;
    }

    kerr = krb5_parse_name(ctx, client_name, &client_princ);
    if (kerr != 0) {
        KRB5_DEBUG(SSSDBG_OP_FAILURE, ctx, kerr);
        DEBUG(SSSDBG_CRIT_FAILURE, ("krb5_parse_name failed.\n"));
        goto done;
    }

    sss_krb5_princ_realm(ctx, client_princ, &realm_name, &realm_length);

    server_name = talloc_asprintf(NULL, "krbtgt/%.*s@%.*s",
                                  realm_length, realm_name,
                                  realm_length, realm_name);
    if (server_name == NULL) {
        kerr = KRB5_CC_NOMEM;
        DEBUG(1, ("talloc_asprintf failed.\n"));
        goto done;
    }

    kerr = krb5_parse_name(ctx, server_name, &server_princ);
    talloc_free(server_name);
    if (kerr != 0) {
        KRB5_DEBUG(SSSDBG_OP_FAILURE, ctx, kerr);
        DEBUG(SSSDBG_CRIT_FAILURE, ("krb5_parse_name failed.\n"));
        goto done;
    }

    kerr = krb5_cc_resolve(ctx, ccache_file, &cc);
    if (kerr != 0) {
        KRB5_DEBUG(SSSDBG_OP_FAILURE, ctx, kerr);
        DEBUG(SSSDBG_CRIT_FAILURE, ("krb5_cc_resolve failed.\n"));
        goto done;
    }

    memset(&mcred, 0, sizeof(mcred));
    memset(&cred, 0, sizeof(mcred));

    mcred.server = server_princ;
    mcred.client = client_princ;

    kerr = krb5_cc_retrieve_cred(ctx, cc, 0, &mcred, &cred);
    if (kerr != 0) {
        KRB5_DEBUG(SSSDBG_OP_FAILURE, ctx, kerr);
        DEBUG(SSSDBG_CRIT_FAILURE, ("krb5_cc_retrieve_cred failed.\n"));
        goto done;
    }

    tgtt->authtime = cred.times.authtime;
    tgtt->starttime = cred.times.starttime;
    tgtt->endtime = cred.times.endtime;
    tgtt->renew_till = cred.times.renew_till;

    krb5_free_cred_contents(ctx, &cred);

    kerr = krb5_cc_close(ctx, cc);
    if (kerr != 0) {
        KRB5_DEBUG(SSSDBG_OP_FAILURE, ctx, kerr);
        DEBUG(SSSDBG_CRIT_FAILURE, ("krb5_cc_close failed.\n"));
        goto done;
    }
    cc = NULL;

    kerr = 0;

done:
    if (cc != NULL) {
        krb5_cc_close(ctx, cc);
    }

    if (client_princ != NULL) {
        krb5_free_principal(ctx, client_princ);
    }

    if (server_princ != NULL) {
        krb5_free_principal(ctx, server_princ);
    }

    if (ctx != NULL) {
        krb5_free_context(ctx);
    }

    if (kerr != 0) {
        return EIO;
    }

    return EOK;
}

static errno_t
create_ccache_dir_head(const char *parent, pcre *illegal_re,
                       uid_t uid, gid_t gid, bool private_path)
{
    char *ccdirname;
    TALLOC_CTX *tmp_ctx = NULL;
    char *end;
    errno_t ret;

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

    /* We'll remove all trailing slashes from the back so that
     * we only pass /some/path to find_ccdir_parent_data, not
     * /some/path/ */
    do {
        end = strrchr(ccdirname, '/');
        if (end == NULL || end == ccdirname) {
            DEBUG(SSSDBG_CRIT_FAILURE, ("Cannot find parent directory of [%s], "
                  "/ is not allowed.\n", ccdirname));
            ret = EINVAL;
            goto done;
        }
        *end = '\0';
    } while (*(end+1) == '\0');

    ret = create_ccache_dir(ccdirname, illegal_re, uid, gid, private_path);
done:
    talloc_free(tmp_ctx);
    return ret;
}

/*======== ccache back end utilities ========*/
struct sss_krb5_cc_be *
get_cc_be_ops(enum sss_krb5_cc_type type)
{
    struct sss_krb5_cc_be *be;

    switch (type) {
        case SSS_KRB5_TYPE_FILE:
            be = &file_cc;
            break;

#ifdef HAVE_KRB5_DIRCACHE
        case SSS_KRB5_TYPE_DIR:
            be = &dir_cc;
            break;
#endif /* HAVE_KRB5_DIRCACHE */

        case SSS_KRB5_TYPE_UNKNOWN:
            be = NULL;
            break;
    }

    return be;
}

struct sss_krb5_cc_be *
get_cc_be_ops_ccache(const char *ccache)
{
    enum sss_krb5_cc_type type;

    type = sss_krb5_get_type(ccache);
    return get_cc_be_ops(type);
}

/*======== Operations on the FILE: back end ========*/
errno_t
cc_file_create(const char *location, pcre *illegal_re,
               uid_t uid, gid_t gid, bool private_path)
{
    const char *filename;

    filename = sss_krb5_residual_check_type(location, SSS_KRB5_TYPE_FILE);
    if (filename == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, ("Bad ccache type %s\n", location));
        return EINVAL;
    }

    return create_ccache_dir_head(filename, illegal_re, uid, gid, private_path);
}

static errno_t
cc_residual_is_used(uid_t uid, const char *ccname,
                    enum sss_krb5_cc_type type, bool *result)
{
    int ret;
    struct stat stat_buf;
    bool active;

    *result = false;

    if (ccname == NULL || *ccname == '\0') {
        return EINVAL;
    }

    ret = lstat(ccname, &stat_buf);

    if (ret == -1 && errno != ENOENT) {
        ret = errno;
        DEBUG(SSSDBG_OP_FAILURE,
              ("stat failed [%d][%s].\n", ret, strerror(ret)));
        return ret;
    } else if (ret == EOK) {
        if (stat_buf.st_uid != uid) {
            DEBUG(SSSDBG_OP_FAILURE,
                  ("Cache file [%s] exists, but is owned by [%d] instead of "
                   "[%d].\n", ccname, stat_buf.st_uid, uid));
            return EINVAL;
        }

        switch (type) {
#ifdef HAVE_KRB5_DIRCACHE
            case SSS_KRB5_TYPE_DIR:
                ret = S_ISDIR(stat_buf.st_mode);
                break;
#endif /* HAVE_KRB5_DIRCACHE */
            case SSS_KRB5_TYPE_FILE:
                ret = S_ISREG(stat_buf.st_mode);
                break;
            default:
                DEBUG(SSSDBG_CRIT_FAILURE, ("Unsupported ccache type\n"));
                return EINVAL;
        }

        if (ret == 0) {
            DEBUG(SSSDBG_OP_FAILURE,
                  ("Cache file [%s] exists, but is not the expected type\n",
                   ccname));
            return EINVAL;
        }
    }

    ret = check_if_uid_is_active(uid, &active);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, ("check_if_uid_is_active failed.\n"));
        return ret;
    }

    if (!active) {
        DEBUG(SSSDBG_TRACE_FUNC, ("User [%d] is not active\n", uid));
    } else {
        DEBUG(SSSDBG_TRACE_LIBS,
              ("User [%d] is still active, reusing ccache [%s].\n",
              uid, ccname));
        *result = true;
    }
    return EOK;
}

errno_t
cc_file_check_existing(const char *location, uid_t uid,
                       const char *realm, const char *princ,
                       bool *_active, bool *_valid)
{
    errno_t ret;
    bool active;
    bool valid;
    const char *filename;
    krb5_ccache ccache = NULL;
    krb5_context context = NULL;
    krb5_error_code kerr;

    filename = sss_krb5_residual_check_type(location, SSS_KRB5_TYPE_FILE);
    if (!filename) {
        DEBUG(SSSDBG_CRIT_FAILURE, ("%s is not of type FILE:\n"));
        return EINVAL;
    }

    if (filename[0] != '/') {
        DEBUG(SSSDBG_OP_FAILURE, ("Only absolute path names are allowed.\n"));
        return EINVAL;
    }

    ret = cc_residual_is_used(uid, filename, SSS_KRB5_TYPE_FILE, &active);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, ("Could not check if ccache is active\n"));
        return ret;
    }

    kerr = krb5_init_context(&context);
    if (kerr != 0) {
        DEBUG(SSSDBG_CRIT_FAILURE, ("Failed to init kerberos context\n"));
        return EIO;
    }

    kerr = krb5_cc_resolve(context, location, &ccache);
    if (kerr != 0) {
        KRB5_DEBUG(SSSDBG_OP_FAILURE, context, kerr);
        krb5_free_context(context);
        DEBUG(SSSDBG_CRIT_FAILURE, ("krb5_cc_resolve failed.\n"));
        return EIO;
    }

    kerr = check_for_valid_tgt(context, ccache, realm, princ, &valid);
    krb5_free_context(context);
    krb5_cc_close(context, ccache);
    if (kerr != EOK) {
        KRB5_DEBUG(SSSDBG_OP_FAILURE, context, kerr);
        DEBUG(SSSDBG_CRIT_FAILURE,
              ("Could not check if ccache contains a valid principal\n"));
        return EIO;
    }

    *_active = active;
    *_valid = valid;
    return EOK;
}

const char *
cc_file_cache_for_princ(TALLOC_CTX *mem_ctx, const char *location,
                        const char *princ)
{
    return talloc_strdup(mem_ctx, location);
}

errno_t
cc_file_remove(const char *location)
{
    errno_t ret;
    const char *filename;

    filename = sss_krb5_residual_check_type(location, SSS_KRB5_TYPE_FILE);
    if (!filename) {
        DEBUG(SSSDBG_CRIT_FAILURE, ("%s is not of type FILE:\n"));
        return EINVAL;
    }

    if (filename[0] != '/') {
        DEBUG(SSSDBG_CRIT_FAILURE,
              ("Ccache file name [%s] is not an absolute path.\n", filename));
        return EINVAL;
    }

    errno = 0;
    ret = unlink(filename);
    if (ret == -1 && errno != ENOENT) {
        ret = errno;
        DEBUG(SSSDBG_CRIT_FAILURE,
              ("unlink [%s] failed [%d][%s].\n", filename, ret,
                                                 strerror(ret)));
        return ret;
    }
    return EOK;
}

struct sss_krb5_cc_be file_cc = {
    .type               = SSS_KRB5_TYPE_FILE,
    .create             = cc_file_create,
    .check_existing     = cc_file_check_existing,
    .ccache_for_princ   = cc_file_cache_for_princ,
    .remove             = cc_file_remove,
};

#ifdef HAVE_KRB5_DIRCACHE
/*======== Operations on the DIR: back end ========*/
errno_t
cc_dir_create(const char *location, pcre *illegal_re,
              uid_t uid, gid_t gid, bool private_path)
{
    const char *dirname;

    dirname = sss_krb5_residual_check_type(location, SSS_KRB5_TYPE_DIR);
    if (dirname == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, ("Bad residual type\n"));
        return EINVAL;
    }

    return create_ccache_dir_head(dirname, illegal_re, uid, gid, private_path);
}

static krb5_error_code
get_ccache_for_princ(krb5_context context, const char *location,
                     const char *princ, krb5_ccache *_ccache)
{
    krb5_error_code krberr;
    krb5_principal client_principal = NULL;

    krberr = krb5_cc_set_default_name(context, location);
    if (krberr != 0) {
        KRB5_DEBUG(SSSDBG_OP_FAILURE, context, krberr);
        DEBUG(SSSDBG_CRIT_FAILURE, ("krb5_cc_resolve failed.\n"));
        return krberr;
    }

    krberr = krb5_parse_name(context, princ, &client_principal);
    if (krberr != 0) {
        KRB5_DEBUG(SSSDBG_OP_FAILURE, context, krberr);
        DEBUG(SSSDBG_CRIT_FAILURE, ("krb5_parse_name failed.\n"));
        return krberr;
    }

    krberr = krb5_cc_cache_match(context, client_principal, _ccache);
    krb5_free_principal(context, client_principal);
    return krberr;
}

errno_t
cc_dir_check_existing(const char *location, uid_t uid,
                      const char *realm, const char *princ,
                      bool *_active, bool *_valid)
{
    bool active = false;
    bool valid = false;
    krb5_ccache ccache = NULL;
    krb5_context context = NULL;
    krb5_error_code krberr;
    enum sss_krb5_cc_type type;
    const char *filename;
    const char *dir;
    char *tmp;
    errno_t ret;

    type = sss_krb5_get_type(location);
    if (type != SSS_KRB5_TYPE_DIR) {
        DEBUG(SSSDBG_CRIT_FAILURE, ("%s is not of type DIR:\n", location));
        return EINVAL;
    }

    filename = sss_krb5_cc_file_path(location);
    if (!filename) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              ("Existing ccname does not contain path into the collection"));
        return EINVAL;
    }

    if (filename[0] != '/') {
        DEBUG(SSSDBG_CRIT_FAILURE,
              ("Only absolute path names are allowed.\n"));
        return EINVAL;
    }

    tmp = talloc_strdup(NULL, filename);
    if (!tmp) return ENOMEM;

    dir = dirname(tmp);
    if (!dir) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              ("Cannot base get directory of %s\n", location));
        return EINVAL;
    }

    ret = cc_residual_is_used(uid, dir, SSS_KRB5_TYPE_DIR, &active);
    talloc_free(tmp);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, ("Could not check if ccache is active\n"));
        return ret;
    }

    krberr = krb5_init_context(&context);
    if (krberr) {
        DEBUG(SSSDBG_CRIT_FAILURE, ("Failed to init kerberos context\n"));
        return EIO;
    }

    krberr = krb5_cc_resolve(context, location, &ccache);
    if (krberr == KRB5_FCC_NOFILE || ccache == NULL) {
        /* KRB5_FCC_NOFILE would be returned if the directory components
         * of the DIR cache do not exist, which is the case in /run
         * after a reboot
         */
        DEBUG(SSSDBG_TRACE_FUNC,
              ("ccache %s is missing or empty\n", location));
        valid = false;
        ret = EOK;
        goto done;
    } else if (krberr != 0) {
        KRB5_DEBUG(SSSDBG_OP_FAILURE, context, krberr);
        DEBUG(SSSDBG_CRIT_FAILURE, ("krb5_cc_resolve failed.\n"));
        ret = EIO;
        goto done;
    }

    krberr = check_for_valid_tgt(context, ccache, realm, princ, &valid);
    if (krberr != EOK) {
        KRB5_DEBUG(SSSDBG_OP_FAILURE, context, krberr);
        DEBUG(SSSDBG_CRIT_FAILURE,
              ("Could not check if ccache contains a valid principal\n"));
        ret = EIO;
        goto done;
    }

    ret = EOK;
done:
    if (ccache) krb5_cc_close(context, ccache);
    krb5_free_context(context);
    *_active = active;
    *_valid = valid;
    return ret;
}

const char *
cc_dir_cache_for_princ(TALLOC_CTX *mem_ctx, const char *location,
                       const char *princ)
{
    krb5_context context = NULL;
    krb5_error_code krberr;
    krb5_ccache ccache = NULL;
    char *name;
    const char *ccname;

    ccname = sss_krb5_residual_check_type(location, SSS_KRB5_TYPE_DIR);
    if (!ccname) {
        DEBUG(SSSDBG_CRIT_FAILURE, ("Cannot get ccname file from %s\n",
              location));
        return NULL;
    }

    /* ccname already points to a subsidiary cache */
    if (ccname[0] == ':' && ccname[1] && ccname[1] == '/') {
        return talloc_strdup(mem_ctx, location);
    }

    krberr = krb5_init_context(&context);
    if (krberr) {
        DEBUG(SSSDBG_OP_FAILURE, ("Failed to init kerberos context\n"));
        return NULL;
    }

    krberr = get_ccache_for_princ(context, location, princ, &ccache);
    if (krberr) {
        DEBUG(SSSDBG_TRACE_FUNC, ("No principal for %s in %s\n",
              princ, location));
        krb5_free_context(context);
        return NULL;
    }

    krberr = krb5_cc_get_full_name(context, ccache, &name);
    if (ccache) krb5_cc_close(context, ccache);
    krb5_free_context(context);
    if (krberr) {
        KRB5_DEBUG(SSSDBG_OP_FAILURE, context, krberr);
        DEBUG(SSSDBG_CRIT_FAILURE, ("Could not get full name of ccache\n"));
        return NULL;
    }

    return talloc_strdup(mem_ctx, name);
}

errno_t
cc_dir_remove(const char *location)
{
    const char *subsidiary;

    if (sss_krb5_get_type(location) != SSS_KRB5_TYPE_DIR) {
        DEBUG(SSSDBG_CRIT_FAILURE, ("%s is not of type DIR\n", location));
        return EINVAL;
    }

    subsidiary = sss_krb5_cc_file_path(location);
    if (!subsidiary) {
        DEBUG(SSSDBG_CRIT_FAILURE, ("Cannot get subsidiary cache from %s\n",
              location));
        return EINVAL;
    }

    return cc_file_remove(subsidiary);
}

struct sss_krb5_cc_be dir_cc = {
    .type               = SSS_KRB5_TYPE_DIR,
    .create             = cc_dir_create,
    .check_existing     = cc_dir_check_existing,
    .ccache_for_princ   = cc_dir_cache_for_princ,
    .remove             = cc_dir_remove
};

#endif /* HAVE_KRB5_DIRCACHE */