/*
    SSSD

    IPA Backend Module -- Access control

    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 <sys/param.h>
#include <security/pam_modules.h>

#include "util/util.h"
#include "providers/ldap/sdap_async.h"
#include "providers/ldap/sdap_access.h"
#include "providers/ipa/ipa_common.h"
#include "providers/ipa/ipa_access.h"
#include "providers/ipa/ipa_hosts.h"
#include "providers/ipa/ipa_hbac_private.h"
#include "providers/ipa/ipa_hbac_rules.h"

/* External logging function for HBAC. */
void hbac_debug_messages(const char *file, int line,
                         const char *function,
                         enum hbac_debug_level level,
                         const char *fmt, ...)
{
    int loglevel;

    switch(level) {
    case HBAC_DBG_FATAL:
        loglevel = SSSDBG_FATAL_FAILURE;
        break;
    case HBAC_DBG_ERROR:
        loglevel = SSSDBG_OP_FAILURE;
        break;
    case HBAC_DBG_WARNING:
        loglevel = SSSDBG_MINOR_FAILURE;
        break;
    case HBAC_DBG_INFO:
        loglevel = SSSDBG_CONF_SETTINGS;
        break;
    case HBAC_DBG_TRACE:
        loglevel = SSSDBG_TRACE_INTERNAL;
        break;
    default:
        loglevel = SSSDBG_UNRESOLVED;
        break;
    }

    if (DEBUG_IS_SET(loglevel)) {
        va_list ap;

        va_start(ap, fmt);
        sss_vdebug_fn(file, line, function, loglevel, 0, fmt, ap);
        va_end(ap);
    }
}

enum hbac_result {
    HBAC_ALLOW = 1,
    HBAC_DENY,
    HBAC_NOT_APPLICABLE
};

enum check_result {
    RULE_APPLICABLE = 0,
    RULE_NOT_APPLICABLE,
    RULE_ERROR
};

struct ipa_fetch_hbac_state {
    struct tevent_context *ev;
    struct be_ctx *be_ctx;
    struct sdap_id_ctx *sdap_ctx;
    struct ipa_access_ctx *access_ctx;
    struct sdap_id_op *sdap_op;
    struct dp_option *ipa_options;
    struct time_rules_ctx *tr_ctx;

    struct sdap_search_base **search_bases;

    /* Hosts */
    size_t host_count;
    struct sysdb_attrs **hosts;
    size_t hostgroup_count;
    struct sysdb_attrs **hostgroups;
    struct sysdb_attrs *ipa_host;

    /* Rules */
    size_t rule_count;
    struct sysdb_attrs **rules;

    /* Services */
    size_t service_count;
    struct sysdb_attrs **services;
    size_t servicegroup_count;
    struct sysdb_attrs **servicegroups;
};

static errno_t ipa_fetch_hbac_retry(struct tevent_req *req);
static void ipa_fetch_hbac_connect_done(struct tevent_req *subreq);
static errno_t ipa_fetch_hbac_hostinfo(struct tevent_req *req);
static void ipa_fetch_hbac_hostinfo_done(struct tevent_req *subreq);
static void ipa_fetch_hbac_services_done(struct tevent_req *subreq);
static void ipa_fetch_hbac_rules_done(struct tevent_req *subreq);
static errno_t ipa_purge_hbac(struct sss_domain_info *domain);
static errno_t ipa_save_hbac(struct sss_domain_info *domain,
                             struct ipa_fetch_hbac_state *state);

static struct tevent_req *
ipa_fetch_hbac_send(TALLOC_CTX *mem_ctx,
                    struct tevent_context *ev,
                    struct be_ctx *be_ctx,
                    struct ipa_access_ctx *access_ctx)
{
    struct ipa_fetch_hbac_state *state;
    struct tevent_req *req;
    time_t now, refresh_interval;
    bool offline;
    errno_t ret;

    req = tevent_req_create(mem_ctx, &state,
                            struct ipa_fetch_hbac_state);
    if (req == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n");
        return NULL;
    }

