/*
    Authors:
        Pavel Březina <pbrezina@redhat.com>

    Copyright (C) 2011 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 <errno.h>
#include <string.h>
#include <tevent.h>

#include "providers/dp_backend.h"
#include "providers/ldap/ldap_common.h"
#include "providers/ldap/sdap.h"
#include "providers/ldap/sdap_async.h"
#include "providers/ldap/sdap_sudo.h"
#include "providers/ldap/sdap_sudo_cache.h"
#include "db/sysdb_sudo.h"

#define SUDO_MAX_FIRST_REFRESH_DELAY 16

struct sdap_sudo_full_refresh_state {
    struct sdap_sudo_ctx *sudo_ctx;
    struct sdap_id_ctx *id_ctx;
    struct sysdb_ctx *sysdb;
    struct sss_domain_info *domain;
    int dp_error;
    int error;
};

static struct tevent_req *sdap_sudo_full_refresh_send(TALLOC_CTX *mem_ctx,
                                                      struct sdap_sudo_ctx *sudo_ctx);

static void sdap_sudo_full_refresh_done(struct tevent_req *subreq);

static int sdap_sudo_full_refresh_recv(struct tevent_req *req,
                                       int *dp_error,
                                       int *error);

struct sdap_sudo_rules_refresh_state {
    struct sdap_id_ctx *id_ctx;
    size_t num_rules;
    int dp_error;
    int error;
};

static struct tevent_req *sdap_sudo_rules_refresh_send(TALLOC_CTX *mem_ctx,
                                                       struct sdap_sudo_ctx *sudo_ctx,
                                                       struct be_ctx *be_ctx,
                                                       struct sdap_options *opts,
                                                       struct sdap_id_conn_cache *conn_cache,
                                                       char **rules);

static void sdap_sudo_rules_refresh_done(struct tevent_req *subreq);

static int sdap_sudo_rules_refresh_recv(struct tevent_req *req,
                                        int *dp_error,
                                        int *error);

struct sdap_sudo_smart_refresh_state {
    struct tevent_req *subreq;
    struct sdap_id_ctx *id_ctx;
    struct sysdb_ctx *sysdb;
};

static struct tevent_req *sdap_sudo_smart_refresh_send(TALLOC_CTX *mem_ctx,
                                                      struct sdap_sudo_ctx *sudo_ctx);

static void sdap_sudo_smart_refresh_done(struct tevent_req *subreq);

static int sdap_sudo_smart_refresh_recv(struct tevent_req *req,
                                        int *dp_error,
                                        int *error);

static void sdap_sudo_periodical_first_refresh_done(struct tevent_req *req);

static void sdap_sudo_periodical_full_refresh_done(struct tevent_req *req);

static void sdap_sudo_periodical_smart_refresh_done(struct tevent_req *req);

static int sdap_sudo_schedule_refresh(TALLOC_CTX *mem_ctx,
                                      struct sdap_sudo_ctx *sudo_ctx,
                                      enum sdap_sudo_refresh_type refresh,
                                      tevent_req_fn callback,
                                      time_t delay,
                                      time_t timeout,
                                      struct tevent_req **_req);

static int sdap_sudo_schedule_full_refresh(struct sdap_sudo_ctx *sudo_ctx,
                                           time_t delay);

static int sdap_sudo_schedule_smart_refresh(struct sdap_sudo_ctx *sudo_ctx,
                                            time_t delay);

static void
sdap_sudo_shutdown(struct be_req *req)
{
    sdap_handler_done(req, DP_ERR_OK, EOK, NULL);
}

struct bet_ops sdap_sudo_ops = {
    .handler = sdap_sudo_handler,
    .finalize = sdap_sudo_shutdown
};

static void sdap_sudo_get_hostinfo_done(struct tevent_req *req);
static int sdap_sudo_setup_periodical_refresh(struct sdap_sudo_ctx *sudo_ctx);

int sdap_sudo_init(struct be_ctx *be_ctx,
                   struct sdap_id_ctx *id_ctx,
                   struct bet_ops **ops,
                   void **pvt_data)
{
    struct sdap_sudo_ctx *sudo_ctx = NULL;
    struct tevent_req *req = NULL;
    int ret;

    DEBUG(SSSDBG_TRACE_INTERNAL, "Initializing sudo LDAP back end\n");

    sudo_ctx = talloc_zero(be_ctx, struct sdap_sudo_ctx);
    if (sudo_ctx == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc() failed\n");
        return ENOMEM;
    }

    sudo_ctx->id_ctx = id_ctx;
    *ops = &sdap_sudo_ops;
    *pvt_data = sudo_ctx;

    /* we didn't do any full refresh now,
     * so we don't have current usn values available */
    sudo_ctx->full_refresh_done = false;

    ret = ldap_get_sudo_options(id_ctx, be_ctx->cdb,
                                be_ctx->conf_path, id_ctx->opts,
                                &sudo_ctx->use_host_filter,
                                &sudo_ctx->include_regexp,
                                &sudo_ctx->include_netgroups);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Cannot get SUDO options [%d]: %s\n",
                                  ret, strerror(ret));
        return ret;
    }

    req = sdap_sudo_get_hostinfo_send(sudo_ctx, id_ctx->opts, be_ctx);
    if (req == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Unable to retrieve host information - "
              "(host filter will be disabled)\n");

        sudo_ctx->use_host_filter = false;

        ret = sdap_sudo_setup_periodical_refresh(sudo_ctx);
        if (ret != EOK) {
             DEBUG(SSSDBG_OP_FAILURE,
                   "Unable to setup periodical refresh"
                    "of sudo rules [%d]: %s\n", ret, strerror(ret));
             /* periodical updates will not work, but specific-rule update
              * is no affected by this, therefore we don't have to fail here */
        }
    } else {
        tevent_req_set_callback(req, sdap_sudo_get_hostinfo_done, sudo_ctx);
    }

    return EOK;
}