    state->ev = ev;
    state->be_ctx = be_ctx;
    state->access_ctx = access_ctx;
    state->sdap_ctx = access_ctx->sdap_ctx;
    state->ipa_options = access_ctx->ipa_options;
    state->tr_ctx = access_ctx->tr_ctx;
    state->search_bases = access_ctx->hbac_search_bases;

    if (state->search_bases == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "No HBAC search base found.\n");
        ret = EINVAL;
        goto immediately;
    }

    state->sdap_op = sdap_id_op_create(state, state->sdap_ctx->conn->conn_cache);
    if (state->sdap_op == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "sdap_id_op_create() failed\n");
        ret = ENOMEM;
        goto immediately;
    }

    offline = be_is_offline(be_ctx);
    DEBUG(SSSDBG_TRACE_ALL, "Connection status is [%s].\n",
          offline ? "offline" : "online");

    refresh_interval = dp_opt_get_int(state->ipa_options, IPA_HBAC_REFRESH);
    now = time(NULL);

    if (offline || now < access_ctx->last_update + refresh_interval) {
        DEBUG(SSSDBG_TRACE_FUNC, "Performing cached HBAC evaluation\n");
        ret = EOK;
        goto immediately;
    }

    ret = ipa_fetch_hbac_retry(req);
    if (ret != EAGAIN) {
        goto immediately;
    }

    return req;

immediately:
    if (ret == EOK) {
        tevent_req_done(req);
    } else {
        tevent_req_error(req, ret);
    }
    tevent_req_post(req, ev);

    return req;
}

static errno_t ipa_fetch_hbac_retry(struct tevent_req *req)
{
    struct ipa_fetch_hbac_state *state;
    struct tevent_req *subreq;
    int ret;

    state = tevent_req_data(req, struct ipa_fetch_hbac_state);

    subreq = sdap_id_op_connect_send(state->sdap_op, state, &ret);
    if (subreq == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "sdap_id_op_connect_send() failed: "
                                   "%d(%s)\n", ret, strerror(ret));
        return ret;
    }

    tevent_req_set_callback(subreq, ipa_fetch_hbac_connect_done, req);

    return EAGAIN;
}

static void ipa_fetch_hbac_connect_done(struct tevent_req *subreq)
{
    struct tevent_req *req = NULL;
    int dp_error;
    errno_t ret;

    req = tevent_req_callback_data(subreq, struct tevent_req);

    ret = sdap_id_op_connect_recv(subreq, &dp_error);
    talloc_zfree(subreq);
    if (ret != EOK) {
        goto done;
    }

    if (dp_error == DP_ERR_OFFLINE) {
        ret = EOK;
        goto done;
    }

    ret = ipa_fetch_hbac_hostinfo(req);
    if (ret == EAGAIN) {
        return;
    }

done:
    if (ret != EOK) {
        tevent_req_error(req, ret);
        return;
    }

    tevent_req_done(req);
}

static errno_t ipa_fetch_hbac_hostinfo(struct tevent_req *req)
{
    struct ipa_fetch_hbac_state *state;
    struct tevent_req *subreq;
    const char *hostname;
    bool srchost;

    state = tevent_req_data(req, struct ipa_fetch_hbac_state);

    srchost = dp_opt_get_bool(state->ipa_options, IPA_HBAC_SUPPORT_SRCHOST);
    if (srchost) {
        /* Support srchost
         * -> we don't want any particular host,
         *    we want all hosts
         */
        hostname = NULL;

        /* THIS FEATURE IS DEPRECATED */
        DEBUG(SSSDBG_MINOR_FAILURE, "WARNING: Using deprecated option "
                    "ipa_hbac_support_srchost.\n");
        sss_log(SSS_LOG_NOTICE, "WARNING: Using deprecated option "
                    "ipa_hbac_support_srchost.\n");
    } else {
        hostname = dp_opt_get_string(state->ipa_options, IPA_HOSTNAME);
    }

    subreq = ipa_host_info_send(state, state->ev,
                                sdap_id_op_handle(state->sdap_op),
                                state->sdap_ctx->opts, hostname,
                                state->access_ctx->host_map,
                                state->access_ctx->hostgroup_map,
                                state->access_ctx->host_search_bases);
    if (subreq == NULL) {
        return ENOMEM;
    }

    tevent_req_set_callback(subreq, ipa_fetch_hbac_hostinfo_done, req);

    return EAGAIN;
}