static void sdap_sudo_get_hostinfo_done(struct tevent_req *req)
{
    struct sdap_sudo_ctx *sudo_ctx = NULL;
    char **hostnames = NULL;
    char **ip_addr = NULL;
    int ret;

    sudo_ctx = tevent_req_callback_data(req, struct sdap_sudo_ctx);

    ret = sdap_sudo_get_hostinfo_recv(sudo_ctx, req, &hostnames, &ip_addr);
    talloc_zfree(req);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Unable to retrieve host information - "
              "(host filter will be disabled) [%d]: %s\n", ret, strerror(ret));
        sudo_ctx->use_host_filter = false;
    }

    talloc_zfree(sudo_ctx->hostnames);
    talloc_zfree(sudo_ctx->ip_addr);

    sudo_ctx->hostnames = talloc_move(sudo_ctx, &hostnames);
    sudo_ctx->ip_addr = talloc_move(sudo_ctx, &ip_addr);

    ret = sdap_sudo_setup_periodical_refresh(sudo_ctx);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Unable to setup periodical refresh"
                                  "of sudo rules [%d]: %s\n", ret, strerror(ret));
    }
}

static int sdap_sudo_setup_periodical_refresh(struct sdap_sudo_ctx *sudo_ctx)
{
    struct sdap_id_ctx *id_ctx = sudo_ctx->id_ctx;
    time_t smart_default;
    time_t smart_interval;
    time_t full_interval;
    time_t last_full;
    time_t delay;
    int ret;

    smart_interval = dp_opt_get_int(id_ctx->opts->basic,
                                    SDAP_SUDO_SMART_REFRESH_INTERVAL);

    full_interval = dp_opt_get_int(id_ctx->opts->basic,
                                   SDAP_SUDO_FULL_REFRESH_INTERVAL);

    if (smart_interval == 0 && full_interval == 0) {
        smart_default = id_ctx->opts->basic[SDAP_SUDO_SMART_REFRESH_INTERVAL].def_val.number;

        DEBUG(SSSDBG_MINOR_FAILURE, "At least one periodical update has to be "
              "enabled. Setting smart refresh interval to default value (%ld).\n",
              smart_default);

        ret = dp_opt_set_int(id_ctx->opts->basic,
                             SDAP_SUDO_SMART_REFRESH_INTERVAL,
                             smart_default);
        if (ret != EOK) {
            return ret;
        }
    }

    if (full_interval <= smart_interval) {
        DEBUG(SSSDBG_MINOR_FAILURE, "Full refresh interval has to be greater"
              "than smart refresh interval. Periodical full refresh will be "
              "disabled.\n");
        ret = dp_opt_set_int(id_ctx->opts->basic,
                             SDAP_SUDO_FULL_REFRESH_INTERVAL,
                             0);
        if (ret != EOK) {
            return ret;
        }
    }

    ret = sysdb_sudo_get_last_full_refresh(id_ctx->be->domain->sysdb,
                                           id_ctx->be->domain,
                                           &last_full);
    if (ret != EOK) {
        return ret;
    }

    if (last_full == 0) {
        /* If this is the first startup, we need to kick off
         * an refresh immediately, to close a window where
         * clients requesting sudo information won't get an
         * immediate reply with no entries
         */
        delay = 0;
    } else {
        /* At least one update has previously run,
         * so clients will get cached data.
         * We will delay the refresh so we don't slow
         * down the startup process if this is happening
         * during system boot.
         */

        /* delay at least by 10s */
        delay = 10;
    }

    ret = sdap_sudo_schedule_refresh(sudo_ctx, sudo_ctx,
                                     SDAP_SUDO_REFRESH_FULL,
                                     sdap_sudo_periodical_first_refresh_done,
                                     delay, full_interval, NULL);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Unable to schedule full refresh of sudo "
              "rules! Periodical updates will not work!\n");
        return ret;
    }

    return EOK;
}

static void sdap_sudo_set_usn(struct sdap_server_opts *srv_opts, char *usn)
{
    unsigned int usn_number;
    char *endptr = NULL;

    if (srv_opts != NULL && usn != NULL) {
        talloc_zfree(srv_opts->max_sudo_value);
        srv_opts->max_sudo_value = talloc_steal(srv_opts, usn);

        usn_number = strtoul(usn, &endptr, 10);
        if ((endptr == NULL || (*endptr == '\0' && endptr != usn))
             && (usn_number > srv_opts->last_usn)) {
             srv_opts->last_usn = usn_number;
        }

        DEBUG(SSSDBG_FUNC_DATA, "SUDO higher USN value: [%s]\n",
                                 srv_opts->max_sudo_value);
    } else {
        DEBUG(SSSDBG_TRACE_FUNC, "srv_opts is NULL\n");
    }
}

static char *sdap_sudo_build_host_filter(TALLOC_CTX *mem_ctx,
                                         struct sdap_attr_map *map,
                                         char **hostnames,
                                         char **ip_addr,
                                         bool netgroups,
                                         bool regexp)
{
    TALLOC_CTX *tmp_ctx = NULL;
    char *filter = NULL;
    int i;

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

    filter = talloc_strdup(tmp_ctx, "(|");
    if (filter == NULL) {
        goto done;
    }

    /* sudoHost is not specified */
    filter = talloc_asprintf_append_buffer(filter, "(!(%s=*))",
                                           map[SDAP_AT_SUDO_HOST].name);
    if (filter == NULL) {
        goto done;
    }

    /* ALL */
    filter = talloc_asprintf_append_buffer(filter, "(%s=ALL)",
                                           map[SDAP_AT_SUDO_HOST].name);
    if (filter == NULL) {
        goto done;
    }

    /* hostnames */
    if (hostnames != NULL) {
        for (i = 0; hostnames[i] != NULL; i++) {
            filter = talloc_asprintf_append_buffer(filter, "(%s=%s)",
                                                   map[SDAP_AT_SUDO_HOST].name,
                                                   hostnames[i]);
            if (filter == NULL) {
                goto done;
            }
        }
    }

    /* ip addresses and networks */
    if (ip_addr != NULL) {
        for (i = 0; ip_addr[i] != NULL; i++) {
            filter = talloc_asprintf_append_buffer(filter, "(%s=%s)",
                                                   map[SDAP_AT_SUDO_HOST].name,
                                                   ip_addr[i]);
            if (filter == NULL) {
                goto done;
            }
        }
    }

    /* sudoHost contains netgroup - will be filtered more by sudo */
    if (netgroups) {
        filter = talloc_asprintf_append_buffer(filter, SDAP_SUDO_FILTER_NETGROUP,
                                               map[SDAP_AT_SUDO_HOST].name,
                                               "*");
        if (filter == NULL) {
            goto done;
        }
    }

    /* sudoHost contains regexp - will be filtered more by sudo */
    /* from sudo match.c :
     * #define has_meta(s)  (strpbrk(s, "\\?*[]") != NULL)
     */
    if (regexp) {
        filter = talloc_asprintf_append_buffer(filter,
                                               "(|(%s=*\\\\*)(%s=*?*)(%s=*\\2A*)"
                                                 "(%s=*[*]*))",
                                               map[SDAP_AT_SUDO_HOST].name,
                                               map[SDAP_AT_SUDO_HOST].name,
                                               map[SDAP_AT_SUDO_HOST].name,
                                               map[SDAP_AT_SUDO_HOST].name);
        if (filter == NULL) {
            goto done;
        }
    }

    filter = talloc_strdup_append_buffer(filter, ")");
    if (filter == NULL) {
        goto done;
    }

    talloc_steal(mem_ctx, filter);

done:
    talloc_free(tmp_ctx);

    return filter;
}

static char *sdap_sudo_get_filter(TALLOC_CTX *mem_ctx,
                                  struct sdap_attr_map *map,
                                  struct sdap_sudo_ctx *sudo_ctx,
                                  const char *rule_filter)
{
    TALLOC_CTX *tmp_ctx = NULL;
    char *host_filter = NULL;
    char *filter = NULL;

    if (!sudo_ctx->use_host_filter) {
        return talloc_strdup(mem_ctx, rule_filter);
    }

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

    host_filter = sdap_sudo_build_host_filter(tmp_ctx, map,
                                              sudo_ctx->hostnames,
                                              sudo_ctx->ip_addr,
                                              sudo_ctx->include_netgroups,
                                              sudo_ctx->include_regexp);
    if (host_filter == NULL) {
        goto done;
    }

    filter = sdap_get_id_specific_filter(tmp_ctx, rule_filter, host_filter);
    if (filter == NULL) {
        goto done;
    }

    talloc_steal(mem_ctx, filter);

done:
    talloc_free(tmp_ctx);
    return filter;
}

static void sdap_sudo_reply(struct tevent_req *req)
{
    struct be_req *be_req = NULL;
    struct be_sudo_req *sudo_req = NULL;
    int dp_error = DP_ERR_OK;
    int error = EOK;
    int ret;

    be_req = tevent_req_callback_data(req, struct be_req);
    sudo_req = talloc_get_type(be_req_get_data(be_req), struct be_sudo_req);

    switch (sudo_req->type) {
    case BE_REQ_SUDO_FULL:
        ret = sdap_sudo_full_refresh_recv(req, &dp_error, &error);
        break;
    case BE_REQ_SUDO_RULES:
        ret = sdap_sudo_rules_refresh_recv(req, &dp_error, &error);
        break;
    default:
        DEBUG(SSSDBG_CRIT_FAILURE, "Invalid request type: %d\n",
                                    sudo_req->type);
        ret = EINVAL;
    }

    talloc_zfree(req);
    if (ret != EOK) {
        sdap_handler_done(be_req, DP_ERR_FATAL, ret, strerror(ret));
        return;
    }

    sdap_handler_done(be_req, dp_error, error, strerror(error));
}