static void ipa_fetch_hbac_hostinfo_done(struct tevent_req *subreq)
{
    struct ipa_fetch_hbac_state *state = NULL;
    struct tevent_req *req = NULL;
    errno_t ret;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct ipa_fetch_hbac_state);

    ret = ipa_host_info_recv(subreq, state,
                             &state->host_count, &state->hosts,
                             &state->hostgroup_count, &state->hostgroups);
    talloc_zfree(subreq);
    if (ret != EOK) {
        goto done;
    }

    subreq = ipa_hbac_service_info_send(state, state->ev,
                                        sdap_id_op_handle(state->sdap_op),
                                        state->sdap_ctx->opts,
                                        state->search_bases);
    if (subreq == NULL) {
        ret = ENOMEM;
        goto done;
    }

    tevent_req_set_callback(subreq, ipa_fetch_hbac_services_done, req);

    return;

done:
    if (ret != EOK) {
        tevent_req_error(req, ret);
        return;
    }

    tevent_req_done(req);
}

static void ipa_fetch_hbac_services_done(struct tevent_req *subreq)
{
    struct ipa_fetch_hbac_state *state;
    struct tevent_req *req;
    const char *ipa_hostname;
    const char *hostname;
    errno_t ret;
    size_t i;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct ipa_fetch_hbac_state);

    ret = ipa_hbac_service_info_recv(subreq, state,
                             &state->service_count, &state->services,
                             &state->servicegroup_count, &state->servicegroups);
    talloc_zfree(subreq);
    if (ret != EOK) {
        goto done;
    }

    /* Get the ipa_host attrs */
    state->ipa_host = NULL;
    ipa_hostname = dp_opt_get_cstring(state->ipa_options, IPA_HOSTNAME);
    if (ipa_hostname == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Missing ipa_hostname, this should never happen.\n");
        ret = EINVAL;
        goto done;
    }

    for (i = 0; i < state->host_count; i++) {
        ret = sysdb_attrs_get_string(state->hosts[i], SYSDB_FQDN, &hostname);
        if (ret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE, "Could not locate IPA host\n");
            goto done;
        }

        if (strcasecmp(hostname, ipa_hostname) == 0) {
            state->ipa_host = state->hosts[i];
            break;
        }
    }

    if (state->ipa_host == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Could not locate IPA host\n");
        ret = EINVAL;
        goto done;
    }

    subreq = ipa_hbac_rule_info_send(state, state->ev,
                                     sdap_id_op_handle(state->sdap_op),
                                     state->sdap_ctx->opts,
                                     state->search_bases,
                                     state->ipa_host);
    if (subreq == NULL) {
        ret = ENOMEM;
        goto done;
    }

    tevent_req_set_callback(subreq, ipa_fetch_hbac_rules_done, req);

    return;

done:
    if (ret != EOK) {
        tevent_req_error(req, ret);
        return;
    }

    tevent_req_done(req);
}

static void ipa_fetch_hbac_rules_done(struct tevent_req *subreq)
{
    struct ipa_fetch_hbac_state *state = NULL;
    struct tevent_req *req = NULL;
    int dp_error;
    errno_t ret;
    bool found;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct ipa_fetch_hbac_state);

    ret = ipa_hbac_rule_info_recv(subreq, state,
                                  &state->rule_count, &state->rules);
    talloc_zfree(subreq);
    if (ret == ENOENT) {
        /* Set ret to EOK so we can safely call sdap_id_op_done. */
        found = false;
        ret = EOK;
    } else if (ret == EOK) {
        found = true;
    } else {
        goto done;
    }

    ret = sdap_id_op_done(state->sdap_op, ret, &dp_error);
    if (dp_error == DP_ERR_OK && ret != EOK) {
        /* retry */
        ret = ipa_fetch_hbac_retry(req);
        if (ret != EAGAIN) {
            tevent_req_error(req, ret);
        }
        return;
    } else if (ret != EOK) {
        tevent_req_error(req, ret);
        return;
    }

    if (found == false) {
        /* No rules were found that apply to this host. */
        ret = ipa_purge_hbac(state->be_ctx->domain);
        if (ret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE, "Unable to remove HBAC rules\n");
            goto done;
        }

        ret = ENOENT;
        goto done;
    }

    ret = ipa_save_hbac(state->be_ctx->domain, state);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Unable to save HBAC rules\n");
        goto done;
    }

    ret = EOK;