void sdap_sudo_handler(struct be_req *be_req)
{
    struct be_ctx *be_ctx = be_req_get_be_ctx(be_req);
    struct tevent_req *req = NULL;
    struct be_sudo_req *sudo_req = NULL;
    struct sdap_sudo_ctx *sudo_ctx = NULL;
    struct sdap_id_ctx *id_ctx = NULL;
    int ret = EOK;

    sudo_ctx = talloc_get_type(be_ctx->bet_info[BET_SUDO].pvt_bet_data,
                               struct sdap_sudo_ctx);
    id_ctx = sudo_ctx->id_ctx;

    sudo_req = talloc_get_type(be_req_get_data(be_req), struct be_sudo_req);

    switch (sudo_req->type) {
    case BE_REQ_SUDO_FULL:
        DEBUG(SSSDBG_TRACE_FUNC, "Issuing a full refresh of sudo rules\n");
        req = sdap_sudo_full_refresh_send(be_req, sudo_ctx);
        break;
    case BE_REQ_SUDO_RULES:
        DEBUG(SSSDBG_TRACE_FUNC, "Issuing a refresh of specific sudo rules\n");
        req = sdap_sudo_rules_refresh_send(be_req, sudo_ctx, id_ctx->be,
                                           id_ctx->opts, id_ctx->conn->conn_cache,
                                           sudo_req->rules);
        break;
    default:
        DEBUG(SSSDBG_CRIT_FAILURE, "Invalid request type: %d\n",
                                    sudo_req->type);
        ret = EINVAL;
        goto fail;
    }

    if (req == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Unable to send request: %d\n",
                                    sudo_req->type);
        ret = ENOMEM;
        goto fail;
    }

    tevent_req_set_callback(req, sdap_sudo_reply, be_req);

    return;

fail:
    sdap_handler_done(be_req, DP_ERR_FATAL, ret, NULL);
}

/* issue full refresh of sudo rules */
static struct tevent_req *sdap_sudo_full_refresh_send(TALLOC_CTX *mem_ctx,
                                                      struct sdap_sudo_ctx *sudo_ctx)
{
    struct tevent_req *req = NULL;
    struct tevent_req *subreq = NULL;
    struct sdap_id_ctx *id_ctx = sudo_ctx->id_ctx;
    struct sdap_sudo_full_refresh_state *state = NULL;
    char *ldap_filter = NULL;
    char *ldap_full_filter = NULL;
    char *sysdb_filter = NULL;
    int ret;

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

    sudo_ctx->full_refresh_in_progress = true;

    state->sudo_ctx = sudo_ctx;
    state->id_ctx = id_ctx;
    state->sysdb = id_ctx->be->domain->sysdb;
    state->domain = id_ctx->be->domain;

    /* Download all rules from LDAP */
    ldap_filter = talloc_asprintf(state, SDAP_SUDO_FILTER_CLASS,
                                  id_ctx->opts->sudorule_map[SDAP_OC_SUDORULE].name);
    if (ldap_filter == NULL) {
        ret = ENOMEM;
        goto immediately;
    }

    ldap_full_filter = sdap_sudo_get_filter(state, id_ctx->opts->sudorule_map,
                                            sudo_ctx, ldap_filter);
    if (ldap_full_filter == NULL) {
        ret = ENOMEM;
        goto immediately;
    }

    /* Remove all rules from cache */
    sysdb_filter = talloc_asprintf(state, "(%s=%s)",
                                   SYSDB_OBJECTCLASS, SYSDB_SUDO_CACHE_OC);
    if (sysdb_filter == NULL) {
        ret = ENOMEM;
        goto immediately;
    }

    DEBUG(SSSDBG_TRACE_FUNC, "Issuing a full refresh of sudo rules\n");

    subreq = sdap_sudo_refresh_send(state, id_ctx->be, id_ctx->opts,
                                    id_ctx->conn->conn_cache,
                                    ldap_full_filter, sysdb_filter);
    if (subreq == NULL) {
        ret = ENOMEM;
        goto immediately;
    }

    tevent_req_set_callback(subreq, sdap_sudo_full_refresh_done, req);

    /* free filters */
    talloc_free(ldap_filter);
    talloc_free(ldap_full_filter);
    talloc_free(sysdb_filter);

    return req;

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

    return req;
}

static void sdap_sudo_full_refresh_done(struct tevent_req *subreq)
{
    struct tevent_req *req = NULL;
    struct sdap_sudo_full_refresh_state *state = NULL;
    char *highest_usn = NULL;
    int ret;

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

    ret = sdap_sudo_refresh_recv(state, subreq, &state->dp_error,
                                 &state->error, &highest_usn, NULL);
    talloc_zfree(subreq);
    if (ret != EOK || state->dp_error != DP_ERR_OK || state->error != EOK) {
        goto done;
    }

    state->sudo_ctx->full_refresh_done = true;

    /* save the time in the sysdb */
    ret = sysdb_sudo_set_last_full_refresh(state->sysdb, state->domain,
                                           time(NULL));
    if (ret != EOK) {
        DEBUG(SSSDBG_MINOR_FAILURE, "Unable to save time of "
                                     "a successful full refresh\n");
        /* this is only a minor error that does not affect the functionality,
         * therefore there is no need to report it with tevent_req_error()
         * which would cause problems in the consumers */
    }

    DEBUG(SSSDBG_TRACE_FUNC, "Successful full refresh of sudo rules\n");

    /* set highest usn */
    if (highest_usn != NULL) {
        sdap_sudo_set_usn(state->id_ctx->srv_opts, highest_usn);
    }

done:
    state->sudo_ctx->full_refresh_in_progress = false;

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

    tevent_req_done(req);
}

static int sdap_sudo_full_refresh_recv(struct tevent_req *req,
                                       int *dp_error,
                                       int *error)
{
    struct sdap_sudo_full_refresh_state *state = NULL;
    state = tevent_req_data(req, struct sdap_sudo_full_refresh_state);

    TEVENT_REQ_RETURN_ON_ERROR(req);

    *dp_error = state->dp_error;
    *error = state->error;

    return EOK;
}

/* issue refresh of specific sudo rules */
static struct tevent_req *sdap_sudo_rules_refresh_send(TALLOC_CTX *mem_ctx,
                                                       struct sdap_sudo_ctx *sudo_ctx,
                                                       struct be_ctx *be_ctx,
                                                       struct sdap_options *opts,
                                                       struct sdap_id_conn_cache *conn_cache,
                                                       char **rules)
{
    struct tevent_req *req = NULL;
    struct tevent_req *subreq = NULL;
    struct sdap_sudo_rules_refresh_state *state = NULL;
    TALLOC_CTX *tmp_ctx = NULL;
    char *ldap_filter = NULL;
    char *ldap_full_filter = NULL;
    char *sysdb_filter = NULL;
    char *safe_rule = NULL;
    int ret;
    int i;

    if (rules == NULL) {
        return NULL;
    }

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

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

    ldap_filter = talloc_zero(tmp_ctx, char); /* assign to tmp_ctx */
    sysdb_filter = talloc_zero(tmp_ctx, char); /* assign to tmp_ctx */

    /* Download only selected rules from LDAP */
    /* Remove all selected rules from cache */
    for (i = 0; rules[i] != NULL; i++) {
        ret = sss_filter_sanitize(tmp_ctx, rules[i], &safe_rule);
        if (ret != EOK) {
            ret = ENOMEM;
            goto immediately;
        }

        ldap_filter = talloc_asprintf_append_buffer(ldap_filter, "(%s=%s)",
                                     opts->sudorule_map[SDAP_AT_SUDO_NAME].name,
                                     safe_rule);
        if (ldap_filter == NULL) {
            ret = ENOMEM;
            goto immediately;
        }

        sysdb_filter = talloc_asprintf_append_buffer(sysdb_filter, "(%s=%s)",
                                                     SYSDB_SUDO_CACHE_AT_CN,
                                                     safe_rule);
        if (sysdb_filter == NULL) {
            ret = ENOMEM;
            goto immediately;
        }
    }

    state->id_ctx = sudo_ctx->id_ctx;
    state->num_rules = i;

    ldap_filter = talloc_asprintf(tmp_ctx, "(&"SDAP_SUDO_FILTER_CLASS"(|%s))",
                                  opts->sudorule_map[SDAP_OC_SUDORULE].name,
                                  ldap_filter);
    if (ldap_filter == NULL) {
        ret = ENOMEM;
        goto immediately;
    }

    ldap_full_filter = sdap_sudo_get_filter(tmp_ctx, opts->sudorule_map,
                                            sudo_ctx, ldap_filter);
    if (ldap_full_filter == NULL) {
        ret = ENOMEM;
        goto immediately;
    }

    sysdb_filter = talloc_asprintf(tmp_ctx, "(&(%s=%s)(|%s))",
                                   SYSDB_OBJECTCLASS, SYSDB_SUDO_CACHE_OC,
                                   sysdb_filter);
    if (sysdb_filter == NULL) {
        ret = ENOMEM;
        goto immediately;
    }

    subreq = sdap_sudo_refresh_send(req, be_ctx, opts, conn_cache,
                                    ldap_full_filter, sysdb_filter);
    if (subreq == NULL) {
        ret = ENOMEM;
        goto immediately;
    }

    tevent_req_set_callback(subreq, sdap_sudo_rules_refresh_done, req);

    ret = EOK;
immediately:
    talloc_free(tmp_ctx);

    if (ret != EOK) {
        tevent_req_error(req, ret);
        tevent_req_post(req, be_ctx->ev);
    }

    return req;
}

static void sdap_sudo_rules_refresh_done(struct tevent_req *subreq)
{
    struct tevent_req *req = NULL;
    struct sdap_sudo_rules_refresh_state *state = NULL;
    char *highest_usn = NULL;
    size_t downloaded_rules_num;
    int ret;

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

    ret = sdap_sudo_refresh_recv(state, subreq, &state->dp_error, &state->error,
                                 &highest_usn, &downloaded_rules_num);
    talloc_zfree(subreq);
    if (ret != EOK || state->dp_error != DP_ERR_OK || state->error != EOK) {
        goto done;
    }

    /* set highest usn */
    if (highest_usn != NULL) {
        sdap_sudo_set_usn(state->id_ctx->srv_opts, highest_usn);
    }

    if (downloaded_rules_num != state->num_rules) {
        state->error = ENOENT;
    }

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

    tevent_req_done(req);
}