done:
    if (ret != EOK) {
        tevent_req_error(req, ret);
        return;
    }

    tevent_req_done(req);
}

static errno_t ipa_fetch_hbac_recv(struct tevent_req *req)
{
    TEVENT_REQ_RETURN_ON_ERROR(req);

    return EOK;
}

static errno_t ipa_purge_hbac(struct sss_domain_info *domain)
{
    TALLOC_CTX *tmp_ctx;
    struct ldb_dn *base_dn;
    errno_t ret;

    tmp_ctx = talloc_new(NULL);
    if (tmp_ctx == NULL) {
        return ENOMEM;
    }

    /* Delete any rules in the sysdb so offline logins are also denied. */
    base_dn = sysdb_custom_subtree_dn(tmp_ctx, domain, HBAC_RULES_SUBDIR);
    if (base_dn == NULL) {
        ret = ENOMEM;
        goto done;
    }

    ret = sysdb_delete_recursive(domain->sysdb, base_dn, true);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "sysdb_delete_recursive failed.\n");
        goto done;
    }

    ret = EOK;

done:
    talloc_free(tmp_ctx);
    return ret;
}

static errno_t ipa_save_hbac(struct sss_domain_info *domain,
                             struct ipa_fetch_hbac_state *state)
{
    bool in_transaction = false;
    errno_t ret;
    errno_t sret;

    ret = sysdb_transaction_start(domain->sysdb);
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Could not start transaction\n");
        goto done;
    }
    in_transaction = true;

    /* Save the hosts */
    ret = ipa_hbac_sysdb_save(domain, HBAC_HOSTS_SUBDIR, SYSDB_FQDN,
                              state->host_count, state->hosts,
                              HBAC_HOSTGROUPS_SUBDIR, SYSDB_NAME,
                              state->hostgroup_count, state->hostgroups);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Error saving hosts [%d]: %s\n",
              ret, sss_strerror(ret));
        goto done;
    }

    /* Save the services */
    ret = ipa_hbac_sysdb_save(domain, HBAC_SERVICES_SUBDIR, IPA_CN,
                              state->service_count, state->services,
                              HBAC_SERVICEGROUPS_SUBDIR, IPA_CN,
                              state->servicegroup_count,
                              state->servicegroups);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Error saving services [%d]: %s\n",
              ret, sss_strerror(ret));
        goto done;
    }
    /* Save the rules */
    ret = ipa_hbac_sysdb_save(domain, HBAC_RULES_SUBDIR, IPA_UNIQUE_ID,
                              state->rule_count, state->rules,
                              NULL, NULL, 0, NULL);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Error saving rules [%d]: %s\n",
              ret, sss_strerror(ret));
        goto done;
    }

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

    state->access_ctx->last_update = time(NULL);

    ret = EOK;

done:
    if (in_transaction) {
        sret = sysdb_transaction_cancel(domain->sysdb);
        if (sret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Could not cancel transaction\n");
        }
    }

    return ret;
}

errno_t ipa_hbac_evaluate_rules(struct be_ctx *be_ctx,
                                struct dp_option *ipa_options,
                                struct pam_data *pd)
{
    TALLOC_CTX *tmp_ctx;
    struct hbac_ctx hbac_ctx;
    struct hbac_rule **hbac_rules;
    struct hbac_eval_req *eval_req;
    enum hbac_eval_result result;
    struct hbac_info *info = NULL;
    errno_t ret;

    tmp_ctx = talloc_new(NULL);
    if (tmp_ctx == NULL) {
        return ENOMEM;
    }

    hbac_ctx.be_ctx = be_ctx;
    hbac_ctx.ipa_options = ipa_options;
    hbac_ctx.pd = pd;