static int sdap_sudo_rules_refresh_recv(struct tevent_req *req,
                                        int *dp_error,
                                        int *error)
{
    struct sdap_sudo_rules_refresh_state *state = NULL;
    state = tevent_req_data(req, struct sdap_sudo_rules_refresh_state);

    TEVENT_REQ_RETURN_ON_ERROR(req);

    *dp_error = state->dp_error;
    *error = state->error;

    return EOK;
}

/* issue smart refresh of sudo rules */
static struct tevent_req *sdap_sudo_smart_refresh_send(TALLOC_CTX *mem_ctx,
                                                       struct sdap_sudo_ctx *sudo_ctx)
{
    struct tevent_req *req = NULL;
    struct tevent_req *subreq = NULL;
    struct sdap_id_ctx *id_ctx = sudo_ctx->id_ctx;
    struct sdap_attr_map *map = id_ctx->opts->sudorule_map;
    struct sdap_server_opts *srv_opts = id_ctx->srv_opts;
    struct sdap_sudo_smart_refresh_state *state = NULL;
    char *ldap_filter = NULL;
    char *ldap_full_filter = NULL;
    const char *usn;
    int ret;

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

    if (!sudo_ctx->full_refresh_done
            && (srv_opts == NULL || srv_opts->max_sudo_value == 0)) {
        /* Perform full refresh first */
        DEBUG(SSSDBG_TRACE_FUNC, "USN value is unknown, "
                                  "waiting for full refresh!\n");
        ret = EINVAL;
        goto immediately;
    }

    state->id_ctx = id_ctx;
    state->sysdb = id_ctx->be->domain->sysdb;

    /* Download all rules from LDAP that are newer than usn */
    usn = srv_opts->max_sudo_value;
    if (usn != NULL) {
        ldap_filter = talloc_asprintf(state,
                                      "(&(objectclass=%s)(%s>=%s)(!(%s=%s)))",
                                      map[SDAP_OC_SUDORULE].name,
                                      map[SDAP_AT_SUDO_USN].name, usn,
                                      map[SDAP_AT_SUDO_USN].name, usn);
    } else {
        /* no valid USN value known */
        ldap_filter = talloc_asprintf(state, SDAP_SUDO_FILTER_CLASS,
                                      map[SDAP_OC_SUDORULE].name);
    }
    if (ldap_filter == NULL) {
        ret = ENOMEM;
        goto immediately;
    }

    ldap_full_filter = sdap_sudo_get_filter(state, map, sudo_ctx, ldap_filter);
    if (ldap_full_filter == NULL) {
        ret = ENOMEM;
        goto immediately;
    }

    /* Do not remove any rules that are already in the sysdb
     * sysdb_filter = NULL; */

    DEBUG(SSSDBG_TRACE_FUNC, "Issuing a smart refresh of sudo rules "
                              "(USN > %s)\n", (usn == NULL ? "0" : usn));

    subreq = sdap_sudo_refresh_send(state, id_ctx->be, id_ctx->opts,
                                    id_ctx->conn->conn_cache,
                                    ldap_full_filter, NULL);
    if (subreq == NULL) {
        ret = ENOMEM;
        goto immediately;
    }

    state->subreq = subreq;
    tevent_req_set_callback(subreq, sdap_sudo_smart_refresh_done, req);

    /* free filters */
    talloc_free(ldap_filter);
    talloc_free(ldap_full_filter);

    return req;

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

    return req;
}

static void sdap_sudo_smart_refresh_done(struct tevent_req *subreq)
{
    struct tevent_req *req = NULL;
    struct sdap_sudo_smart_refresh_state *state = NULL;
    char *highest_usn = NULL;
    int dp_error;
    int error;
    int ret;

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

    ret = sdap_sudo_refresh_recv(state, subreq, &dp_error, &error,
                                 &highest_usn, NULL);
    if (ret != EOK || dp_error != DP_ERR_OK || error != EOK) {
        goto done;
    }

    DEBUG(SSSDBG_TRACE_FUNC, "Successful smart refresh of sudo rules\n");

    /* set highest usn */
    if (highest_usn != NULL) {
        sdap_sudo_set_usn(state->id_ctx->srv_opts, highest_usn);
    }

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

    tevent_req_done(req);
}

static int sdap_sudo_smart_refresh_recv(struct tevent_req *req,
                                        int *dp_error,
                                        int *error)
{
    struct sdap_sudo_smart_refresh_state *state = NULL;
    state = tevent_req_data(req, struct sdap_sudo_smart_refresh_state);

    TEVENT_REQ_RETURN_ON_ERROR(req);

    return sdap_sudo_refresh_recv(state, state->subreq, dp_error, error,
                                  NULL, NULL);
}