    /* Get HBAC rules from the sysdb */
    ret = hbac_get_cached_rules(tmp_ctx, be_ctx->domain,
                                &hbac_ctx.rule_count, &hbac_ctx.rules);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Could not retrieve rules from the cache\n");
        goto done;
    }

    ret = hbac_ctx_to_rules(tmp_ctx, &hbac_ctx, &hbac_rules, &eval_req);
    if (ret == EPERM) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "DENY rules detected. Denying access to all users\n");
        ret = ERR_ACCESS_DENIED;
        goto done;
    } else if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Could not construct HBAC rules\n");
        goto done;
    }

    hbac_enable_debug(hbac_debug_messages);

    result = hbac_evaluate(hbac_rules, eval_req, &info);
    if (result == HBAC_EVAL_ALLOW) {
        DEBUG(SSSDBG_MINOR_FAILURE, "Access granted by HBAC rule [%s]\n",
              info->rule_name);
        ret = EOK;
        goto done;
    } else if (result == HBAC_EVAL_ERROR) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Error [%s] occurred in rule [%s]\n",
              hbac_error_string(info->code), info->rule_name);
        ret = EIO;
        goto done;
    } else if (result == HBAC_EVAL_OOM) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Insufficient memory\n");
        ret = ENOMEM;
        goto done;
    }

    DEBUG(SSSDBG_MINOR_FAILURE, "Access denied by HBAC rules\n");
    ret = ERR_ACCESS_DENIED;

done:
    hbac_free_info(info);
    talloc_free(tmp_ctx);
    return ret;
}

errno_t hbac_get_cached_rules(TALLOC_CTX *mem_ctx,
                              struct sss_domain_info *domain,
                              size_t *_rule_count,
                              struct sysdb_attrs ***_rules)
{
    errno_t ret;
    struct ldb_message **msgs;
    struct sysdb_attrs **rules;
    size_t rule_count;
    TALLOC_CTX *tmp_ctx;
    char *filter;
    const char *attrs[] = { OBJECTCLASS,
                            IPA_CN,
                            SYSDB_ORIG_DN,
                            IPA_UNIQUE_ID,
                            IPA_ENABLED_FLAG,
                            IPA_ACCESS_RULE_TYPE,
                            IPA_MEMBER_USER,
                            IPA_USER_CATEGORY,
                            IPA_MEMBER_SERVICE,
                            IPA_SERVICE_CATEGORY,
                            IPA_SOURCE_HOST,
                            IPA_SOURCE_HOST_CATEGORY,
                            IPA_EXTERNAL_HOST,
                            IPA_MEMBER_HOST,
                            IPA_HOST_CATEGORY,
                            NULL };

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

    filter = talloc_asprintf(tmp_ctx, "(objectClass=%s)", IPA_HBAC_RULE);
    if (filter == NULL) {
        ret = ENOMEM;
        goto done;
    }

    ret = sysdb_search_custom(tmp_ctx, domain, filter,
                              HBAC_RULES_SUBDIR, attrs,
                              &rule_count, &msgs);
    if (ret != EOK && ret != ENOENT) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Error looking up HBAC rules\n");
        goto done;
    } if (ret == ENOENT) {
       rule_count = 0;
    }

    ret = sysdb_msg2attrs(tmp_ctx, rule_count, msgs, &rules);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Could not convert ldb message to sysdb_attrs\n");
        goto done;
    }

    if (_rules) *_rules = talloc_steal(mem_ctx, rules);
    if (_rule_count) *_rule_count = rule_count;

    ret = EOK;
done:
    talloc_free(tmp_ctx);
    return ret;
}

struct ipa_pam_access_handler_state {
    struct tevent_context *ev;
    struct be_ctx *be_ctx;
    struct ipa_access_ctx *access_ctx;
    struct pam_data *pd;
};

static void ipa_pam_access_handler_sdap_done(struct tevent_req *subreq);
static void ipa_pam_access_handler_done(struct tevent_req *subreq);

struct tevent_req *
ipa_pam_access_handler_send(TALLOC_CTX *mem_ctx,
                           struct ipa_access_ctx *access_ctx,
                           struct pam_data *pd,
                           struct dp_req_params *params)
{
    struct ipa_pam_access_handler_state *state;
    struct tevent_req *subreq;
    struct tevent_req *req;