static void sdap_sudo_full_refresh_online_cb(void *pvt)
{
    struct sdap_sudo_ctx *sudo_ctx = NULL;
    time_t timeout;
    int ret;

    sudo_ctx = talloc_get_type(pvt, struct sdap_sudo_ctx);

    /* remove online callback */
    talloc_zfree(sudo_ctx->first_refresh_online_cb);

    /* schedule new first refresh only if this callback wasn't triggered
     * by ongoing full refresh */
    if (sudo_ctx->full_refresh_in_progress) {
        return;
    }

    /* otherwise cancel the concurrent timer for full refresh */
    talloc_zfree(sudo_ctx->first_refresh_timer);

    /* and fire full refresh immediately */
    timeout = dp_opt_get_int(sudo_ctx->id_ctx->opts->basic,
                             SDAP_SUDO_FULL_REFRESH_INTERVAL);
    if (timeout == 0) {
        /* runtime configuration change? */
        DEBUG(SSSDBG_TRACE_FUNC, "Periodical full refresh of sudo rules "
                                  "is disabled\n");
        return;
    }

    ret = sdap_sudo_schedule_refresh(sudo_ctx, sudo_ctx,
                                     SDAP_SUDO_REFRESH_FULL,
                                     sdap_sudo_periodical_first_refresh_done,
                                     0, timeout, NULL);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Unable to schedule full refresh of sudo "
              "rules! Periodical updates will not work!\n");
    }
}

static void sdap_sudo_periodical_first_refresh_done(struct tevent_req *req)
{
    struct tevent_req *subreq = NULL; /* req from sdap_sudo_full_refresh_send() */
    struct sdap_sudo_ctx *sudo_ctx = NULL;
    time_t delay;
    time_t timeout;
    int dp_error = DP_ERR_OK;
    int error = EOK;
    int ret;

    ret = sdap_sudo_timer_recv(req, req, &subreq);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Sudo timer failed [%d]: %s\n", ret, strerror(ret));
        goto schedule;
    }

    ret = sdap_sudo_full_refresh_recv(subreq, &dp_error, &error);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Periodical full refresh of sudo rules "
                                  "failed [%d]: %s)\n", ret, strerror(ret));
        goto schedule;
    }

    if (dp_error != DP_ERR_OK || error != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Periodical full refresh of sudo rules "
                                  "failed [dp_error: %d] ([%d]: %s)\n",
                                  dp_error, error, strerror(error));
        goto schedule;
    }

schedule:
    sudo_ctx = tevent_req_callback_data(req, struct sdap_sudo_ctx);
    if (sudo_ctx->first_refresh_timer == req) {
        sudo_ctx->first_refresh_timer = NULL;
    }
    talloc_zfree(req);

    /* full refresh */
    delay = dp_opt_get_int(sudo_ctx->id_ctx->opts->basic,
                           SDAP_SUDO_FULL_REFRESH_INTERVAL);
    if (delay == 0) {
        /* runtime configuration change? */
        DEBUG(SSSDBG_TRACE_FUNC, "Periodical full refresh of sudo rules "
                                  "is disabled\n");
        return;
    }

    /* if we are offline, we will try to perform another full refresh */
    if (dp_error == DP_ERR_OFFLINE) {
        sudo_ctx->full_refresh_attempts++;
        timeout = delay;
        delay = sudo_ctx->full_refresh_attempts << 1;
        if (delay > SUDO_MAX_FIRST_REFRESH_DELAY) {
            delay = SUDO_MAX_FIRST_REFRESH_DELAY;
        }

        DEBUG(SSSDBG_TRACE_FUNC, "Data provider is offline. "
              "Scheduling another full refresh in %ld minutes.\n", delay);

        ret = sdap_sudo_schedule_refresh(sudo_ctx, sudo_ctx,
                                         SDAP_SUDO_REFRESH_FULL,
                                         sdap_sudo_periodical_first_refresh_done,
                                         delay * 60, timeout,
                                         &sudo_ctx->first_refresh_timer);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Unable to schedule full refresh of sudo "
                  "rules! Periodical updates will not work!\n");
        }

        /* also setup online callback to make sure the refresh is fired as soon
         * as possible */
        ret = be_add_online_cb(sudo_ctx->id_ctx->be, sudo_ctx->id_ctx->be,
                               sdap_sudo_full_refresh_online_cb,
                               sudo_ctx, &sudo_ctx->first_refresh_online_cb);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Could not set up online callback\n");
        }

        return;
    }

    ret = sdap_sudo_schedule_full_refresh(sudo_ctx, delay);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Full periodical refresh will not work.\n");
    }

    /* smart refresh */
    delay = dp_opt_get_int(sudo_ctx->id_ctx->opts->basic,
                           SDAP_SUDO_SMART_REFRESH_INTERVAL);
    if (delay == 0) {
        /* runtime configuration change? */
        DEBUG(SSSDBG_TRACE_FUNC, "Periodical smart refresh of sudo rules "
                                  "is disabled\n");
        return;
    }

    ret = sdap_sudo_schedule_smart_refresh(sudo_ctx, delay);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Smart periodical refresh will not work.\n");
    }
}

static void sdap_sudo_periodical_full_refresh_done(struct tevent_req *req)
{
    struct tevent_req *subreq = NULL; /* req from sdap_sudo_full_refresh_send() */
    struct sdap_sudo_ctx *sudo_ctx = NULL;
    time_t delay;
    int dp_error = DP_ERR_FATAL;
    int error = EFAULT;
    int ret;

    ret = sdap_sudo_timer_recv(req, req, &subreq);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Sudo timer failed [%d]: %s\n", ret, strerror(ret));
        goto schedule;
    }

    ret = sdap_sudo_full_refresh_recv(subreq, &dp_error, &error);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Periodical full refresh of sudo rules "
                                  "failed [%d]: %s)\n", ret, strerror(ret));
        goto schedule;
    }

    if (dp_error != DP_ERR_OK || error != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Periodical full refresh of sudo rules "
                                  "failed [dp_error: %d] ([%d]: %s)\n",
                                  dp_error, error, strerror(error));
        goto schedule;
    }

schedule:
    sudo_ctx = tevent_req_callback_data(req, struct sdap_sudo_ctx);
    talloc_zfree(req);

    delay = dp_opt_get_int(sudo_ctx->id_ctx->opts->basic,
                           SDAP_SUDO_FULL_REFRESH_INTERVAL);
    if (delay == 0) {
        /* runtime configuration change? */
        DEBUG(SSSDBG_TRACE_FUNC, "Periodical full refresh of sudo rules "
                                  "is disabled\n");
        return;
    }

    ret = sdap_sudo_schedule_full_refresh(sudo_ctx, delay);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Full periodical refresh will not work.\n");
    }
}

static void sdap_sudo_periodical_smart_refresh_done(struct tevent_req *req)
{
    struct tevent_req *subreq = NULL; /* req from sdap_sudo_smart_refresh_send() */
    struct sdap_sudo_ctx *sudo_ctx = NULL;
    time_t delay;
    int dp_error;
    int error;
    int ret;

    ret = sdap_sudo_timer_recv(req, req, &subreq);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Sudo timer failed [%d]: %s\n", ret, strerror(ret));
        goto schedule;
    }

    ret = sdap_sudo_smart_refresh_recv(subreq, &dp_error, &error);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Periodical smart refresh of sudo rules "
                                  "failed [%d]: %s\n", ret, strerror(ret));
    }

    if (dp_error != DP_ERR_OK || error != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Periodical smart refresh of sudo rules "
                                  "failed [dp_error: %d] ([%d]: %s)\n",
                                  dp_error, error, strerror(error));
        goto schedule;
    }

schedule:
    sudo_ctx = tevent_req_callback_data(req, struct sdap_sudo_ctx);
    talloc_zfree(req);

    delay = dp_opt_get_int(sudo_ctx->id_ctx->opts->basic,
                           SDAP_SUDO_SMART_REFRESH_INTERVAL);
    if (delay == 0) {
        /* runtime configuration change? */
        DEBUG(SSSDBG_TRACE_FUNC, "Periodical smart refresh of sudo rules "
                                  "is disabled\n");
        return;
    }

    ret = sdap_sudo_schedule_smart_refresh(sudo_ctx, delay);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Smart periodical refresh will not work.\n");
    }
}

static int sdap_sudo_schedule_refresh(TALLOC_CTX *mem_ctx,
                                      struct sdap_sudo_ctx *sudo_ctx,
                                      enum sdap_sudo_refresh_type refresh,
                                      tevent_req_fn callback,
                                      time_t delay,
                                      time_t timeout,
                                      struct tevent_req **_req)
{
    struct tevent_req *req = NULL;
    sdap_sudo_timer_fn_t send_fn = NULL;
    const char *name = NULL;
    struct timeval when;

    when = tevent_timeval_current_ofs(delay, 0);

    switch (refresh) {
    case SDAP_SUDO_REFRESH_FULL:
        send_fn = sdap_sudo_full_refresh_send;
        name = "Full refresh";
        break;
    case SDAP_SUDO_REFRESH_SMART:
        send_fn = sdap_sudo_smart_refresh_send;
        name = "Smart refresh";
        break;
    case SDAP_SUDO_REFRESH_RULES:
        DEBUG(SSSDBG_OP_FAILURE, "Rules refresh can't be scheduled!\n");
        return EINVAL;
    default:
        DEBUG(SSSDBG_CRIT_FAILURE, "Unknown refresh type [%d].\n", refresh);
        return EINVAL;
    }



    req = sdap_sudo_timer_send(mem_ctx, sudo_ctx->id_ctx->be->ev, sudo_ctx,
                               when, timeout, send_fn);
    if (req == NULL) {
        return ENOMEM;
    }

    tevent_req_set_callback(req, callback, sudo_ctx);

    DEBUG(SSSDBG_TRACE_FUNC, "%s scheduled at: %lld\n",
                              name, (long long)when.tv_sec);

    if (_req != NULL) {
        *_req = req;
    }

    return EOK;
}

static int sdap_sudo_schedule_full_refresh(struct sdap_sudo_ctx *sudo_ctx,
                                           time_t delay)
{
    int ret;

    ret = sdap_sudo_schedule_refresh(sudo_ctx, sudo_ctx,
                                     SDAP_SUDO_REFRESH_FULL,
                                     sdap_sudo_periodical_full_refresh_done,
                                     delay, delay, NULL);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Unable to schedule full refresh of sudo "
              "rules!\n");
        return ret;
    }

    return EOK;
}

static int sdap_sudo_schedule_smart_refresh(struct sdap_sudo_ctx *sudo_ctx,
                                            time_t delay)
{
    int ret;

    ret = sdap_sudo_schedule_refresh(sudo_ctx, sudo_ctx,
                                     SDAP_SUDO_REFRESH_SMART,
                                     sdap_sudo_periodical_smart_refresh_done,
                                     delay, delay, NULL);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Unable to schedule smart refresh of sudo "
              "rules!\n");
        return ret;
    }

    return EOK;
}