    req = tevent_req_create(mem_ctx, &state,
                            struct ipa_pam_access_handler_state);
    if (req == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "tevent_req_create() failed\n");
        return NULL;
    }

    state->pd = pd;
    state->ev = params->ev;
    state->be_ctx = params->be_ctx;
    state->access_ctx = access_ctx;

    subreq = sdap_access_send(state, params->ev, params->be_ctx,
                              params->domain, access_ctx->sdap_access_ctx,
                              access_ctx->sdap_ctx->conn, pd);
    if (subreq == NULL) {
        state->pd->pam_status = PAM_SYSTEM_ERR;
        goto immediately;
    }

    tevent_req_set_callback(subreq, ipa_pam_access_handler_sdap_done, req);

    return req;

immediately:
    /* TODO For backward compatibility we always return EOK to DP now. */
    tevent_req_done(req);
    tevent_req_post(req, params->ev);

    return req;
}

static void ipa_pam_access_handler_sdap_done(struct tevent_req *subreq)
{
    struct ipa_pam_access_handler_state *state;
    struct tevent_req *req;
    errno_t ret;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct ipa_pam_access_handler_state);

    ret = sdap_access_recv(subreq);
    talloc_free(subreq);
    switch (ret) {
    case EOK:
        /* Account wasn't locked. Continue below to HBAC processing. */
        break;
    case ERR_ACCESS_DENIED:
        /* Account was locked. Return permission denied here. */
        state->pd->pam_status = PAM_PERM_DENIED;
        goto done;
    case ERR_ACCOUNT_EXPIRED:
        state->pd->pam_status = PAM_ACCT_EXPIRED;
        goto done;
    default:
        DEBUG(SSSDBG_CRIT_FAILURE, "Error retrieving access check result "
              "[%d]: %s.\n", ret, sss_strerror(ret));
        state->pd->pam_status = PAM_SYSTEM_ERR;
        break;
    }

    subreq = ipa_fetch_hbac_send(state, state->ev, state->be_ctx,
                                 state->access_ctx);
    if (subreq == NULL) {
        state->pd->pam_status = PAM_SYSTEM_ERR;
        goto done;
    }

    tevent_req_set_callback(subreq, ipa_pam_access_handler_done, req);

    return;

done:
    /* TODO For backward compatibility we always return EOK to DP now. */
    tevent_req_done(req);
}

static void ipa_pam_access_handler_done(struct tevent_req *subreq)
{
    struct ipa_pam_access_handler_state *state;
    struct tevent_req *req;
    errno_t ret;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct ipa_pam_access_handler_state);

    ret = ipa_fetch_hbac_recv(subreq);
    talloc_free(subreq);

    if (ret == ENOENT) {
        DEBUG(SSSDBG_CRIT_FAILURE, "No HBAC rules find, denying access\n");
        state->pd->pam_status = PAM_PERM_DENIED;
        goto done;
    } else if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Unable to fetch HBAC rules [%d]: %s\n",
              ret, sss_strerror(ret));
        state->pd->pam_status = PAM_SYSTEM_ERR;
        goto done;
    }

    ret = ipa_hbac_evaluate_rules(state->be_ctx,
                                  state->access_ctx->ipa_options, state->pd);
    if (ret == EOK) {
        state->pd->pam_status = PAM_SUCCESS;
    } else if (ret == ERR_ACCESS_DENIED) {
        state->pd->pam_status = PAM_PERM_DENIED;
    } else {
        state->pd->pam_status = PAM_SYSTEM_ERR;
    }

done:
    /* TODO For backward compatibility we always return EOK to DP now. */
    tevent_req_done(req);
}

errno_t
ipa_pam_access_handler_recv(TALLOC_CTX *mem_ctx,
                             struct tevent_req *req,
                             struct pam_data **_data)
{
    struct ipa_pam_access_handler_state *state = NULL;

    state = tevent_req_data(req, struct ipa_pam_access_handler_state);

    TEVENT_REQ_RETURN_ON_ERROR(req);

    *_data = talloc_steal(mem_ctx, state->pd);

    return EOK;
}
