/*
    SSSD

    Authors:
        Yassir Elley <yelley@redhat.com>

    Copyright (C) 2013 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/>.
*/

/*
 * This file implements the following pair of *public* functions (see header):
 *   ad_gpo_access_send/recv: provides client-side GPO processing
 *
 * This file also implements the following pairs of *private* functions (which
 * are used by the public functions):
 *   ad_gpo_process_som_send/recv: populate list of gp_som objects
 *   ad_gpo_process_gpo_send/recv: populate list of gp_gpo objects
 *   ad_gpo_process_cse_send/recv: retrieve policy file data
 */

#include <ctype.h>
#include <security/pam_modules.h>
#include <syslog.h>
#include <fcntl.h>
#include <ini_configobj.h>
#include "util/util.h"
#include "util/strtonum.h"
#include "util/child_common.h"
#include "providers/data_provider.h"
#include "providers/backend.h"
#include "providers/ad/ad_access.h"
#include "providers/ad/ad_common.h"
#include "providers/ad/ad_domain_info.h"
#include "providers/ad/ad_gpo.h"
#include "providers/ad/ad_opts.h"
#include "providers/ldap/sdap_access.h"
#include "providers/ldap/sdap_async.h"
#include "providers/ldap/sdap.h"
#include "providers/ldap/sdap_idmap.h"
#include "util/util_sss_idmap.h"
#include "util/sss_chain_id.h"
#include <ndr.h>
#include <gen_ndr/security.h>

/* == gpo-ldap constants =================================================== */

#define AD_AT_DISPLAY_NAME "displayName"
#define AD_AT_DN "distinguishedName"
#define AD_AT_UAC "userAccountControl"
#define AD_AT_SAMACCOUNTNAME "sAMAccountName"
#define AD_AT_CONFIG_NC "configurationNamingContext"
#define AD_AT_GPLINK "gPLink"
#define AD_AT_GPOPTIONS "gpOptions"
#define AD_AT_NT_SEC_DESC "nTSecurityDescriptor"
#define AD_AT_CN "cn"
#define AD_AT_FILE_SYS_PATH "gPCFileSysPath"
#define AD_AT_MACHINE_EXT_NAMES "gPCMachineExtensionNames"
#define AD_AT_FUNC_VERSION "gPCFunctionalityVersion"
#define AD_AT_FLAGS "flags"
#define AD_AT_SID "objectSid"

#define UAC_WORKSTATION_TRUST_ACCOUNT 0x00001000
#define UAC_SERVER_TRUST_ACCOUNT 0x00002000
#define AD_AGP_GUID "edacfd8f-ffb3-11d1-b41d-00a0c968f939"
#define AD_AUTHENTICATED_USERS_SID "S-1-5-11"

/* == gpo-smb constants ==================================================== */

#define SMB_STANDARD_URI "smb://"
#define BUFSIZE 65536

#define RIGHTS_SECTION "Privilege Rights"
#define ALLOW_LOGON_INTERACTIVE "SeInteractiveLogonRight"
#define DENY_LOGON_INTERACTIVE "SeDenyInteractiveLogonRight"
#define ALLOW_LOGON_REMOTE_INTERACTIVE "SeRemoteInteractiveLogonRight"
#define DENY_LOGON_REMOTE_INTERACTIVE "SeDenyRemoteInteractiveLogonRight"
#define ALLOW_LOGON_NETWORK "SeNetworkLogonRight"
#define DENY_LOGON_NETWORK "SeDenyNetworkLogonRight"
#define ALLOW_LOGON_BATCH "SeBatchLogonRight"
#define DENY_LOGON_BATCH "SeDenyBatchLogonRight"
#define ALLOW_LOGON_SERVICE "SeServiceLogonRight"
#define DENY_LOGON_SERVICE "SeDenyServiceLogonRight"

#define GP_EXT_GUID_SECURITY "{827D319E-6EAC-11D2-A4EA-00C04F79F83A}"
#define GP_EXT_GUID_SECURITY_SUFFIX "/Machine/Microsoft/Windows NT/SecEdit/GptTmpl.inf"

#ifndef SSSD_LIBEXEC_PATH
#error "SSSD_LIBEXEC_PATH not defined"
#else
#define GPO_CHILD SSSD_LIBEXEC_PATH"/gpo_child"
#endif

#define GPO_CHILD_LOG_FILE "gpo_child"

/* If INI_PARSE_IGNORE_NON_KVP is not defined, use 0 (no effect) */
#ifndef INI_PARSE_IGNORE_NON_KVP
#define INI_PARSE_IGNORE_NON_KVP 0
#warning INI_PARSE_IGNORE_NON_KVP not defined.
#endif

/* == common data structures and declarations ============================= */

struct gp_som {
    const char *som_dn;
    struct gp_gplink **gplink_list;
    int num_gplinks;
};

struct gp_gplink {
    const char *gpo_dn;
    bool enforced;
};

struct gp_gpo {
    struct security_descriptor *gpo_sd;
    const char *gpo_dn;
    const char *gpo_dpname;
    const char *gpo_guid;
    const char *smb_server;
    const char *smb_share;
    const char *smb_path;
    const char **gpo_cse_guids;
    int num_gpo_cse_guids;
    int gpo_func_version;
    int gpo_flags;
    bool send_to_child;
    const char *policy_filename;
};

enum ace_eval_agp_status {
    AD_GPO_ACE_DENIED,
    AD_GPO_ACE_ALLOWED,
    AD_GPO_ACE_NEUTRAL
};

struct tevent_req *ad_gpo_process_som_send(TALLOC_CTX *mem_ctx,
                                           struct tevent_context *ev,
                                           struct sdap_id_conn_ctx *conn,
                                           struct ldb_context *ldb_ctx,
                                           struct sdap_id_op *sdap_op,
                                           struct sdap_options *opts,
                                           struct dp_option *ad_options,
                                           int timeout,
                                           const char *target_dn,
                                           const char *domain_name);
int ad_gpo_process_som_recv(struct tevent_req *req,
                            TALLOC_CTX *mem_ctx,
                            struct gp_som ***som_list);

struct tevent_req *
ad_gpo_process_gpo_send(TALLOC_CTX *mem_ctx,
                        struct tevent_context *ev,
                        struct sdap_id_op *sdap_op,
                        struct sdap_options *opts,
                        char *server_hostname,
                        struct sss_domain_info *host_domain,
                        struct ad_access_ctx *access_ctx,
                        int timeout,
                        struct gp_som **som_list);
int ad_gpo_process_gpo_recv(struct tevent_req *req,
                            TALLOC_CTX *mem_ctx,
                            struct gp_gpo ***candidate_gpos,
                            int *num_candidate_gpos);

struct tevent_req *ad_gpo_process_cse_send(TALLOC_CTX *mem_ctx,
                                           struct tevent_context *ev,
                                           bool send_to_child,
                                           struct sss_domain_info *domain,
                                           const char *gpo_guid,
                                           const char *gpo_dpname,
                                           const char *smb_server,
                                           const char *smb_share,
                                           const char *smb_path,
                                           const char *smb_cse_suffix,
                                           int cached_gpt_version,
                                           int gpo_timeout_option);

int ad_gpo_process_cse_recv(struct tevent_req *req);

/* == ad_gpo_parse_map_options and helpers ==================================*/

#define GPO_LOGIN "login"
#define GPO_SU "su"
#define GPO_SU_L "su-l"
#define GPO_GDM_FINGERPRINT "gdm-fingerprint"
#define GPO_GDM_PASSWORD "gdm-password"
#define GPO_GDM_SMARTCARD "gdm-smartcard"
#define GPO_KDM "kdm"
#define GPO_LIGHTDM "lightdm"
#define GPO_LXDM "lxdm"
#define GPO_SDDM "sddm"
#define GPO_UNITY "unity"
#define GPO_XDM "xdm"
#define GPO_SSHD "sshd"
#define GPO_FTP "ftp"
#define GPO_SAMBA "samba"
#ifdef HAVE_DEBIAN
#define GPO_CROND "cron"
#else
#define GPO_CROND "crond"
#endif
#define GPO_POLKIT "polkit-1"
#define GPO_SUDO "sudo"
#define GPO_SUDO_I "sudo-i"
#define GPO_SYSTEMD_USER "systemd-user"
#define GPO_COCKPIT "cockpit"

struct gpo_map_option_entry {
    enum gpo_map_type gpo_map_type;
    enum ad_basic_opt ad_basic_opt;
    const char **gpo_map_defaults;
    const char *allow_key;
    const char *deny_key;
};

const char *gpo_map_interactive_defaults[] =
    {GPO_LOGIN, GPO_SU, GPO_SU_L,
     GPO_GDM_FINGERPRINT, GPO_GDM_PASSWORD, GPO_GDM_SMARTCARD, GPO_KDM,
     GPO_LIGHTDM, GPO_LXDM, GPO_SDDM, GPO_UNITY, GPO_XDM, NULL};
const char *gpo_map_remote_interactive_defaults[] = {GPO_SSHD, GPO_COCKPIT,
                                                     NULL};
const char *gpo_map_network_defaults[] = {GPO_FTP, GPO_SAMBA, NULL};
const char *gpo_map_batch_defaults[] = {GPO_CROND, NULL};
const char *gpo_map_service_defaults[] = {NULL};
const char *gpo_map_permit_defaults[] = {GPO_POLKIT,
                                         GPO_SUDO, GPO_SUDO_I,
                                         GPO_SYSTEMD_USER,  NULL};
const char *gpo_map_deny_defaults[] = {NULL};

struct gpo_map_option_entry gpo_map_option_entries[] = {
    {GPO_MAP_INTERACTIVE, AD_GPO_MAP_INTERACTIVE, gpo_map_interactive_defaults,
     ALLOW_LOGON_INTERACTIVE, DENY_LOGON_INTERACTIVE},
    {GPO_MAP_REMOTE_INTERACTIVE, AD_GPO_MAP_REMOTE_INTERACTIVE,
     gpo_map_remote_interactive_defaults,
     ALLOW_LOGON_REMOTE_INTERACTIVE, DENY_LOGON_REMOTE_INTERACTIVE},
    {GPO_MAP_NETWORK, AD_GPO_MAP_NETWORK, gpo_map_network_defaults,
     ALLOW_LOGON_NETWORK, DENY_LOGON_NETWORK},
    {GPO_MAP_BATCH, AD_GPO_MAP_BATCH, gpo_map_batch_defaults,
     ALLOW_LOGON_BATCH, DENY_LOGON_BATCH},
    {GPO_MAP_SERVICE, AD_GPO_MAP_SERVICE, gpo_map_service_defaults,
     ALLOW_LOGON_SERVICE, DENY_LOGON_SERVICE},
    {GPO_MAP_PERMIT, AD_GPO_MAP_PERMIT, gpo_map_permit_defaults, NULL, NULL},
    {GPO_MAP_DENY, AD_GPO_MAP_DENY, gpo_map_deny_defaults, NULL, NULL},
};

static const char* gpo_map_type_string(int gpo_map_type)
{
    switch(gpo_map_type) {
    case GPO_MAP_INTERACTIVE:        return "Interactive";
    case GPO_MAP_REMOTE_INTERACTIVE: return "Remote Interactive";
    case GPO_MAP_NETWORK:            return "Network";
    case GPO_MAP_BATCH:              return "Batch";
    case GPO_MAP_SERVICE:            return "Service";
    case GPO_MAP_PERMIT:             return "Permitted";
    case GPO_MAP_DENY:               return "Denied";
    }
    return "-unknown-";  /* this helper is only used in logs */
}

static inline bool
ad_gpo_service_in_list(char **list, size_t nlist, const char *str)
{
    size_t i;

    for (i = 0; i < nlist; i++) {
        if (strcasecmp(list[i], str) == 0) {
            break;
        }
    }

    return (i < nlist) ? true : false;
}

errno_t
ad_gpo_parse_map_option_helper(enum gpo_map_type gpo_map_type,
                               hash_key_t key,
                               hash_table_t *options_table)
{
    hash_value_t val;
    int hret;
    int ret;

    hret = hash_lookup(options_table, &key, &val);
    if (hret != HASH_SUCCESS && hret != HASH_ERROR_KEY_NOT_FOUND) {
        DEBUG(SSSDBG_OP_FAILURE, "Error checking hash table: [%s]\n",
              hash_error_string(hret));
        ret = EINVAL;
        goto done;
    } else if (hret == HASH_SUCCESS) {
        /* handle unexpected case where mapping for key already exists */
        if (val.i == gpo_map_type) {
            /* mapping for key exists for same map type; no error */
            DEBUG(SSSDBG_TRACE_FUNC,
                  "PAM service %s maps to %s multiple times\n", key.str,
                  gpo_map_type_string(gpo_map_type));
            ret = EOK;
        } else {
            /* mapping for key exists for different map type; error! */
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "Configuration error: PAM service %s maps to both %s and "
                  "%s. If you are changing the default mappings of Group "
                  "Policy rules to PAM services using one of the ad_gpo_map_*"
                  " options make sure that the PAM service you add to one map "
                  "using the '+service' syntax is not already present in "
                  "another map by default (if it is then remove it from the "
                  "other map by using the '-service' syntax. Check manual "
                  "pages 'man sssd-ad' for details).\n", key.str,
                  gpo_map_type_string(val.i), gpo_map_type_string(gpo_map_type));
            sss_log(SSS_LOG_ERR,
                  "Configuration error: PAM service %s maps to both %s and "
                  "%s. If you are changing the default mappings of Group "
                  "Policy rules to PAM services using one of the ad_gpo_map_*"
                  " options make sure that the PAM service you add to one map "
                  "using the '+service' syntax is not already present in "
                  "another map by default (if it is then remove it from the "
                  "other map by using the '-service' syntax. Check manual "
                  "pages 'man sssd-ad' for details).\n", key.str,
                  gpo_map_type_string(val.i), gpo_map_type_string(gpo_map_type));
            ret = EINVAL;
        }
        goto done;
    } else {
        /* handle expected case where mapping for key doesn't already exist */
        val.type = HASH_VALUE_INT;
        val.i = gpo_map_type;

        hret = hash_enter(options_table, &key, &val);
        if (hret != HASH_SUCCESS) {
            DEBUG(SSSDBG_OP_FAILURE, "Error checking hash table: [%s]\n",
                  hash_error_string(hret));
            ret = EIO;
            goto done;
        }
        ret = EOK;
    }

done:
    return ret;
}

errno_t
ad_gpo_parse_map_option(TALLOC_CTX *mem_ctx,
                        enum gpo_map_type gpo_map_type,
                        hash_table_t *options_table,
                        char *conf_str,
                        const char **defaults)
{
    TALLOC_CTX *tmp_ctx;
    errno_t ret;
    char **conf_list = NULL;
    int conf_list_size = 0;
    char **add_list = NULL;
    char **remove_list = NULL;
    int ai = 0, ri = 0;
    int i;
    hash_key_t key;

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

    DEBUG(SSSDBG_TRACE_ALL, "gpo_map_type: %s\n",
          gpo_map_type_string(gpo_map_type));

    if (conf_str) {
        ret = split_on_separator(tmp_ctx, conf_str, ',', true, true,
                                 &conf_list, &conf_list_size);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE,
                  "Cannot parse list of service names %s: %d\n", conf_str, ret);
            ret = EINVAL;
            goto done;
        }

        add_list = talloc_zero_array(tmp_ctx, char *, conf_list_size);
        remove_list = talloc_zero_array(tmp_ctx, char *, conf_list_size);
        if (add_list == NULL || remove_list == NULL) {
            ret = ENOMEM;
            goto done;
        }
    }

    for (i = 0; i < conf_list_size; i++) {
        switch (conf_list[i][0]) {
        case '+':
            add_list[ai] = conf_list[i] + 1;
            ai++;
            continue;
        case '-':
            remove_list[ri] = conf_list[i] + 1;
            ri++;
            continue;
        default:
            DEBUG(SSSDBG_CRIT_FAILURE, "ad_gpo_map values must start with"
                  "either '+' (for adding service) or '-' (for removing service), "
                  "got '%s'\n",
                  conf_list[i]);
            ret = EINVAL;
            goto done;
        }
    }

    /* Start by adding explicitly added services ('+') to hashtable */
    for (i = 0; i < ai; i++) {
        /* if the service is explicitly configured to be removed, skip it */
        if (ad_gpo_service_in_list(remove_list, ri, add_list[i])) {
            continue;
        }

        key.type = HASH_KEY_STRING;
        key.str = (char *)add_list[i];

        ret = ad_gpo_parse_map_option_helper(gpo_map_type, key, options_table);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Invalid configuration: %d\n", ret);
            goto done;
        }

        DEBUG(SSSDBG_TRACE_ALL, "Explicitly added service: %s\n", key.str);
    }

    /* Add defaults to hashtable */
    for (i = 0; defaults[i]; i++) {
        /* if the service is explicitly configured to be removed, skip it */
        if (ad_gpo_service_in_list(remove_list, ri, defaults[i])) {
            continue;
        }

        key.type = HASH_KEY_STRING;
        key.str = talloc_strdup(mem_ctx, defaults[i]);

        ret = ad_gpo_parse_map_option_helper(gpo_map_type, key, options_table);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Invalid configuration: %d\n", ret);
            goto done;
        }

        DEBUG(SSSDBG_TRACE_ALL, "Default service (not explicitly removed): %s\n",
              key.str);
    }

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

errno_t
ad_gpo_parse_map_options(struct ad_access_ctx *access_ctx)
{
    char *gpo_default_right_config;
    enum gpo_map_type gpo_default_right;
    errno_t ret;
    int i;

    for (i = 0; i < GPO_MAP_NUM_OPTS; i++) {

        struct gpo_map_option_entry entry = gpo_map_option_entries[i];

        char *entry_config =  dp_opt_get_string(access_ctx->ad_options,
                                                entry.ad_basic_opt);

        ret = ad_gpo_parse_map_option(access_ctx, entry.gpo_map_type,
                                      access_ctx->gpo_map_options_table,
                                      entry_config, entry.gpo_map_defaults);

        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Invalid configuration: %d\n", ret);
            ret = EINVAL;
            goto fail;
        }
    }

    /* default right (applicable for services without any mapping) */
    gpo_default_right_config =
        dp_opt_get_string(access_ctx->ad_options, AD_GPO_DEFAULT_RIGHT);

    DEBUG(SSSDBG_TRACE_ALL, "gpo_default_right_config: %s\n",
          gpo_default_right_config);

    /* if default right not set in config, set them to DENY */
    if (gpo_default_right_config == NULL) {
        gpo_default_right = GPO_MAP_DENY;
    } else if (strncasecmp(gpo_default_right_config, "interactive",
                           strlen("interactive")) == 0) {
        gpo_default_right = GPO_MAP_INTERACTIVE;
    } else if (strncasecmp(gpo_default_right_config, "remote_interactive",
                           strlen("remote_interactive")) == 0) {
        gpo_default_right = GPO_MAP_REMOTE_INTERACTIVE;
    } else if (strncasecmp(gpo_default_right_config, "network",
                           strlen("network")) == 0) {
        gpo_default_right = GPO_MAP_NETWORK;
    } else if (strncasecmp(gpo_default_right_config, "batch",
                           strlen("batch")) == 0) {
        gpo_default_right = GPO_MAP_BATCH;
    } else if (strncasecmp(gpo_default_right_config, "service",
                           strlen("service")) == 0) {
        gpo_default_right = GPO_MAP_SERVICE;
    } else if (strncasecmp(gpo_default_right_config, "permit",
                           strlen("permit")) == 0) {
        gpo_default_right = GPO_MAP_PERMIT;
    } else if (strncasecmp(gpo_default_right_config, "deny",
                           strlen("deny")) == 0) {
        gpo_default_right = GPO_MAP_DENY;
    } else {
        ret = EINVAL;
        goto fail;
    }

    DEBUG(SSSDBG_TRACE_ALL, "gpo_default_right: %d\n", gpo_default_right);
    access_ctx->gpo_default_right = gpo_default_right;

fail:
    return ret;
}

/* == ad_gpo_access_send/recv helpers =======================================*/

static bool
ad_gpo_dom_sid_equal(const struct dom_sid *sid1, const struct dom_sid *sid2)
{
    int i;

    if (sid1 == sid2) {
        return true;
    }

    if (!sid1 || !sid2) {
        return false;
    }

    if (sid1->sid_rev_num != sid2->sid_rev_num) {
        return false;
    }

    for (i = 0; i < 6; i++) {
        if (sid1->id_auth[i] != sid2->id_auth[i]) {
            return false;
        }
    }

    if (sid1->num_auths != sid2->num_auths) {
        return false;
    }

    for (i = 0; i < sid1->num_auths; i++) {
        if (sid1->sub_auths[i] != sid2->sub_auths[i]) {
            return false;
        }
    }

    return true;
}

/*
 * This function retrieves the SID of the group with given gid.
 */
static char *
ad_gpo_get_primary_group_sid(TALLOC_CTX *mem_ctx,
                             gid_t gid,
                             struct sss_domain_info *domain,
                             struct sss_idmap_ctx *idmap_ctx)
{
    char *idmap_sid = NULL;
    const char *cache_sid;
    char *result;
    const char *attrs[] = {
        SYSDB_SID_STR,
        NULL
    };
    struct ldb_message *msg;
    int ret;

    if (gid == 0) {
        return NULL;
    }

    ret = sss_idmap_unix_to_sid(idmap_ctx, gid, &idmap_sid);
    if (ret == EOK) {
        result = talloc_strdup(mem_ctx, idmap_sid);
        sss_idmap_free_sid(idmap_ctx, idmap_sid);
        if (result == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, "Out of memory while getting SID of the group\n");
        }
        return result;
    }

    if (ret == IDMAP_EXTERNAL) {
        /* no ID mapping in this domain, search for the group object and get sid there */
        ret = sysdb_search_group_by_gid(mem_ctx, domain, gid, attrs, &msg);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Search for group '%"SPRIgid"' failded with error '%d'\n", gid, ret);
            return NULL;
        }

        cache_sid = ldb_msg_find_attr_as_string(msg, SYSDB_SID_STR, NULL);
        if (cache_sid == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, "Failed to get SID attribute of the group '%"SPRIgid"'\n", gid);
            return NULL;
        }

        result = talloc_strdup(mem_ctx, cache_sid);
        if (result == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, "Out of memory while getting group SID\n");
        }
        return result;
    }

    DEBUG(SSSDBG_OP_FAILURE, "Failed to get SID of primary the group '%"SPRIgid"'\n", gid);
    return NULL;
}

/*
 * This function retrieves the SIDs corresponding to the input user and returns
 * the user_sid, group_sids, and group_size in their respective output params.
 *
 * Note: since authentication must complete successfully before the
 * gpo access checks are called, we can safely assume that the user/computer
 * has been authenticated. As such, this function always adds the
 * AD_AUTHENTICATED_USERS_SID to the group_sids.
 */
static errno_t
ad_gpo_get_sids(TALLOC_CTX *mem_ctx,
                const char *user,
                struct sss_domain_info *domain,
                struct sss_idmap_ctx *idmap_ctx,
                const char **_user_sid,
                const char ***_group_sids,
                int *_group_size)
{
    TALLOC_CTX *tmp_ctx = NULL;
    struct ldb_result *res;
    int ret = 0;
    int i = 0;
    int num_group_sids = 0;
    const char *user_sid = NULL;
    const char *group_sid = NULL;
    const char **group_sids = NULL;
    gid_t orig_gid = 0;
    char *orig_gid_sid = NULL;

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

    /* first result from sysdb_initgroups is user_sid; rest are group_sids */
    ret = sysdb_initgroups(tmp_ctx, domain, user, &res);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "sysdb_initgroups failed: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto done;
    }

    if (res->count == 0) {
        ret = ENOENT;
        DEBUG(SSSDBG_OP_FAILURE,
              "sysdb_initgroups returned empty result\n");
        goto done;
    }

    user_sid = ldb_msg_find_attr_as_string(res->msgs[0], SYSDB_SID_STR, NULL);

    /* if there is origPrimaryGroupGidNumber, it's SID must be added to list */
    orig_gid = ldb_msg_find_attr_as_uint64(res->msgs[0],
                                           SYSDB_PRIMARY_GROUP_GIDNUM,
                                           0);
    orig_gid_sid = ad_gpo_get_primary_group_sid(tmp_ctx,
                                                orig_gid,
                                                domain,
                                                idmap_ctx);
    DEBUG(SSSDBG_TRACE_INTERNAL, "SID of the primary group with gid '%"SPRIgid"' is '%s'\n", orig_gid, orig_gid_sid);

    num_group_sids = (res->count) - 1;

    /* include space for AD_AUTHENTICATED_USERS_SID, original GID sid and NULL */
    group_sids = talloc_array(tmp_ctx, const char *, num_group_sids + 3);
    if (group_sids == NULL) {
        ret = ENOMEM;
        goto done;
    }

    for (i = 0; i < num_group_sids; i++) {
        group_sid = ldb_msg_find_attr_as_string(res->msgs[i+1],
                                                SYSDB_SID_STR, NULL);
        if (group_sid == NULL) {
            DEBUG(SSSDBG_CRIT_FAILURE, "Missing SID for cache entry [%s].\n",
                  ldb_dn_get_linearized(res->msgs[i+1]->dn));
            ret = EINVAL;
            goto done;
        }

        group_sids[i] = talloc_steal(group_sids, group_sid);
        if (group_sids[i] == NULL) {
            ret = ENOMEM;
            goto done;
        }
    }
    group_sids[i++] = talloc_strdup(group_sids, AD_AUTHENTICATED_USERS_SID);
    if (orig_gid_sid != NULL) {
        group_sids[i++] = talloc_steal(group_sids, orig_gid_sid);
    }
    group_sids[i] = NULL;

    *_group_size = i;
    *_group_sids = talloc_steal(mem_ctx, group_sids);
    *_user_sid = talloc_steal(mem_ctx, user_sid);
    ret = EOK;

 done:
    talloc_free(tmp_ctx);
    return ret;
}

/*
 * This function determines whether the input ace_dom_sid matches any of the
 * client's SIDs. The boolean result is assigned to the _included output param.
 */
static errno_t
ad_gpo_ace_includes_client_sid(const char *user_sid,
                               const char *host_sid,
                               const char **group_sids,
                               int group_size,
                               const char **host_group_sids,
                               int host_group_size,
                               struct dom_sid ace_dom_sid,
                               struct sss_idmap_ctx *idmap_ctx,
                               bool *_included)
{
    int i = 0;
    struct dom_sid *user_dom_sid;
    struct dom_sid *host_dom_sid;
    struct dom_sid *group_dom_sid;
    enum idmap_error_code err;
    bool included = false;

    err = sss_idmap_sid_to_smb_sid(idmap_ctx, user_sid, &user_dom_sid);
    if (err != IDMAP_SUCCESS) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "sss_idmap_sid_to_smb_sid() failed for user_sid '%s': %d\n",
              user_sid, err);
        return EFAULT;
    }

    included = ad_gpo_dom_sid_equal(&ace_dom_sid, user_dom_sid);
    sss_idmap_free_smb_sid(idmap_ctx, user_dom_sid);
    if (included) {
        *_included = true;
        return EOK;
    }

    err = sss_idmap_sid_to_smb_sid(idmap_ctx, host_sid, &host_dom_sid);
    if (err != IDMAP_SUCCESS) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "sss_idmap_sid_to_smb_sid() failed for host_sid '%s': %d\n",
              host_sid, err);
        return EFAULT;
    }

    included = ad_gpo_dom_sid_equal(&ace_dom_sid, host_dom_sid);
    sss_idmap_free_smb_sid(idmap_ctx, host_dom_sid);
    if (included) {
        *_included = true;
        return EOK;
    }

    for (i = 0; i < group_size; i++) {
        err = sss_idmap_sid_to_smb_sid(idmap_ctx, group_sids[i], &group_dom_sid);
        if (err != IDMAP_SUCCESS) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "sss_idmap_sid_to_smb_sid() failed for group_sid '%s': %d\n",
              group_sids[i], err);
            return EFAULT;
        }
        included = ad_gpo_dom_sid_equal(&ace_dom_sid, group_dom_sid);
        sss_idmap_free_smb_sid(idmap_ctx, group_dom_sid);
        if (included) {
            *_included = true;
            return EOK;
        }
    }

    for (i = 0; i < host_group_size; i++) {
        err = sss_idmap_sid_to_smb_sid(idmap_ctx, host_group_sids[i], &group_dom_sid);
        if (err != IDMAP_SUCCESS) {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "sss_idmap_sid_to_smb_sid() failed for group_sid '%s': %d\n",
                  group_sids[i], err);
            return EFAULT;
        }
        included = ad_gpo_dom_sid_equal(&ace_dom_sid, group_dom_sid);
        sss_idmap_free_smb_sid(idmap_ctx, group_dom_sid);
        if (included) {
            *_included = true;
            return EOK;
        }
    }

    *_included = false;
    return EOK;
}

/*
 * This function determines whether use of the extended right named
 * "ApplyGroupPolicy" (AGP) is allowed for the GPO, by comparing the
 * specified user_sid and group_sids against the passed access control
 * entry (ACE).
 * This function returns ALLOWED, DENIED, or NEUTRAL depending on whether
 * the ACE explicitly allows, explicitly denies, or does neither.
 *
 * Notes:
 * (1) Abbreviation 'M' used in the evaluation algorithm stands for
 * "access_mask", which represents the set of access rights associated with
 * the passed ACE. The access right of interest to the GPO code is
 * RIGHT_DS_CONTROL_ACCESS, which serves as a container for all control access
 * rights. The specific control access right is identified by a GUID in the
 * ACE's ObjectType. In our case, this is the GUID corresponding to AGP.
 * (2) ACE that require an evaluation algorithm different from [MS-ADTS]
 * 5.1.3.3.4, e. g. RIGHT_DS_CONTROL_ACCESS (CR) is not present in M, are
 * ignored.
 *
 * The ACE evaluation algorithm is specified in [MS-ADTS] 5.1.3.3.4:
 * Evaluate the DACL by examining each ACE in sequence, starting with the first
 * ACE. Perform the following sequence of actions for each ACE in the order as
 * shown:
 * 1. If the "Inherit Only" (IO) flag is set in the ACE, skip the ACE.
 * 2. If the SID in the ACE does not match any SID in the requester's
 *    security context, skip the ACE.
 * 3. If the ACE type is "Object Access Allowed", the access right
 *    RIGHT_DS_CONTROL_ACCESS (CR) is present in M, and the ObjectType
 *    field in the ACE is not present, then grant the requested control
 *    access right. Stop any further access checks.
 * 4. If the ACE type is "Object Access Allowed" the access right
 *    RIGHT_DS_CONTROL_ACCESS (CR) is present in M, and the ObjectType
 *    field in the ACE contains a GUID value equal to AGP, then grant
 *    the requested control access right. Stop any further access checks.
 * 5. If the ACE type is "Object Access Denied", the access right
 *    RIGHT_DS_CONTROL_ACCESS (CR) is present in M, and the ObjectType
 *    field in the ACE is not present, then deny the requested control
 *    access right. Stop any further access checks.
 * 6. If the ACE type is "Object Access Denied" the access right
 *    RIGHT_DS_CONTROL_ACCESS (CR) is present in M, and the ObjectType
 *    field in the ACE contains a GUID value equal to AGP, then deny
 *    the requested control access right. Stop any further access checks.
 */
static enum ace_eval_agp_status ad_gpo_evaluate_ace(struct security_ace *ace,
                                                    struct sss_idmap_ctx *idmap_ctx,
                                                    const char *user_sid,
                                                    const char *host_sid,
                                                    const char **group_sids,
                                                    int group_size,
                                                    const char **host_group_sids,
                                                    int host_group_size)
{
    bool included = false;
    int ret = 0;
    struct security_ace_object object;
    struct GUID ext_right_agp_guid;

    if (ace->flags & SEC_ACE_FLAG_INHERIT_ONLY) {
        return AD_GPO_ACE_NEUTRAL;
    }

    ret = ad_gpo_ace_includes_client_sid(user_sid, host_sid, group_sids,
                                         group_size, host_group_sids,
                                         host_group_size, ace->trustee,
                                         idmap_ctx, &included);
    if (ret != EOK) {
        return AD_GPO_ACE_DENIED;
    }

    if (!included) {
        return AD_GPO_ACE_NEUTRAL;
    }

    if (ace->access_mask & SEC_ADS_CONTROL_ACCESS) {
        object = ace->object.object;
        if (object.flags & SEC_ACE_OBJECT_TYPE_PRESENT) {
            GUID_from_string(AD_AGP_GUID, &ext_right_agp_guid);
            if (!GUID_equal(&object.type.type, &ext_right_agp_guid)) {
                return AD_GPO_ACE_NEUTRAL;
            }
        }
        if (ace->type == SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT) {
            return AD_GPO_ACE_ALLOWED;
        } else if (ace->type == SEC_ACE_TYPE_ACCESS_DENIED_OBJECT) {
            return AD_GPO_ACE_DENIED;
        }
    }

    return AD_GPO_ACE_NEUTRAL;
}

/*
 * This function evaluates, which standard access rights the passed access
 * control entry (ACE) allows or denies for the entire GPO.
 *
 * Notes:
 * (1) Abbreviation 'M' used in the evaluation algorithm stands for
 * "access_mask", which represents the set of access rights associated with
 * the passed ACE.
 * (2) Abbreviation 'G' used in the evaluation algorithm stands for
 * "granted rights", which represents the set of access rights, that
 * have already been granted by previously evaluated ACEs.
 * (3) Abbreviation 'D' used in the evaluation algorithm stands for
 * "denied rights", which represents the set of access rights, that
 * have already been explicitly denied by previously evaluated ACEs.
 *
 * The simple ACE evaluation algorithm is specified in [MS-ADTS] 5.1.3.3.2:
 * Evaluate the DACL by examining each ACE in sequence, starting with the first
 * ACE. Perform the following sequence of actions for each ACE in the order as
 * shown:
 * 1. If the "Inherit Only" (IO) flag is set in the ACE, skip the ACE.
 * 2. If the SID in the ACE does not match any SID in the requester's
 *    security context, skip the ACE.
 * 3. If the ACE type is "Access Denied" and the access rights in M
 *    are not in G, then add the rights in M to D.
 * 4. If the ACE type is "Access Allowed" and the access rights in M
 *    are not in D, then add the rights in M to G.
 */
static errno_t ad_gpo_simple_evaluate_ace(struct security_ace *ace,
                                          struct sss_idmap_ctx *idmap_ctx,
                                          const char *user_sid,
                                          const char *host_sid,
                                          const char **group_sids,
                                          int group_size,
                                          const char **host_group_sids,
                                          int host_group_size,
                                          uint32_t *_gpo_access_granted_status,
                                          uint32_t *_gpo_access_denied_status)
{
    bool included = false;
    uint32_t filtered_access_rights = 0;
    int ret = 0;

    if (ace->flags & SEC_ACE_FLAG_INHERIT_ONLY) {
        return EOK;
    }

    ret = ad_gpo_ace_includes_client_sid(user_sid, host_sid, group_sids, group_size,
                                         host_group_sids, host_group_size,
                                         ace->trustee, idmap_ctx, &included);

    if (ret != EOK || !included) {
        return ret;
    }

    if (ace->type == SEC_ACE_TYPE_ACCESS_DENIED) {
        filtered_access_rights = ace->access_mask & ~*_gpo_access_granted_status;
        *_gpo_access_denied_status |= filtered_access_rights;
    } else if (ace->type == SEC_ACE_TYPE_ACCESS_ALLOWED) {
        filtered_access_rights = ace->access_mask & ~*_gpo_access_denied_status;
        *_gpo_access_granted_status |= filtered_access_rights;
    }

    return ret;
}


/*
 * This function extracts the GPO's DACL (discretionary access control list)
 * from the GPO's specified security descriptor, and determines whether
 * the GPO is applicable to the policy target, by comparing the specified
 * user_sid and group_sids against each access control entry (ACE) in the DACL.
 * The GPO is only applicable to the target, if the requester has been granted
 * read access (RIGHT_DS_READ_PROPERTY) to the properties of the GPO and
 * control access (RIGHT_DS_CONTROL_ACCESS) to apply the GPO (AGP).
 * The required read and control access rights for a particular trustee are
 * usually located in different ACEs, i.e. one ACE for control of read access
 * and one for control access.
 * If it comes to the end of the DACL, and the required access is still not
 * explicitly allowed or denied, SSSD denies access to the object as specified
 * in [MS-ADTS] 5.1.3.1.
 */
static errno_t ad_gpo_evaluate_dacl(struct security_acl *dacl,
                                    struct sss_idmap_ctx *idmap_ctx,
                                    const char *user_sid,
                                    const char *host_sid,
                                    const char **group_sids,
                                    int group_size,
                                    const char **host_group_sids,
                                    int host_group_size,
                                    bool *_dacl_access_allowed)
{
    uint32_t num_aces = 0;
    uint32_t access_granted_status = 0;
    uint32_t access_denied_status = 0;
    enum ace_eval_agp_status ace_status;
    struct security_ace *ace = NULL;
    int i = 0;
    int ret = 0;
    enum idmap_error_code err;
    char *trustee_dom_sid_str = NULL;

    num_aces = dacl->num_aces;

    /*
     * [MS-ADTS] 5.1.3.3.2. and 5.1.3.3.4:
     * If the DACL does not have any ACE, then deny the requester the
     * requested control access right.
     */
    if (num_aces == 0) {
        *_dacl_access_allowed = false;
        return EOK;
    }

    /*
     * [MS-GOPD] 2.4:
     * To process a policy that applies to a Group Policy client, the core
     * Group Policy engine must be able to read the policy data from the
     * directory service so that the policy settings can be applied to the
     * Group Policy client or the interactive user.
     */
    for (i = 0; i < dacl->num_aces; i++) {
        ace = &dacl->aces[i];

        ret = ad_gpo_simple_evaluate_ace(ace, idmap_ctx, user_sid, host_sid,
                                         group_sids, group_size,
                                         host_group_sids, host_group_size,
                                         &access_granted_status,
                                         &access_denied_status);

        if (ret != EOK) {
            err = sss_idmap_smb_sid_to_sid(idmap_ctx, &ace->trustee,
                                           &trustee_dom_sid_str);
            if (err != IDMAP_SUCCESS) {
                DEBUG(SSSDBG_OP_FAILURE,
                      "sss_idmap_smb_sid_to_sid failed.\n");
                return EFAULT;
            }

            DEBUG(SSSDBG_MINOR_FAILURE,
                  "Could not determine if ACE is applicable; "
                  " Trustee: %s\n", trustee_dom_sid_str);
            sss_idmap_free_sid(idmap_ctx, trustee_dom_sid_str);
            trustee_dom_sid_str = NULL;
            continue;
        }
    }

    for (i = 0; i < dacl->num_aces; i ++) {
        ace = &dacl->aces[i];

        err = sss_idmap_smb_sid_to_sid(idmap_ctx, &ace->trustee,
                                       &trustee_dom_sid_str);
        if (err != IDMAP_SUCCESS) {
            DEBUG(SSSDBG_OP_FAILURE, "sss_idmap_smb_sid_to_sid failed.\n");
            return EFAULT;
        }

        ace_status = ad_gpo_evaluate_ace(ace, idmap_ctx, user_sid, host_sid,
                                         group_sids, group_size,
                                         host_group_sids, host_group_size);

        switch (ace_status) {
        case AD_GPO_ACE_NEUTRAL:
            break;
        case AD_GPO_ACE_ALLOWED:
            if (access_granted_status & SEC_ADS_READ_PROP) {
                *_dacl_access_allowed = true;
                sss_idmap_free_sid(idmap_ctx, trustee_dom_sid_str);
                return EOK;
            } else {
                DEBUG(SSSDBG_TRACE_FUNC,
                      "GPO read properties access denied (security); "
                      " Trustee: %s\n", trustee_dom_sid_str);
                break;
            }
        case AD_GPO_ACE_DENIED:
            if (access_granted_status & SEC_ADS_READ_PROP) {
                DEBUG(SSSDBG_TRACE_FUNC,
                      "GPO denied (security); "
                      " Trustee: %s\n", trustee_dom_sid_str);
                sss_idmap_free_sid(idmap_ctx, trustee_dom_sid_str);
                *_dacl_access_allowed = false;
                return EOK;
            } else {
                DEBUG(SSSDBG_TRACE_FUNC,
                      "GPO read properties access denied (security); "
                      " Trustee: %s\n", trustee_dom_sid_str);
                break;
            }
        }
        sss_idmap_free_sid(idmap_ctx, trustee_dom_sid_str);
        trustee_dom_sid_str = NULL;
    }

    if (access_granted_status & SEC_ADS_READ_PROP) {
        DEBUG(SSSDBG_TRACE_FUNC,
              "GPO apply group policy access denied (security)\n");
    }

    *_dacl_access_allowed = false;
    return EOK;
}

/*
 * This function takes candidate_gpos as input, filters out any gpo that is
 * not applicable to the policy target and assigns the result to the
 * _dacl_filtered_gpos output parameter. The filtering algorithm is
 * defined in [MS-GPOL] 3.2.5.1.6
 */
static errno_t
ad_gpo_filter_gpos_by_dacl(TALLOC_CTX *mem_ctx,
                           const char *user,
                           const char *host_fqdn,
                           struct sss_domain_info *domain,
                           struct sss_domain_info *host_domain,
                           struct sss_idmap_ctx *idmap_ctx,
                           struct gp_gpo **candidate_gpos,
                           int num_candidate_gpos,
                           struct gp_gpo ***_dacl_filtered_gpos,
                           int *_num_dacl_filtered_gpos)
{
    TALLOC_CTX *tmp_ctx = NULL;
    int i = 0;
    int ret = 0;
    struct gp_gpo *candidate_gpo = NULL;
    struct security_descriptor *sd = NULL;
    struct security_acl *dacl = NULL;
    const char *user_sid = NULL;
    const char **group_sids = NULL;
    int group_size = 0;
    const char *host_sid = NULL;
    const char **host_group_sids = NULL;
    int host_group_size = 0;
    int gpo_dn_idx = 0;
    bool access_allowed = false;
    struct gp_gpo **dacl_filtered_gpos = NULL;

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

    ret = ad_gpo_get_sids(tmp_ctx, user, domain, idmap_ctx, &user_sid,
                          &group_sids, &group_size);
    if (ret != EOK) {
        ret = ERR_NO_SIDS;
        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to retrieve SIDs: [%d](%s)\n", ret, sss_strerror(ret));
        goto done;
    }

    ret = ad_gpo_get_sids(tmp_ctx, host_fqdn, host_domain, idmap_ctx, &host_sid,
                          &host_group_sids, &host_group_size);
    if (ret != EOK) {
        ret = ERR_NO_SIDS;
        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to retrieve host SIDs: [%d](%s)\n", ret, sss_strerror(ret));
        goto done;
    }

    dacl_filtered_gpos = talloc_array(tmp_ctx,
                                 struct gp_gpo *,
                                 num_candidate_gpos + 1);

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

    for (i = 0; i < num_candidate_gpos; i++) {

        access_allowed = false;
        candidate_gpo = candidate_gpos[i];

        DEBUG(SSSDBG_TRACE_FUNC, "examining dacl candidate_gpo_guid:%s\n",
              candidate_gpo->gpo_guid);

        /* gpo_func_version must be set to version 2 */
        if (candidate_gpo->gpo_func_version != 2) {
            DEBUG(SSSDBG_TRACE_FUNC,
                  "GPO not applicable to target per security filtering: "
                  "gPCFunctionalityVersion is not 2\n");
            continue;
        }

        sd = candidate_gpo->gpo_sd;
        if (sd == NULL) {
            DEBUG(SSSDBG_MINOR_FAILURE, "Security descriptor is missing\n");
            ret = EINVAL;
            goto done;
        }

        dacl = candidate_gpo->gpo_sd->dacl;

        /* gpo_flags value of 2 means that GPO's computer portion is disabled */
        if (candidate_gpo->gpo_flags == 2) {
            DEBUG(SSSDBG_TRACE_FUNC,
                  "GPO not applicable to target per security filtering: "
                  "GPO's computer portion is disabled\n");
            continue;
        }

        if ((sd->type & SEC_DESC_DACL_PRESENT) && (dacl != NULL)) {
            ret = ad_gpo_evaluate_dacl(dacl, idmap_ctx, user_sid, host_sid,
                                       group_sids, group_size, host_group_sids,
                                       host_group_size, &access_allowed);
            if (ret != EOK) {
                DEBUG(SSSDBG_MINOR_FAILURE,
                      "Could not determine if GPO is applicable\n");
                continue;
            }
        } else {
            /*
             * [MS-ADTS] 5.1.3.3.4:
             * If the security descriptor has no DACL or its "DACL Present" bit
             * is not set, then grant requester the requested control access right.
             */

            DEBUG(SSSDBG_TRACE_ALL, "DACL is not present\n");
            access_allowed = true;
        }

        if (access_allowed) {
            DEBUG(SSSDBG_TRACE_FUNC,
                  "GPO applicable to target per security filtering\n");
            dacl_filtered_gpos[gpo_dn_idx] = talloc_steal(dacl_filtered_gpos,
                                                          candidate_gpo);
            gpo_dn_idx++;
        } else {
            DEBUG(SSSDBG_TRACE_FUNC,
                  "GPO not applicable to target per security filtering: "
                  "result of DACL evaluation\n");
            continue;
        }
    }

    dacl_filtered_gpos[gpo_dn_idx] = NULL;

    *_dacl_filtered_gpos = talloc_steal(mem_ctx, dacl_filtered_gpos);
    *_num_dacl_filtered_gpos = gpo_dn_idx;

    ret = EOK;

 done:
    talloc_free(tmp_ctx);
    return ret;
}

/*
 * This function determines whether the input cse_guid matches any of the input
 * gpo_cse_guids. The boolean result is assigned to the _included output param.
 */
static bool
ad_gpo_includes_cse_guid(const char *cse_guid,
                         const char **gpo_cse_guids,
                         int num_gpo_cse_guids)
{
    int i = 0;
    const char *gpo_cse_guid = NULL;

    for (i = 0; i < num_gpo_cse_guids; i++) {
        gpo_cse_guid = gpo_cse_guids[i];
        if (strcmp(gpo_cse_guid, cse_guid) == 0) {
            return true;
        }
    }

    return false;
}

/*
 * This function takes an input dacl_filtered_gpos list, filters out any gpo
 * that does not contain the input cse_guid, and assigns the result to the
 * _cse_filtered_gpos output parameter.
 */
static errno_t
ad_gpo_filter_gpos_by_cse_guid(TALLOC_CTX *mem_ctx,
                               const char *cse_guid,
                               struct gp_gpo **dacl_filtered_gpos,
                               int num_dacl_filtered_gpos,
                               struct gp_gpo ***_cse_filtered_gpos,
                               int *_num_cse_filtered_gpos)
{
    TALLOC_CTX *tmp_ctx = NULL;
    int i = 0;
    int ret = 0;
    struct gp_gpo *dacl_filtered_gpo = NULL;
    int gpo_dn_idx = 0;
    struct gp_gpo **cse_filtered_gpos = NULL;
    bool included;

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

    cse_filtered_gpos = talloc_array(tmp_ctx,
                                     struct gp_gpo *,
                                     num_dacl_filtered_gpos + 1);
    if (cse_filtered_gpos == NULL) {
        ret = ENOMEM;
        goto done;
    }

    for (i = 0; i < num_dacl_filtered_gpos; i++) {

        dacl_filtered_gpo = dacl_filtered_gpos[i];

        DEBUG(SSSDBG_TRACE_ALL, "examining cse candidate_gpo_guid: %s\n",
              dacl_filtered_gpo->gpo_guid);

        included = ad_gpo_includes_cse_guid(cse_guid,
                                            dacl_filtered_gpo->gpo_cse_guids,
                                            dacl_filtered_gpo->num_gpo_cse_guids);

        if (included) {
            DEBUG(SSSDBG_TRACE_ALL,
                  "GPO applicable to target per cse_guid filtering\n");
            cse_filtered_gpos[gpo_dn_idx] = talloc_steal(cse_filtered_gpos,
                                                         dacl_filtered_gpo);
            dacl_filtered_gpos[i] = NULL;
            gpo_dn_idx++;
        } else {
            DEBUG(SSSDBG_TRACE_ALL,
                  "GPO not applicable to target per cse_guid filtering\n");
            continue;
        }
    }

    cse_filtered_gpos[gpo_dn_idx] = NULL;

    *_cse_filtered_gpos = talloc_steal(mem_ctx, cse_filtered_gpos);
    *_num_cse_filtered_gpos = gpo_dn_idx;

    ret = EOK;

 done:
    talloc_free(tmp_ctx);
    return ret;
}

/*
 * This cse-specific function (GP_EXT_GUID_SECURITY) returns a boolean value
 * based on whether the input user_sid or any of the input group_sids appear
 * in the input list of privilege_sids.
 */
static bool
check_rights(char **privilege_sids,
             int privilege_size,
             const char *user_sid,
             const char **group_sids,
             int group_size)
{
    int i, j;

    for (i = 0; i < privilege_size; i++) {
        if (strcmp(user_sid, privilege_sids[i]) == 0) {
            return true;
        }
        for (j = 0; j < group_size; j++) {
            if (strcmp(group_sids[j], privilege_sids[i]) == 0) {
                return true;
            }
        }
    }

    return false;
}

/*
 * This function parses the input ini_config object (which represents
 * the cse-specific filename), and returns the policy_setting_value
 * corresponding to the input policy_setting_key.
 */
static errno_t
ad_gpo_extract_policy_setting(TALLOC_CTX *mem_ctx,
                              struct ini_cfgobj *ini_config,
                              const char *policy_setting_key,
                              char **_policy_setting_value)
{
    struct value_obj *vobj = NULL;
    int ret;
    const char *policy_setting_value;

    ret = ini_get_config_valueobj(RIGHTS_SECTION, policy_setting_key, ini_config,
                                  INI_GET_FIRST_VALUE, &vobj);
    if (ret != 0) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "ini_get_config_valueobj failed [%d][%s]\n", ret, strerror(ret));
        goto done;
    }
    if (vobj == NULL) {
        DEBUG(SSSDBG_TRACE_ALL, "section/name not found: [%s][%s]\n",
              RIGHTS_SECTION, policy_setting_key);
        ret = ENOENT;
        goto done;
    }
    policy_setting_value = ini_get_string_config_value(vobj, &ret);
    if (ret != 0) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "ini_get_string_config_value failed [%d][%s]\n",
              ret, strerror(ret));
        goto done;
    }

    if (policy_setting_value[0]) {
        *_policy_setting_value = talloc_strdup(mem_ctx, policy_setting_value);
        if (!*_policy_setting_value) {
            ret = ENOMEM;
            goto done;
        }
    } else {
        /* This is an explicitly empty policy setting.
         * We need to remove this from the LDB.
         */
        *_policy_setting_value = NULL;
    }

    ret = EOK;

 done:

    return ret;
}

static errno_t
add_result_to_hash(hash_table_t *hash, const char *key, char *value)
{
    int hret;
    hash_key_t k;
    hash_value_t v;

    if (hash == NULL || key == NULL || value == NULL) {
        return EINVAL;
    }

    k.type = HASH_KEY_CONST_STRING;
    k.c_str = key;

    v.type = HASH_VALUE_PTR;
    v.ptr = value;

    hret = hash_enter(hash, &k, &v);
    if (hret != HASH_SUCCESS) {
        DEBUG(SSSDBG_OP_FAILURE, "Failed to add [%s][%s] to hash: [%s].\n",
                                 key, value, hash_error_string(hret));
        return EIO;
    }

    return EOK;
}

/*
 * This function parses the cse-specific (GP_EXT_GUID_SECURITY) filename,
 * and stores the allow_key and deny_key of all of the gpo_map_types present
 * in the file (as part of the GPO Result object in the sysdb cache).
 */
static errno_t
ad_gpo_store_policy_settings(struct sss_domain_info *domain,
                             hash_table_t *allow_maps, hash_table_t *deny_maps,
                             const char *filename)
{
    struct ini_cfgfile *file_ctx = NULL;
    struct ini_cfgobj *ini_config = NULL;
    int ret;
    int i;
    char *allow_value = NULL;
    char *deny_value = NULL;
    const char *empty_val = "NO_SID";
    const char *allow_key = NULL;
    const char *deny_key = NULL;
    TALLOC_CTX *tmp_ctx = NULL;

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

    ret = ini_config_create(&ini_config);
    if (ret != 0) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "ini_config_create failed [%d][%s]\n", ret, strerror(ret));
        goto done;
    }

    ret = ini_config_file_open(filename, 0, &file_ctx);
    if (ret != 0) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "ini_config_file_open failed [%d][%s]\n", ret, strerror(ret));
        goto done;
    }

    ret = ini_config_parse(file_ctx, INI_STOP_ON_NONE, 0, 0, ini_config);
    if (ret != 0) {
        int lret;
        char **errors;

        DEBUG(SSSDBG_CRIT_FAILURE,
              "[%s]: ini_config_parse failed [%d][%s]\n",
              filename, ret, strerror(ret));

        /* Now get specific errors if there are any */
        lret = ini_config_get_errors(ini_config, &errors);
        if (lret != 0) {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "Failed to get specific parse error [%d][%s]\n", lret,
                  strerror(lret));
            goto done;
        }

        for (int a = 0; errors[a]; a++) {
             DEBUG(SSSDBG_CRIT_FAILURE, "%s\n", errors[a]);
        }
        ini_config_free_errors(errors);

        /* Do not 'goto done' here. We will try to parse
         * the GPO file again. */
    }

    if (ret != EOK) {
        /* A problem occurred during parsing. Try again
         * with INI_PARSE_IGNORE_NON_KVP flag */

        ini_config_file_destroy(file_ctx);
        file_ctx = NULL;
        ini_config_destroy(ini_config);
        ini_config = NULL;

        ret = ini_config_file_open(filename, 0, &file_ctx);
        if (ret != 0) {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "ini_config_file_open failed [%d][%s]\n",
                  ret, strerror(ret));
            goto done;
        }

        ret = ini_config_create(&ini_config);
        if (ret != 0) {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "ini_config_create failed [%d][%s]\n", ret, strerror(ret));
            goto done;
        }

        ret = ini_config_parse(file_ctx, INI_STOP_ON_NONE, 0,
                               INI_PARSE_IGNORE_NON_KVP, ini_config);
        if (ret != 0) {
            int lret;
            char **errors;

            DEBUG(SSSDBG_CRIT_FAILURE,
                  "[%s]: ini_config_parse failed [%d][%s]\n",
                  filename, ret, strerror(ret));

            /* Now get specific errors if there are any */
            lret = ini_config_get_errors(ini_config, &errors);
            if (lret != 0) {
                DEBUG(SSSDBG_CRIT_FAILURE,
                      "Failed to get specific parse error [%d][%s]\n", lret,
                      strerror(lret));
                goto done;
            }

            for (int a = 0; errors[a]; a++) {
                 DEBUG(SSSDBG_CRIT_FAILURE, "%s\n", errors[a]);
            }
            ini_config_free_errors(errors);

            goto done;
        }
    }

    for (i = 0; i < GPO_MAP_NUM_OPTS; i++) {
        /* The NO_SID val is used as special SID value for the case when
         * no SIDs are found in the rule, but we need to store some
         * value (SID) with the key (rule name) so that it is clear
         * that the rule is defined on the server. */
        struct gpo_map_option_entry entry = gpo_map_option_entries[i];

        allow_key = entry.allow_key;
        if (allow_key != NULL) {
            DEBUG(SSSDBG_TRACE_ALL, "allow_key = %s\n", allow_key);
            ret = ad_gpo_extract_policy_setting(tmp_ctx,
                                                ini_config,
                                                allow_key,
                                                &allow_value);
            if (ret != EOK && ret != ENOENT) {
                DEBUG(SSSDBG_CRIT_FAILURE,
                      "ad_gpo_extract_policy_setting failed for %s [%d][%s]\n",
                      allow_key, ret, sss_strerror(ret));
                goto done;
            } else if (ret != ENOENT) {
                const char *value = allow_value ? allow_value : empty_val;
                ret = add_result_to_hash(allow_maps, allow_key,
                                         talloc_strdup(allow_maps, value));
                if (ret != EOK) {
                    DEBUG(SSSDBG_CRIT_FAILURE, "Failed to add key: [%s] "
                                               "value: [%s] to allow maps "
                                               "[%d][%s].\n",
                                               allow_key, value, ret,
                                               sss_strerror(ret));
                    goto done;
                }
            }
        }

        deny_key = entry.deny_key;
        if (deny_key != NULL) {
            DEBUG(SSSDBG_TRACE_ALL, "deny_key = %s\n", deny_key);
            ret = ad_gpo_extract_policy_setting(tmp_ctx,
                                                ini_config,
                                                deny_key,
                                                &deny_value);
            if (ret != EOK && ret != ENOENT) {
                DEBUG(SSSDBG_CRIT_FAILURE,
                      "ad_gpo_extract_policy_setting failed for %s [%d][%s]\n",
                      deny_key, ret, sss_strerror(ret));
                goto done;
            } else if (ret != ENOENT) {
                const char *value = deny_value ? deny_value : empty_val;
                ret = add_result_to_hash(deny_maps, deny_key,
                                         talloc_strdup(deny_maps, value));
                if (ret != EOK) {
                    DEBUG(SSSDBG_CRIT_FAILURE, "Failed to add key: [%s] "
                                               "value: [%s] to deny maps "
                                               "[%d][%s].\n",
                                               deny_key, value, ret,
                                               sss_strerror(ret));
                    goto done;
                }
            }
        }
    }

    ret = EOK;

 done:

    if (ret != EOK) {
      DEBUG(SSSDBG_CRIT_FAILURE, "Error encountered: %d.\n", ret);
    }
    ini_config_file_destroy(file_ctx);
    ini_config_destroy(ini_config);
    talloc_free(tmp_ctx);
    return ret;
}

/*
 * This cse-specific function (GP_EXT_GUID_SECURITY) performs the access
 * check for determining whether logon access is granted or denied for
 * the {user,domain} tuple specified in the inputs. This function returns EOK
 * to indicate that access is granted. Any other return value indicates that
 * access is denied.
 *
 * The access control algorithm first determines whether the "principal_sids"
 * (i.e. user_sid or group_sids) appear in allowed_sids and denied_sids.
 *
 * For access to be granted, both the "allowed_sids_condition" *and* the
 * "denied_sids_condition" must be met (in all other cases, access is denied).
 * 1) The "allowed_sids_condition" is satisfied if any of the principal_sids
 *    appears in allowed_sids OR if the allowed_sids list is empty
 * 2) The "denied_sids_condition" is satisfied if none of the principal_sids
 *    appear in denied_sids
 *
 * Note that a deployment that is unaware of GPO-based access-control policy
 * settings is unaffected by them (b/c absence of allowed_sids grants access).
 *
 * Note that if a principal_sid appears in both allowed_sids and denied_sids,
 * the "allowed_sids_condition" is met, but the "denied_sids_condition" is not.
 * In other words, Deny takes precedence over Allow.
 */
static errno_t
ad_gpo_access_check(TALLOC_CTX *mem_ctx,
                    enum gpo_access_control_mode gpo_mode,
                    enum gpo_map_type gpo_map_type,
                    const char *user,
                    bool gpo_implicit_deny,
                    struct sss_domain_info *domain,
                    struct sss_idmap_ctx *idmap_ctx,
                    char **allowed_sids,
                    int allowed_size,
                    char **denied_sids,
                    int denied_size)
{
    const char *user_sid;
    const char **group_sids;
    int group_size = 0;
    bool access_granted = false;
    bool access_denied = false;
    int ret;
    int j;

    DEBUG(SSSDBG_TRACE_FUNC, "RESULTANT POLICY:\n");
    DEBUG(SSSDBG_TRACE_FUNC, "gpo_map_type: %s\n",
          gpo_map_type_string(gpo_map_type));
    DEBUG(SSSDBG_TRACE_FUNC, "allowed_size = %d\n", allowed_size);
    for (j= 0; j < allowed_size; j++) {
        DEBUG(SSSDBG_TRACE_FUNC, "allowed_sids[%d] = %s\n", j, allowed_sids[j]);
    }

    DEBUG(SSSDBG_TRACE_FUNC, "denied_size = %d\n", denied_size);
    for (j= 0; j < denied_size; j++) {
        DEBUG(SSSDBG_TRACE_FUNC, " denied_sids[%d] = %s\n", j, denied_sids[j]);
    }

    ret = ad_gpo_get_sids(mem_ctx, user, domain, idmap_ctx, &user_sid,
                          &group_sids, &group_size);
    if (ret != EOK) {
        ret = ERR_NO_SIDS;
        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to retrieve SIDs: [%d](%s)\n", ret, sss_strerror(ret));
        goto done;
    }

    DEBUG(SSSDBG_TRACE_FUNC, "CURRENT USER:\n");
    DEBUG(SSSDBG_TRACE_FUNC, "       user_sid = %s\n", user_sid);

    for (j= 0; j < group_size; j++) {
        DEBUG(SSSDBG_TRACE_FUNC, "  group_sids[%d] = %s\n", j,
              group_sids[j]);
    }

    if (allowed_size == 0 && !gpo_implicit_deny) {
        access_granted = true;
    }  else {
        access_granted = check_rights(allowed_sids, allowed_size, user_sid,
                                      group_sids, group_size);
    }

    DEBUG(SSSDBG_TRACE_FUNC, "POLICY DECISION:\n");

    DEBUG(SSSDBG_TRACE_FUNC, " access_granted = %d\n", access_granted);

    access_denied = check_rights(denied_sids, denied_size, user_sid,
                                 group_sids, group_size);
    DEBUG(SSSDBG_TRACE_FUNC, "  access_denied = %d\n", access_denied);

    if (access_granted && !access_denied) {
        return EOK;
    } else {
        switch (gpo_mode) {
        case GPO_ACCESS_CONTROL_ENFORCING:
            return ERR_ACCESS_DENIED;
        case GPO_ACCESS_CONTROL_PERMISSIVE:
            DEBUG(SSSDBG_TRACE_FUNC, "access denied: permissive mode\n");
            sss_log_ext(SSS_LOG_WARNING, LOG_AUTHPRIV, "Warning: user would " \
                        "have been denied GPO-based logon access if the " \
                        "ad_gpo_access_control option were set to enforcing " \
                        "mode.");
            return EOK;
        default:
            return EINVAL;
        }
    }

 done:

    if (ret) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Error encountered: %d.\n", ret);
    }

    return ret;
}

/*
 * This function retrieves the raw policy_setting_value for the input key from
 * the GPO_Result object in the sysdb cache. It then parses the raw value and
 * uses the results to populate the output parameters with the sids_list and
 * the size of the sids_list.
 */
errno_t
parse_policy_setting_value(TALLOC_CTX *mem_ctx,
                           struct sss_domain_info *domain,
                           const char *key,
                           char ***_sids_list,
                           int *_sids_list_size)
{
    int ret;
    int i;
    const char *value;
    int sids_list_size;
    char **sids_list = NULL;

    ret = sysdb_gpo_get_gpo_result_setting(mem_ctx, domain, key, &value);
    if (ret == ENOENT) {
        DEBUG(SSSDBG_TRACE_FUNC, "No previous GPO result\n");
        value = NULL;
    } else if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Cannot retrieve settings from sysdb for key: '%s' [%d][%s].\n",
              key, ret, sss_strerror(ret));
        goto done;
    }

    if (value == NULL) {
        DEBUG(SSSDBG_TRACE_FUNC,
              "No value for key [%s] found in gpo result\n", key);
        sids_list_size = 0;
    } else {
        ret = split_on_separator(mem_ctx, value, ',', true, true,
                                 &sids_list, &sids_list_size);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE,
                  "Cannot parse list of sids %s: %d\n", value, ret);
            ret = EINVAL;
            goto done;
        }

        for (i = 0; i < sids_list_size; i++) {
            /* remove the asterisk prefix found on sids */
            sids_list[i]++;
        }
    }

    *_sids_list = talloc_steal(mem_ctx, sids_list);
    *_sids_list_size = sids_list_size;

    ret = EOK;

 done:
    return ret;
}

/*
 * This cse-specific function (GP_EXT_GUID_SECURITY) performs HBAC policy
 * processing and determines whether logon access is granted or denied for
 * the {user,domain} tuple specified in the inputs. This function returns EOK
 * to indicate that access is granted. Any other return value indicates that
 * access is denied.
 *
 * Internally, this function retrieves the allow_value and deny_value for the
 * input gpo_map_type from the GPO Result object in the sysdb cache, parses
 * the values into allow_sids and deny_sids, and executes the access control
 * algorithm which compares the allow_sids and deny_sids against the user_sid
 * and group_sids for the input user.
 */
static errno_t
ad_gpo_perform_hbac_processing(TALLOC_CTX *mem_ctx,
                               enum gpo_access_control_mode gpo_mode,
                               enum gpo_map_type gpo_map_type,
                               const char *user,
                               bool gpo_implicit_deny,
                               struct sss_domain_info *user_domain,
                               struct sss_domain_info *host_domain,
                               struct sss_idmap_ctx *idmap_ctx)
{
    int ret;
    const char *allow_key = NULL;
    char **allow_sids;
    int allow_size ;
    const char *deny_key = NULL;
    char **deny_sids;
    int deny_size;

    allow_key = gpo_map_option_entries[gpo_map_type].allow_key;
    DEBUG(SSSDBG_TRACE_ALL, "allow_key: %s\n", allow_key);
    deny_key = gpo_map_option_entries[gpo_map_type].deny_key;
    DEBUG(SSSDBG_TRACE_ALL, "deny_key: %s\n", deny_key);

    ret = parse_policy_setting_value(mem_ctx, host_domain, allow_key,
                                     &allow_sids, &allow_size);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "parse_policy_setting_value failed for key %s: [%d](%s)\n",
              allow_key, ret, sss_strerror(ret));
        ret = EINVAL;
        goto done;
    }

    ret = parse_policy_setting_value(mem_ctx, host_domain, deny_key,
                                     &deny_sids, &deny_size);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "parse_policy_setting_value failed for key %s: [%d](%s)\n",
              deny_key, ret, sss_strerror(ret));
        ret = EINVAL;
        goto done;
    }

    /* perform access check with the final resultant allow_sids and deny_sids */
    ret = ad_gpo_access_check(mem_ctx, gpo_mode, gpo_map_type, user,
                              gpo_implicit_deny, user_domain, idmap_ctx,
                              allow_sids, allow_size, deny_sids, deny_size);

    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "GPO access check failed: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto done;
    }

 done:
    return ret;
}

/* == ad_gpo_access_send/recv implementation ================================*/

struct ad_gpo_access_state {
    struct tevent_context *ev;
    struct ldb_context *ldb_ctx;
    struct ad_access_ctx *access_ctx;
    enum gpo_access_control_mode gpo_mode;
    bool gpo_implicit_deny;
    enum gpo_map_type gpo_map_type;
    struct sdap_id_conn_ctx *conn;
    struct sdap_id_op *sdap_op;
    char *server_hostname;
    struct sdap_options *opts;
    int timeout;
    struct sss_domain_info *user_domain;
    struct sss_domain_info *host_domain;
    const char *host_sam_account_name;
    char *host_fqdn;
    const char *user;
    int gpo_timeout_option;
    const char *ad_hostname;
    const char *host_sid;
    const char *target_dn;
    struct gp_gpo **dacl_filtered_gpos;
    int num_dacl_filtered_gpos;
    struct gp_gpo **cse_filtered_gpos;
    int num_cse_filtered_gpos;
    int cse_gpo_index;
    const char *ad_domain;
    hash_table_t *allow_maps;
    hash_table_t *deny_maps;
};

static void ad_gpo_connect_done(struct tevent_req *subreq);
static void ad_gpo_target_dn_retrieval_done(struct tevent_req *subreq);
static void ad_gpo_process_som_done(struct tevent_req *subreq);
static void ad_gpo_process_gpo_done(struct tevent_req *subreq);

static errno_t ad_gpo_cse_step(struct tevent_req *req);
static void ad_gpo_cse_done(struct tevent_req *subreq);

struct tevent_req *
ad_gpo_access_send(TALLOC_CTX *mem_ctx,
                   struct tevent_context *ev,
                   struct sss_domain_info *domain,
                   struct ad_access_ctx *ctx,
                   const char *user,
                   const char *service)
{
    struct tevent_req *req;
    struct tevent_req *subreq;
    struct ad_gpo_access_state *state;
    errno_t ret;
    int hret;
    hash_key_t key;
    hash_value_t val;
    enum gpo_map_type gpo_map_type;

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

    /* determine service's option_type (e.g. interactive, network, etc) */
    key.type = HASH_KEY_STRING;
    key.str = talloc_strdup(state, service);

    hret = hash_lookup(ctx->gpo_map_options_table, &key, &val);
    if (hret != HASH_SUCCESS && hret != HASH_ERROR_KEY_NOT_FOUND) {
        DEBUG(SSSDBG_OP_FAILURE, "Error checking hash table: [%s]\n",
              hash_error_string(hret));
        ret = EINVAL;
        goto immediately;
    }

    /* if service isn't mapped, map it to value of ad_gpo_default_right option */
    if (hret == HASH_ERROR_KEY_NOT_FOUND) {
        DEBUG(SSSDBG_TRACE_FUNC,
              "Configuration hint: PAM service '%s' is not mapped to any Group"
              " Policy rule. If you plan to use this PAM service it is "
              "recommended to use the ad_gpo_map_* family of options to map "
              "this PAM service to a Group Policy rule. PAM services not "
              "present in any map will fall back to value set in "
              "ad_gpo_default_right, which is currently set to %s (see manual "
              "pages 'man sssd-ad' for more details).\n", service,
              gpo_map_type_string(ctx->gpo_default_right));
        gpo_map_type = ctx->gpo_default_right;
    } else {
        gpo_map_type = (enum gpo_map_type) val.i;
    }

    DEBUG(SSSDBG_TRACE_FUNC, "service %s maps to %s\n", service,
          gpo_map_type_string(gpo_map_type));

    if (gpo_map_type == GPO_MAP_PERMIT) {
        ret = EOK;
        goto immediately;
    }

    if (gpo_map_type == GPO_MAP_DENY) {
        switch (ctx->gpo_access_control_mode) {
        case GPO_ACCESS_CONTROL_ENFORCING:
            ret = ERR_ACCESS_DENIED;
            goto immediately;
        case GPO_ACCESS_CONTROL_PERMISSIVE:
            DEBUG(SSSDBG_TRACE_FUNC, "access denied: permissive mode\n");
            sss_log_ext(SSS_LOG_WARNING, LOG_AUTHPRIV, "Warning: user would " \
                        "have been denied GPO-based logon access if the " \
                        "ad_gpo_access_control option were set to enforcing " \
                        "mode.");
            ret = EOK;
            goto immediately;
        default:
            ret = EINVAL;
            goto immediately;
        }
    }

    /* GPO Operations all happen against the enrolled domain,
     * not the user's domain (which may be a trusted realm)
     */
    state->user_domain = domain;
    state->host_domain = get_domains_head(domain);
    state->ad_domain = dp_opt_get_string(ctx->ad_id_ctx->ad_options->basic,
                                         AD_DOMAIN);

    state->gpo_map_type = gpo_map_type;
    state->dacl_filtered_gpos = NULL;
    state->num_dacl_filtered_gpos = 0;
    state->cse_filtered_gpos = NULL;
    state->num_cse_filtered_gpos = 0;
    state->cse_gpo_index = 0;
    state->ev = ev;
    state->user = user;
    state->ldb_ctx = sysdb_ctx_get_ldb(state->host_domain->sysdb);
    state->gpo_mode = ctx->gpo_access_control_mode;
    state->gpo_timeout_option = ctx->gpo_cache_timeout;
    state->ad_hostname = dp_opt_get_string(ctx->ad_options, AD_HOSTNAME);
    state->gpo_implicit_deny = dp_opt_get_bool(ctx->ad_options,
                                               AD_GPO_IMPLICIT_DENY);
    state->access_ctx = ctx;
    state->opts = ctx->sdap_access_ctx->id_ctx->opts;
    state->timeout = dp_opt_get_int(state->opts->basic, SDAP_SEARCH_TIMEOUT);
    state->conn = ad_get_dom_ldap_conn(ctx->ad_id_ctx, state->host_domain);
    state->sdap_op = sdap_id_op_create(state, state->conn->conn_cache);
    if (state->sdap_op == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "sdap_id_op_create failed.\n");
        ret = ENOMEM;
        goto immediately;
    }

    ret = sss_hash_create(state, 0, &state->allow_maps);
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Could not create allow maps "
              "hash table [%d]: %s\n", ret, sss_strerror(ret));
        goto immediately;
    }

    ret = sss_hash_create(state, 0, &state->deny_maps);
    if (ret != EOK) {
        DEBUG(SSSDBG_FATAL_FAILURE, "Could not create deny maps "
              "hash table [%d]: %s\n", ret, sss_strerror(ret));
        goto immediately;
    }

    subreq = sdap_id_op_connect_send(state->sdap_op, state, &ret);
    if (subreq == NULL) {
        DEBUG(SSSDBG_OP_FAILURE,
              "sdap_id_op_connect_send failed: [%d](%s)\n",
               ret, sss_strerror(ret));
        goto immediately;
    }
    tevent_req_set_callback(subreq, ad_gpo_connect_done, req);

    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
process_offline_gpos(TALLOC_CTX *mem_ctx,
                     const char *user,
                     bool gpo_implicit_deny,
                     enum gpo_access_control_mode gpo_mode,
                     struct sss_domain_info *user_domain,
                     struct sss_domain_info *host_domain,
                     struct sss_idmap_ctx *idmap_ctx,
                     enum gpo_map_type gpo_map_type)

{
    errno_t ret;

    ret = ad_gpo_perform_hbac_processing(mem_ctx,
                                         gpo_mode,
                                         gpo_map_type,
                                         user,
                                         gpo_implicit_deny,
                                         user_domain,
                                         host_domain,
                                         idmap_ctx);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "HBAC processing failed: [%d](%s}\n",
              ret, sss_strerror(ret));
        goto done;
    }

    /* we have successfully processed all offline gpos */
    ret = EOK;

 done:
    return ret;
}

static void
ad_gpo_connect_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_gpo_access_state *state;
    int dp_error;
    errno_t ret;
    char *server_uri;
    LDAPURLDesc *lud;
    struct sdap_domain *sdom;
    struct sdap_search_base **search_bases;

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

    ret = sdap_id_op_connect_recv(subreq, &dp_error);
    talloc_zfree(subreq);

    if (ret != EOK) {
        if (dp_error != DP_ERR_OFFLINE) {
            DEBUG(SSSDBG_OP_FAILURE,
                  "Failed to connect to AD server: [%d](%s)\n",
                  ret, sss_strerror(ret));
            goto done;
        } else {
            DEBUG(SSSDBG_TRACE_FUNC, "Preparing for offline operation.\n");
            ret = process_offline_gpos(state,
                                       state->user,
                                       state->gpo_implicit_deny,
                                       state->gpo_mode,
                                       state->user_domain,
                                       state->host_domain,
                                       state->opts->idmap_ctx->map,
                                       state->gpo_map_type);

            if (ret == EOK) {
                DEBUG(SSSDBG_TRACE_FUNC, "process_offline_gpos succeeded\n");
                tevent_req_done(req);
                goto done;
            } else {
                DEBUG(SSSDBG_OP_FAILURE,
                      "process_offline_gpos failed [%d](%s)\n",
                      ret, sss_strerror(ret));
                goto done;
            }
        }
    }

    /* extract server_hostname from server_uri */
    server_uri = state->conn->service->uri;
    ret = ldap_url_parse(server_uri, &lud);
    if (ret != LDAP_SUCCESS) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Failed to parse ldap URI (%s)!\n", server_uri);
        ret = EINVAL;
        goto done;
    }

    if (lud->lud_host == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "The LDAP URI (%s) did not contain a host name\n", server_uri);
        ldap_free_urldesc(lud);
        ret = EINVAL;
        goto done;
    }

    state->server_hostname = talloc_strdup(state, lud->lud_host);
    ldap_free_urldesc(lud);
    if (!state->server_hostname) {
        ret = ENOMEM;
        goto done;
    }
    DEBUG(SSSDBG_TRACE_ALL, "server_hostname from uri: %s\n",
          state->server_hostname);

    /* SDAP_SASL_AUTHID contains the name used for kinit and SASL bind which
     * in the AD case is the NetBIOS name. */
    state->host_sam_account_name = dp_opt_get_string(state->opts->basic,
                                                     SDAP_SASL_AUTHID);
    if (state->host_sam_account_name == NULL) {
        ret = ENOMEM;
        goto done;
    }

    DEBUG(SSSDBG_TRACE_FUNC, "sam_account_name is %s\n",
                             state->host_sam_account_name);

    state->host_fqdn = sss_create_internal_fqname(state, state->host_sam_account_name,
                                                  state->host_domain->name);
    if (state->host_fqdn == NULL) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Failed to create fully-qualified host name.\n");
        ret = ENOMEM;
        goto done;
    }

    /* AD handle computers the same as users */
    sdom = sdap_domain_get(state->access_ctx->ad_id_ctx->sdap_id_ctx->opts,
                           state->host_domain);
    if (sdom == NULL) {
        ret = EIO;
        goto done;
    }

    ret = common_parse_search_base(state,
                                   sdom->naming_context == NULL ? sdom->basedn
                                                                : sdom->naming_context,
                                   state->ldb_ctx, "AD_HOSTS", NULL, &search_bases);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Failed to create dedicated search base for host lookups, "
              "trying with user search base.");
    }

    if (state->access_ctx->host_attr_map == NULL) {
        ret = sdap_copy_map(state->access_ctx,
                            ad_2008r2_user_map, SDAP_OPTS_USER,
                            &state->access_ctx->host_attr_map);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Failed to copy user map.\n");
            goto done;
        }
    }

    subreq = groups_by_user_send(state, state->ev,
                                 state->access_ctx->ad_id_ctx->sdap_id_ctx,
                                 sdom, state->conn,
                                 search_bases,
                                 state->host_fqdn,
                                 BE_FILTER_NAME,
                                 NULL,
                                 state->access_ctx->host_attr_map,
                                 SDAP_OPTS_USER,
                                 true,
                                 true);
    tevent_req_set_callback(subreq, ad_gpo_target_dn_retrieval_done, req);

    ret = EOK;

 done:

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

static void
ad_gpo_target_dn_retrieval_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_gpo_access_state *state;
    int ret;
    int dp_error;
    int sdap_ret;
    const char *target_dn = NULL;
    uint32_t uac;
    static const char *host_attrs[] = { SYSDB_ORIG_DN, SYSDB_AD_USER_ACCOUNT_CONTROL, SYSDB_SID_STR, NULL };
    struct ldb_result *res = NULL;
    const char *tmp = NULL;
    char *endptr;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct ad_gpo_access_state);
    ret = groups_by_user_recv(subreq, &dp_error, &sdap_ret);
    talloc_zfree(subreq);
    if (ret != EOK) {
        if (sdap_ret == EAGAIN && dp_error == DP_ERR_OFFLINE) {
            DEBUG(SSSDBG_TRACE_FUNC, "Preparing for offline operation.\n");
            ret = process_offline_gpos(state,
                                       state->user,
                                       state->gpo_implicit_deny,
                                       state->gpo_mode,
                                       state->user_domain,
                                       state->host_domain,
                                       state->opts->idmap_ctx->map,
                                       state->gpo_map_type);

            if (ret == EOK) {
                DEBUG(SSSDBG_TRACE_FUNC, "process_offline_gpos succeeded\n");
                tevent_req_done(req);
                goto done;
            } else {
                DEBUG(SSSDBG_OP_FAILURE,
                      "process_offline_gpos failed [%d](%s)\n",
                      ret, sss_strerror(ret));
                goto done;
            }
        }

        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to get policy target's DN: [%d](%s)\n",
               ret, sss_strerror(ret));
        ret = ENOENT;
        goto done;
    }

    ret = sysdb_get_user_attr(state, state->host_domain,
                              state->host_fqdn,
                              host_attrs, &res);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Failed to read host attributes.\n");
        goto done;
    }
    if (res->count != 1) {
        DEBUG(SSSDBG_OP_FAILURE, "Unexpected number [%d] of results searching "
                                 "for [%s], expected 1.\n", res->count,
                                 state->host_sam_account_name);
        ret = EINVAL;
        goto done;
    }

    target_dn = ldb_msg_find_attr_as_string(res->msgs[0], SYSDB_ORIG_DN, NULL);
    if (target_dn == NULL) {
        DEBUG(SSSDBG_OP_FAILURE,
              "ldb_msg_find_attr_as_string failed: [%d](%s)\n",
               ret, sss_strerror(ret));
        goto done;
    }
    state->target_dn = talloc_steal(state, target_dn);
    if (state->target_dn == NULL) {
        ret = ENOMEM;
        goto done;
    }

    tmp = ldb_msg_find_attr_as_string(res->msgs[0], SYSDB_AD_USER_ACCOUNT_CONTROL,
                                      NULL);
    if (tmp == NULL) {
        DEBUG(SSSDBG_OP_FAILURE,
              "ldb_msg_find_attr_as_string failed: [%d](%s)\n",
               ret, sss_strerror(ret));
        goto done;
    }

    uac = strtouint32(tmp, &endptr, 10);
    if (errno != 0) {
        ret =  errno;
        DEBUG(SSSDBG_OP_FAILURE, "Failed to convert UAC [%s] into uint32_t.\n",
                                 tmp);
        goto done;
    }
    if (*endptr != '\0') {
        ret = EINVAL;
        DEBUG(SSSDBG_OP_FAILURE, "UAC [%s] is not a pure numerical value.\n",
                                 tmp);
        goto done;
    }

    /* we only support computer policy targets, not users */
    if (!(uac & UAC_WORKSTATION_TRUST_ACCOUNT ||
          uac & UAC_SERVER_TRUST_ACCOUNT)) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Invalid userAccountControl (%x) value for machine account.\n",
              uac);
        ret = EINVAL;
        goto done;
    }

    state->host_sid = ldb_msg_find_attr_as_string(res->msgs[0], SYSDB_SID_STR,
                                                  NULL);
    talloc_steal(state, state->host_sid);

    subreq = ad_gpo_process_som_send(state,
                                     state->ev,
                                     state->conn,
                                     state->ldb_ctx,
                                     state->sdap_op,
                                     state->opts,
                                     state->access_ctx->ad_options,
                                     state->timeout,
                                     state->target_dn,
                                     state->ad_domain);
    if (subreq == NULL) {
        ret = ENOMEM;
        goto done;
    }

    tevent_req_set_callback(subreq, ad_gpo_process_som_done, req);

    ret = EOK;

 done:

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

}

static void
ad_gpo_process_som_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_gpo_access_state *state;
    int ret;
    struct gp_som **som_list;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct ad_gpo_access_state);
    ret = ad_gpo_process_som_recv(subreq, state, &som_list);
    talloc_zfree(subreq);

    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to get som list: [%d](%s)\n",
               ret, sss_strerror(ret));
        ret = ENOENT;
        goto done;
    }

    subreq = ad_gpo_process_gpo_send(state,
                                     state->ev,
                                     state->sdap_op,
                                     state->opts,
                                     state->server_hostname,
                                     state->host_domain,
                                     state->access_ctx,
                                     state->timeout,
                                     som_list);
    if (subreq == NULL) {
        ret = ENOMEM;
        goto done;
    }

    tevent_req_set_callback(subreq, ad_gpo_process_gpo_done, req);

    ret = EOK;

 done:

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

/*
 * This function retrieves a list of candidate_gpos and potentially reduces it
 * to a list of dacl_filtered_gpos, based on each GPO's DACL.
 *
 * This function then takes the list of dacl_filtered_gpos and potentially
 * reduces it to a list of cse_filtered_gpos, based on whether each GPO's list
 * of cse_guids includes the "SecuritySettings" CSE GUID (used for HBAC).
 *
 * Ultimately, this function then sends each cse_filtered_gpo to the gpo_child,
 * which retrieves the GPT.INI and policy files (as needed). Once all files
 * have been downloaded, the ad_gpo_cse_done function performs HBAC processing.
 */
static void
ad_gpo_process_gpo_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_gpo_access_state *state;
    int ret;
    int dp_error;
    struct gp_gpo **candidate_gpos = NULL;
    int num_candidate_gpos = 0;
    int i = 0;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct ad_gpo_access_state);
    ret = ad_gpo_process_gpo_recv(subreq, state, &candidate_gpos,
                                  &num_candidate_gpos);

    talloc_zfree(subreq);

    ret = sdap_id_op_done(state->sdap_op, ret, &dp_error);

    if (ret != EOK && ret != ENOENT) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to get GPO list from server %s: [%d](%s)\n",
              state->ad_hostname ? state->ad_hostname : "NULL", ret, sss_strerror(ret));
        goto done;
    } else if (ret == ENOENT) {
        DEBUG(SSSDBG_TRACE_FUNC,
              "No GPOs found that apply to this system.\n");
        /*
         * Delete the result object list, since there are no
         * GPOs to include in it.
         */
        ret = sysdb_gpo_delete_gpo_result_object(state, state->host_domain);
        if (ret != EOK) {
            switch (ret) {
            case ENOENT:
                DEBUG(SSSDBG_TRACE_FUNC, "No GPO Result available in cache\n");
                break;
            default:
                DEBUG(SSSDBG_FATAL_FAILURE,
                      "Could not delete GPO Result from cache: [%s]\n",
                      sss_strerror(ret));
                goto done;
            }
        }

        if (state->gpo_implicit_deny == true) {
            DEBUG(SSSDBG_TRACE_FUNC,
                  "No applicable GPOs have been found and ad_gpo_implicit_deny"
                  " is set to 'true'. The user will be denied access.\n");
            ret = ERR_ACCESS_DENIED;
        } else {
            ret = EOK;
        }

        goto done;
    }

    ret = ad_gpo_filter_gpos_by_dacl(state, state->user, state->host_fqdn,
                                     state->user_domain,
                                     state->host_domain,
                                     state->opts->idmap_ctx->map,
                                     candidate_gpos, num_candidate_gpos,
                                     &state->dacl_filtered_gpos,
                                     &state->num_dacl_filtered_gpos);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to filter GPO list by DACL: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto done;
    }

    if (state->dacl_filtered_gpos[0] == NULL) {
        /* since no applicable gpos were found, there is nothing to enforce */
        DEBUG(SSSDBG_TRACE_FUNC,
              "no applicable gpos found after dacl filtering\n");

        /*
         * Delete the result object list, since there are no
         * GPOs to include in it.
         */
        ret = sysdb_gpo_delete_gpo_result_object(state, state->host_domain);
        if (ret != EOK) {
            switch (ret) {
            case ENOENT:
                DEBUG(SSSDBG_TRACE_FUNC, "No GPO Result available in cache\n");
                break;
            default:
                DEBUG(SSSDBG_FATAL_FAILURE,
                      "Could not delete GPO Result from cache: [%s]\n",
                      sss_strerror(ret));
                goto done;
            }
        }

        if (state->gpo_implicit_deny == true) {
            DEBUG(SSSDBG_TRACE_FUNC,
                  "No applicable GPOs have been found and ad_gpo_implicit_deny"
                  " is set to 'true'. The user will be denied access.\n");
            ret = ERR_ACCESS_DENIED;
        } else {
            ret = EOK;
        }

        goto done;
    }

    for (i = 0; i < state->num_dacl_filtered_gpos; i++) {
        DEBUG(SSSDBG_TRACE_FUNC, "dacl_filtered_gpos[%d]->gpo_guid is %s\n", i,
              state->dacl_filtered_gpos[i]->gpo_guid);
    }

    ret = ad_gpo_filter_gpos_by_cse_guid(state,
                                         GP_EXT_GUID_SECURITY,
                                         state->dacl_filtered_gpos,
                                         state->num_dacl_filtered_gpos,
                                         &state->cse_filtered_gpos,
                                         &state->num_cse_filtered_gpos);

    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to filter GPO list by CSE_GUID: [%d](%s)\n",
               ret, sss_strerror(ret));
        goto done;
    }

    if (state->cse_filtered_gpos[0] == NULL) {
        /* no gpos contain "SecuritySettings" cse_guid, nothing to enforce */
        DEBUG(SSSDBG_TRACE_FUNC,
              "no applicable gpos found after cse_guid filtering\n");

        if (state->gpo_implicit_deny == true) {
            DEBUG(SSSDBG_TRACE_FUNC,
                  "No applicable GPOs have been found and ad_gpo_implicit_deny"
                  " is set to 'true'. The user will be denied access.\n");
            ret = ERR_ACCESS_DENIED;
        } else {
            ret = EOK;
        }

        goto done;
    }

    for (i = 0; i < state->num_cse_filtered_gpos; i++) {
        DEBUG(SSSDBG_TRACE_FUNC, "cse_filtered_gpos[%d]->gpo_guid is %s\n", i,
                                  state->cse_filtered_gpos[i]->gpo_guid);
    }

    DEBUG(SSSDBG_TRACE_FUNC, "num_cse_filtered_gpos: %d\n",
          state->num_cse_filtered_gpos);

    /*
     * before we start processing each gpo, we delete the GPO Result object
     * from the sysdb cache so that any previous policy settings are cleared;
     * subsequent functions will add the GPO Result object (and populate it
     * with resultant policy settings) for this policy application
     */
    ret = sysdb_gpo_delete_gpo_result_object(state, state->host_domain);
    if (ret != EOK) {
        switch (ret) {
        case ENOENT:
            DEBUG(SSSDBG_TRACE_FUNC, "No GPO Result available in cache\n");
            break;
        default:
            DEBUG(SSSDBG_FATAL_FAILURE,
                  "Could not delete GPO Result from cache: [%s]\n",
                  sss_strerror(ret));
            goto done;
        }
    }

    ret = ad_gpo_cse_step(req);

 done:

    if (ret == EOK) {
        tevent_req_done(req);
    } else if (ret != EAGAIN) {
        tevent_req_error(req, ret);
    }
}

static errno_t
ad_gpo_cse_step(struct tevent_req *req)
{
    struct tevent_req *subreq;
    struct ad_gpo_access_state *state;
    int i = 0;
    struct ldb_result *res;
    errno_t ret;
    bool send_to_child = true;
    int cached_gpt_version = 0;
    time_t policy_file_timeout = 0;

    state = tevent_req_data(req, struct ad_gpo_access_state);

    struct gp_gpo *cse_filtered_gpo =
        state->cse_filtered_gpos[state->cse_gpo_index];

    /* cse_filtered_gpo is NULL after all GPO policy files have been downloaded */
    if (cse_filtered_gpo == NULL) return EOK;

    DEBUG(SSSDBG_TRACE_FUNC, "cse filtered_gpos[%d]->gpo_guid is %s\n",
          state->cse_gpo_index, cse_filtered_gpo->gpo_guid);
    for (i = 0; i < cse_filtered_gpo->num_gpo_cse_guids; i++) {
        DEBUG(SSSDBG_TRACE_ALL,
              "cse_filtered_gpos[%d]->gpo_cse_guids[%d]->gpo_guid is %s\n",
              state->cse_gpo_index, i, cse_filtered_gpo->gpo_cse_guids[i]);
    }

    DEBUG(SSSDBG_TRACE_FUNC, "smb_server: %s\n", cse_filtered_gpo->smb_server);
    DEBUG(SSSDBG_TRACE_FUNC, "smb_share: %s\n", cse_filtered_gpo->smb_share);
    DEBUG(SSSDBG_TRACE_FUNC, "smb_path: %s\n", cse_filtered_gpo->smb_path);
    DEBUG(SSSDBG_TRACE_FUNC, "gpo_guid: %s\n", cse_filtered_gpo->gpo_guid);

    cse_filtered_gpo->policy_filename =
        talloc_asprintf(state,
                        GPO_CACHE_PATH"%s%s",
                        cse_filtered_gpo->smb_path,
                        GP_EXT_GUID_SECURITY_SUFFIX);
    if (cse_filtered_gpo->policy_filename == NULL) {
        return ENOMEM;
    }

    /* retrieve gpo cache entry; set cached_gpt_version to -1 if unavailable */
    DEBUG(SSSDBG_TRACE_FUNC, "retrieving GPO from cache [%s]\n",
          cse_filtered_gpo->gpo_guid);
    ret = sysdb_gpo_get_gpo_by_guid(state,
                                    state->host_domain,
                                    cse_filtered_gpo->gpo_guid,
                                    &res);
    if (ret == EOK) {
        /*
         * Note: if the timeout is valid, then we can later avoid downloading
         * the GPT.INI file, as well as any policy files (i.e. we don't need
         * to interact with the gpo_child at all). However, even if the timeout
         * is not valid, while we will have to interact with the gpo child to
         * download the GPT.INI file, we may still be able to avoid downloading
         * the policy files (if the cached_gpt_version is the same as the
         * GPT.INI version). In other words, the timeout is *not* an expiration
         * for the entire cache entry; the cached_gpt_version never expires.
         */

        cached_gpt_version = ldb_msg_find_attr_as_int(res->msgs[0],
                                                      SYSDB_GPO_VERSION_ATTR,
                                                      0);

        policy_file_timeout = ldb_msg_find_attr_as_uint64
            (res->msgs[0], SYSDB_GPO_TIMEOUT_ATTR, 0);

        if (policy_file_timeout >= time(NULL)) {
            send_to_child = false;
        }
    } else if (ret == ENOENT) {
        DEBUG(SSSDBG_TRACE_FUNC, "ENOENT\n");
        cached_gpt_version = -1;
    } else {
        DEBUG(SSSDBG_FATAL_FAILURE, "Could not read GPO from cache: [%s]\n",
              sss_strerror(ret));
        return ret;
    }

    DEBUG(SSSDBG_TRACE_FUNC, "send_to_child: %d\n", send_to_child);
    DEBUG(SSSDBG_TRACE_FUNC, "cached_gpt_version: %d\n", cached_gpt_version);

    cse_filtered_gpo->send_to_child = send_to_child;

    subreq = ad_gpo_process_cse_send(state,
                                     state->ev,
                                     send_to_child,
                                     state->host_domain,
                                     cse_filtered_gpo->gpo_guid,
                                     cse_filtered_gpo->gpo_dpname,
                                     cse_filtered_gpo->smb_server,
                                     cse_filtered_gpo->smb_share,
                                     cse_filtered_gpo->smb_path,
                                     GP_EXT_GUID_SECURITY_SUFFIX,
                                     cached_gpt_version,
                                     state->gpo_timeout_option);

    tevent_req_set_callback(subreq, ad_gpo_cse_done, req);
    return EAGAIN;
}

static errno_t
store_hash_maps_in_cache(struct sss_domain_info *domain,
                         hash_table_t *allow_maps, hash_table_t *deny_maps)
{
    int ret;
    struct hash_iter_context_t *iter;
    hash_entry_t *entry;
    size_t c;
    hash_table_t *hash_list[] = { allow_maps, deny_maps, NULL};


    for (c = 0; hash_list[c] != NULL; c++) {
        iter = new_hash_iter_context(hash_list[c]);
        if (iter == NULL) {
            DEBUG(SSSDBG_OP_FAILURE, "Failed to create hash iterator.\n");
            return EINVAL;
        }

        while ((entry = iter->next(iter)) != NULL) {
            ret = sysdb_gpo_store_gpo_result_setting(domain,
                                                     entry->key.c_str,
                                                     entry->value.ptr);
            if (ret != EOK) {
                free(iter);
                DEBUG(SSSDBG_OP_FAILURE,
                      "sysdb_gpo_store_gpo_result_setting failed for key:"
                      "[%s] value:[%s] [%d][%s]\n", entry->key.c_str,
                      (char *) entry->value.ptr, ret, sss_strerror(ret));
                return ret;
            }
        }
        talloc_free(iter);
    }

    return EOK;
}

/*
 * This cse-specific function (GP_EXT_GUID_SECURITY) increments the
 * cse_gpo_index until the policy settings for all applicable GPOs have been
 * stored as part of the GPO Result object in the sysdb cache. Once all
 * GPOs have been processed, this functions performs HBAC processing by
 * comparing the resultant policy setting values in the GPO Result object
 * with the user_sid/group_sids of interest.
 */
static void
ad_gpo_cse_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_gpo_access_state *state;
    int ret;

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

    struct gp_gpo *cse_filtered_gpo =
        state->cse_filtered_gpos[state->cse_gpo_index];

    const char *gpo_guid = cse_filtered_gpo->gpo_guid;
    const char *gpo_dpname = cse_filtered_gpo->gpo_dpname;

    DEBUG(SSSDBG_TRACE_FUNC, "gpo_guid: %s, display name: %s\n",
          gpo_guid, gpo_dpname);

    ret = ad_gpo_process_cse_recv(subreq);

    talloc_zfree(subreq);

    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Unable to retrieve policy data: [%d](%s}\n",
              ret, sss_strerror(ret));
        goto done;
    }

    /*
     * now that the policy file for this gpo have been downloaded to the
     * GPO CACHE, we store all of the supported keys present in the file
     * (as part of the GPO Result object in the sysdb cache).
     */
    ret = ad_gpo_store_policy_settings(state->host_domain,
                                       state->allow_maps, state->deny_maps,
                                       cse_filtered_gpo->policy_filename);
    if (ret != EOK && ret != ENOENT) {
        DEBUG(SSSDBG_OP_FAILURE,
              "ad_gpo_store_policy_settings failed: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto done;
    }

    state->cse_gpo_index++;
    ret = ad_gpo_cse_step(req);

    if (ret == EOK) {
        /* ret is EOK only after all GPO policy files have been downloaded */
        ret = store_hash_maps_in_cache(state->host_domain,
                                       state->allow_maps, state->deny_maps);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "Failed to store evaluated GPO maps "
                                     "[%d][%s].\n", ret, sss_strerror(ret));
            goto done;
        }
        ret = ad_gpo_perform_hbac_processing(state,
                                             state->gpo_mode,
                                             state->gpo_map_type,
                                             state->user,
                                             state->gpo_implicit_deny,
                                             state->user_domain,
                                             state->host_domain,
                                             state->opts->idmap_ctx->map);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE, "HBAC processing failed: [%d](%s}\n",
                  ret, sss_strerror(ret));
            goto done;
        }

    }

 done:

    if (ret == EOK) {
        tevent_req_done(req);
    } else if (ret != EAGAIN) {
        tevent_req_error(req, ret);
    }
}

errno_t
ad_gpo_access_recv(struct tevent_req *req)
{
    TEVENT_REQ_RETURN_ON_ERROR(req);

    return EOK;
}

/* == ad_gpo_process_som_send/recv helpers ================================= */

/*
 * This function returns the parent of an LDAP DN
 */
static errno_t
ad_gpo_parent_dn(TALLOC_CTX *mem_ctx,
                 struct ldb_context *ldb_ctx,
                 const char *dn,
                 const char **_parent_dn)
{
    struct ldb_dn *ldb_dn;
    struct ldb_dn *parent_ldb_dn;
    const char *p;
    int ret;
    TALLOC_CTX *tmp_ctx = NULL;

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

    ldb_dn = ldb_dn_new(tmp_ctx, ldb_ctx, dn);
    parent_ldb_dn = ldb_dn_get_parent(tmp_ctx, ldb_dn);
    p = ldb_dn_get_linearized(parent_ldb_dn);

    *_parent_dn = talloc_steal(mem_ctx, p);
    ret = EOK;

 done:
    talloc_free(tmp_ctx);
    return ret;
}

/*
 * This function populates the _som_list output parameter by parsing the input
 * DN into a list of gp_som objects. This function essentially repeatedly
 * appends the input DN's parent to the SOM List (if the parent starts with
 * "OU=" or "DC="), until the first "DC=" component is reached.
 * Example: if input DN is "CN=MyComputer,CN=Computers,OU=Sales,DC=FOO,DC=COM",
 * then SOM List has 2 SOM entries: {[OU=Sales,DC=FOO,DC=COM], [DC=FOO, DC=COM]}
 */

static errno_t
ad_gpo_populate_som_list(TALLOC_CTX *mem_ctx,
                         struct ldb_context *ldb_ctx,
                         const char *target_dn,
                         int *_num_soms,
                         struct gp_som ***_som_list)
{
    TALLOC_CTX *tmp_ctx = NULL;
    int ret;
    int rdn_count = 0;
    int som_idx = 0;
    struct gp_som **som_list;
    const char *parent_dn = NULL;
    const char *tmp_dn = NULL;
    struct ldb_dn *ldb_target_dn;

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

    ldb_target_dn = ldb_dn_new(tmp_ctx, ldb_ctx, target_dn);
    if (ldb_target_dn == NULL) {
        ret = EINVAL;
        goto done;
    }

    rdn_count = ldb_dn_get_comp_num(ldb_target_dn);
    if (rdn_count == -1) {
        ret = EINVAL;
        goto done;
    }

    if (rdn_count == 0) {
        *_som_list = NULL;
        ret = EOK;
        goto done;
    }

    /* assume the worst-case, in which every parent is a SOM */
    /* include space for Site SOM and NULL: rdn_count + 1 + 1 */
    som_list = talloc_array(tmp_ctx, struct gp_som *, rdn_count + 1 + 1);
    if (som_list == NULL) {
        ret = ENOMEM;
        goto done;
    }

    /* first, populate the OU and Domain SOMs */
    tmp_dn = target_dn;
    while ((ad_gpo_parent_dn(tmp_ctx, ldb_ctx, tmp_dn, &parent_dn)) == EOK) {

        if ((strncasecmp(parent_dn, "OU=", strlen("OU=")) == 0) ||
            (strncasecmp(parent_dn, "DC=", strlen("DC=")) == 0)) {

            som_list[som_idx] = talloc_zero(som_list, struct gp_som);
            if (som_list[som_idx] == NULL) {
                ret = ENOMEM;
                goto done;
            }
            som_list[som_idx]->som_dn = talloc_steal(som_list[som_idx],
                                                     parent_dn);
            if (som_list[som_idx]->som_dn == NULL) {
                ret = ENOMEM;
                goto done;
            }
            som_idx++;
        }

        if (strncasecmp(parent_dn, "DC=", strlen("DC=")) == 0) {
            break;
        }
        tmp_dn = parent_dn;
    }

    som_list[som_idx] = NULL;

    *_num_soms = som_idx;
    *_som_list = talloc_steal(mem_ctx, som_list);

    ret = EOK;

 done:
    talloc_free(tmp_ctx);
    return ret;
}

/*
 * This function populates the _gplink_list output parameter by parsing the
 * input raw_gplink_value into an array of gp_gplink objects, each consisting of
 * a GPO DN and bool enforced field.
 *
 * The raw_gplink_value is single string consisting of multiple gplink strings.
 * The raw_gplink_value is in the following format:
 *  "[GPO_DN_1;GPLinkOptions_1]...[GPO_DN_n;GPLinkOptions_n]"
 *
 * Each gplink string consists of a GPO DN and a GPLinkOptions field (which
 * indicates whether its associated GPO DN is ignored, unenforced, or enforced).
 * If a GPO DN is flagged as ignored, it is discarded and will not be added to
 * the _gplink_list. If the allow_enforced_only input is true, AND a GPO DN is
 * flagged as unenforced, it will also be discarded.
 *
 * Example: if raw_gplink_value="[OU=Sales,DC=FOO,DC=COM;0][DC=FOO,DC=COM;2]"
 *   and allow_enforced_only=FALSE, then the output would consist of following:
 *    _gplink_list[0]: {GPO DN: "OU=Sales,DC=FOO,DC=COM", enforced: FALSE}
 *    _gplink_list[1]: {GPO DN: "DC=FOO,DC=COM",          enforced: TRUE}
 */
static errno_t
ad_gpo_populate_gplink_list(TALLOC_CTX *mem_ctx,
                            const char *som_dn,
                            char *raw_gplink_value,
                            struct gp_gplink ***_gplink_list,
                            bool allow_enforced_only)
{
    TALLOC_CTX *tmp_ctx = NULL;
    char *ptr;
    char *first;
    char *last;
    char *dn;
    char *gplink_options;
    const char delim = ']';
    struct gp_gplink **gplink_list;
    int i;
    int ret;
    uint32_t gplink_number;
    int gplink_count = 0;
    int num_enabled = 0;

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

    DEBUG(SSSDBG_TRACE_FUNC, "som_dn: %s\n", som_dn);
    tmp_ctx = talloc_new(NULL);
    if (tmp_ctx == NULL) {
        ret = ENOMEM;
        goto done;
    }

    ptr = raw_gplink_value;

    while ((ptr = strchr(ptr, delim))) {
        ptr++;
        gplink_count++;
    }

    if (gplink_count == 0) {
        ret = EOK;
        goto done;
    }

    gplink_list = talloc_array(tmp_ctx, struct gp_gplink *, gplink_count + 1);
    if (gplink_list == NULL) {
        ret = ENOMEM;
        goto done;
    }

    num_enabled = 0;
    ptr = raw_gplink_value;
    for (i = 0; i < gplink_count; i++) {
        first = ptr + 1;
        last = strchr(first, delim);
        if (last == NULL) {
            ret = EINVAL;
            goto done;
        }
        *last = '\0';
        last++;
        dn = first;
        if ( strncasecmp(dn, "LDAP://", 7)== 0 ) {
            dn = dn + 7;
        }
        gplink_options = strchr(first, ';');
        if (gplink_options == NULL) {
            ret = EINVAL;
            goto done;
        }
        *gplink_options = '\0';
        gplink_options++;

        gplink_number = strtouint32(gplink_options, NULL, 10);
        if (errno != 0) {
            ret = errno;
            DEBUG(SSSDBG_OP_FAILURE,
                  "strtouint32 failed: [%d](%s)\n", ret, sss_strerror(ret));
            goto done;
        }

        DEBUG(SSSDBG_TRACE_ALL,
              "gplink_list[%d]: [%s; %d]\n", num_enabled, dn, gplink_number);

        if ((gplink_number == 1) || (gplink_number ==3)) {
            /* ignore flag is set */
            DEBUG(SSSDBG_TRACE_ALL, "ignored gpo skipped\n");
            ptr = last;
            continue;
        }

        if (allow_enforced_only && (gplink_number == 0)) {
            /* unenforced flag is set; only enforced gpos allowed */
            DEBUG(SSSDBG_TRACE_ALL, "unenforced gpo skipped\n");
            ptr = last;
            continue;
        }

        gplink_list[num_enabled] = talloc_zero(gplink_list, struct gp_gplink);
        if (gplink_list[num_enabled] == NULL) {
            ret = ENOMEM;
            goto done;
        }
        gplink_list[num_enabled]->gpo_dn =
            talloc_strdup(gplink_list[num_enabled], dn);

        if (gplink_list[num_enabled]->gpo_dn == NULL) {
            ret = ENOMEM;
            goto done;
        }

        if (gplink_number == 0) {
            gplink_list[num_enabled]->enforced = 0;
            num_enabled++;
        } else if (gplink_number == 2) {
            gplink_list[num_enabled]->enforced = 1;
            num_enabled++;
        } else {
            ret = EINVAL;
            goto done;
        }

        ptr = last;
    }
    gplink_list[num_enabled] = NULL;

    *_gplink_list = talloc_steal(mem_ctx, gplink_list);
    ret = EOK;

 done:
    talloc_free(tmp_ctx);
    return ret;
}

/* == ad_gpo_process_som_send/recv implementation ========================== */

struct ad_gpo_process_som_state {
    struct tevent_context *ev;
    struct sdap_id_op *sdap_op;
    struct sdap_options *opts;
    struct dp_option *ad_options;
    int timeout;
    bool allow_enforced_only;
    char *site_name;
    char *site_dn;
    struct gp_som **som_list;
    int som_index;
    int num_soms;
};

static void ad_gpo_site_name_retrieval_done(struct tevent_req *subreq);
static void ad_gpo_site_dn_retrieval_done(struct tevent_req *subreq);
static errno_t ad_gpo_get_som_attrs_step(struct tevent_req *req);
static void ad_gpo_get_som_attrs_done(struct tevent_req *subreq);

/*
 * This function uses the input target_dn and input domain_name to populate
 * a list of gp_som objects. Each object in this list represents a SOM
 * associated with the target (such as OU, Domain, and Site).
 *
 * The inputs are used to determine the DNs of each SOM associated with the
 * target. In turn, the SOM object DNs are used to retrieve certain LDAP
 * attributes of each SOM object, that are parsed into an array of gp_gplink
 * objects, essentially representing the GPOs that have been linked to each
 * SOM object. Note that it is perfectly valid for there to be *no* GPOs
 * linked to a SOM object.
 */
struct tevent_req *
ad_gpo_process_som_send(TALLOC_CTX *mem_ctx,
                        struct tevent_context *ev,
                        struct sdap_id_conn_ctx *conn,
                        struct ldb_context *ldb_ctx,
                        struct sdap_id_op *sdap_op,
                        struct sdap_options *opts,
                        struct dp_option *ad_options,
                        int timeout,
                        const char *target_dn,
                        const char *domain_name)
{
    struct tevent_req *req;
    struct tevent_req *subreq;
    struct ad_gpo_process_som_state *state;
    errno_t ret;

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

    state->ev = ev;
    state->sdap_op = sdap_op;
    state->opts = opts;
    state->ad_options = ad_options;
    state->timeout = timeout;
    state->som_index = 0;
    state->allow_enforced_only = 0;

    ret = ad_gpo_populate_som_list(state, ldb_ctx, target_dn,
                                   &state->num_soms, &state->som_list);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to retrieve SOM List : [%d](%s)\n",
              ret, sss_strerror(ret));
        ret = ENOENT;
        goto immediately;
    }

    if (state->som_list == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "target dn must have at least one parent\n");
        ret = EINVAL;
        goto immediately;
    }

    subreq = ad_domain_info_send(state, state->ev, conn,
                                 state->sdap_op, domain_name);

    if (subreq == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "ad_domain_info_send failed.\n");
        ret = ENOMEM;
        goto immediately;
    }

    tevent_req_set_callback(subreq, ad_gpo_site_name_retrieval_done, req);

    ret = EOK;

 immediately:

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

    return req;
}

static void
ad_gpo_site_name_retrieval_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_gpo_process_som_state *state;
    int ret;
    char *site = NULL;
    char *site_override = NULL;
    const char *attrs[] = {AD_AT_CONFIG_NC, NULL};

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

    /* gpo code only cares about the site name */
    ret = ad_domain_info_recv(subreq, state, NULL, NULL, &site, NULL);
    talloc_zfree(subreq);

    if (ret != EOK || site == NULL) {
        DEBUG(SSSDBG_TRACE_FUNC,
              "Could not autodiscover AD site. This is not fatal if "
              "ad_site option was set.\n");
    }

    site_override = dp_opt_get_string(state->ad_options, AD_SITE);
    if (site_override != NULL) {
        DEBUG(SSSDBG_TRACE_FUNC,
              "Overriding autodiscovered AD site value '%s' with '%s' from "
              "configuration.\n", site ? site : "none", site_override);
    }

    if (site == NULL && site_override == NULL) {
        sss_log(SSS_LOG_WARNING,
                "Could not autodiscover AD site value using DNS and ad_site "
                "option was not set in configuration. GPO will not work. "
                "To work around this issue you can use ad_site option in SSSD "
                "configuration.");
        DEBUG(SSSDBG_OP_FAILURE,
              "Could not autodiscover AD site value using DNS and ad_site "
              "option was not set in configuration. GPO will not work. "
              "To work around this issue you can use ad_site option in SSSD "
              "configuration.\n");
        tevent_req_error(req, ENOENT);
        return;
    }

    state->site_name = talloc_asprintf(state, "cn=%s",
                                       site_override ? site_override
                                                     : site);
    if (state->site_name == NULL) {
        tevent_req_error(req, ENOMEM);
        return;
    }

    DEBUG(SSSDBG_TRACE_FUNC, "Using AD site '%s'.\n", state->site_name);

    /*
     * note: the configNC attribute is being retrieved here from the rootDSE
     * entry. In future, since we already make an LDAP query for the rootDSE
     * entry when LDAP connection is made, this attribute should really be
     * retrieved at that point (see https://fedorahosted.org/sssd/ticket/2276)
     */
    subreq = sdap_get_generic_send(state, state->ev, state->opts,
                                   sdap_id_op_handle(state->sdap_op),
                                   "", LDAP_SCOPE_BASE,
                                   "(objectclass=*)", attrs, NULL, 0,
                                   state->timeout,
                                   false);

    if (subreq == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "sdap_get_generic_send failed.\n");
        tevent_req_error(req, ENOMEM);
        return;
    }

    tevent_req_set_callback(subreq, ad_gpo_site_dn_retrieval_done, req);
}

static void
ad_gpo_site_dn_retrieval_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_gpo_process_som_state *state;
    int ret;
    int dp_error;
    int i = 0;
    size_t reply_count;
    struct sysdb_attrs **reply;
    const char *configNC;

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

    ret = sdap_get_generic_recv(subreq, state,
                                &reply_count, &reply);
    talloc_zfree(subreq);
    if (ret != EOK) {
        ret = sdap_id_op_done(state->sdap_op, ret, &dp_error);

        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to get configNC: [%d](%s)\n", ret, sss_strerror(ret));
        ret = ENOENT;
        goto done;
    }

    /* make sure there is only one non-NULL reply returned */

    if (reply_count < 1) {
        DEBUG(SSSDBG_OP_FAILURE, "No configNC retrieved\n");
        ret = ENOENT;
        goto done;
    } else if (reply_count > 1) {
        DEBUG(SSSDBG_OP_FAILURE, "Multiple replies for configNC\n");
        ret = ERR_INTERNAL;
        goto done;
    } else if (reply == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "reply_count is 1, but reply is NULL\n");
        ret = ERR_INTERNAL;
        goto done;
    }

    /* reply[0] holds requested attributes of single reply */
    ret = sysdb_attrs_get_string(reply[0], AD_AT_CONFIG_NC, &configNC);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "sysdb_attrs_get_string failed: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto done;
    }
    state->site_dn =
        talloc_asprintf(state, "%s,cn=Sites,%s", state->site_name, configNC);
    if (state->site_dn == NULL) {
        ret = ENOMEM;
        goto done;
    }

    /* note that space was allocated for site_dn when allocating som_list */
    state->som_list[state->num_soms] =
        talloc_zero(state->som_list, struct gp_som);
    if (state->som_list[state->num_soms] == NULL) {
        ret = ENOMEM;
        goto done;
    }

    state->som_list[state->num_soms]->som_dn =
        talloc_steal(state->som_list[state->num_soms], state->site_dn);

    if (state->som_list[state->num_soms]->som_dn == NULL) {
        ret = ENOMEM;
        goto done;
    }

    state->num_soms++;
    state->som_list[state->num_soms] = NULL;

    i = 0;
    while (state->som_list[i]) {
        DEBUG(SSSDBG_TRACE_FUNC, "som_list[%d]->som_dn is %s\n", i,
              state->som_list[i]->som_dn);
        i++;
    }

    ret = ad_gpo_get_som_attrs_step(req);

 done:

    if (ret == EOK) {
        tevent_req_done(req);
    } else if (ret != EAGAIN) {
        tevent_req_error(req, ret);
    }

}
static errno_t
ad_gpo_get_som_attrs_step(struct tevent_req *req)
{
    const char *attrs[] = {AD_AT_GPLINK, AD_AT_GPOPTIONS, NULL};
    struct tevent_req *subreq;
    struct ad_gpo_process_som_state *state;

    state = tevent_req_data(req, struct ad_gpo_process_som_state);

    struct gp_som *gp_som = state->som_list[state->som_index];

    /* gp_som is NULL only after all SOMs have been processed */
    if (gp_som == NULL) return EOK;

    const char *som_dn = gp_som->som_dn;
    subreq = sdap_get_generic_send(state, state->ev,  state->opts,
                                   sdap_id_op_handle(state->sdap_op),
                                   som_dn, LDAP_SCOPE_BASE,
                                   "(objectclass=*)", attrs, NULL, 0,
                                   state->timeout,
                                   false);

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

    tevent_req_set_callback(subreq, ad_gpo_get_som_attrs_done, req);
    return EAGAIN;
}

static void
ad_gpo_get_som_attrs_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_gpo_process_som_state *state;
    int ret;
    int dp_error;
    size_t num_results;
    struct sysdb_attrs **results;
    struct ldb_message_element *el = NULL;
    uint8_t *raw_gplink_value;
    uint8_t *raw_gpoptions_value;
    uint32_t allow_enforced_only = 0;
    struct gp_som *gp_som;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct ad_gpo_process_som_state);
    ret = sdap_get_generic_recv(subreq, state,
                                &num_results, &results);
    talloc_zfree(subreq);

    if (ret != EOK) {
        ret = sdap_id_op_done(state->sdap_op, ret, &dp_error);

        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to get SOM attributes: [%d](%s)\n",
              ret, sss_strerror(ret));
        ret = ENOENT;
        goto done;
    }
    if ((num_results < 1) || (results == NULL)) {
        DEBUG(SSSDBG_FUNC_DATA, "no attrs found for SOM; try next SOM.\n");
        state->som_index++;
        ret = ad_gpo_get_som_attrs_step(req);
        goto done;
    } else if (num_results > 1) {
        DEBUG(SSSDBG_OP_FAILURE, "Received multiple replies\n");
        ret = ERR_INTERNAL;
        goto done;
    }

    /* Get the gplink value, if available */
    ret = sysdb_attrs_get_el(results[0], AD_AT_GPLINK, &el);

    if (ret != EOK && ret != ENOENT) {
        DEBUG(SSSDBG_OP_FAILURE,
              "sysdb_attrs_get_el() failed: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto done;
    }

    if ((ret == ENOENT) || (el->num_values == 0)) {
        DEBUG(SSSDBG_FUNC_DATA, "gpLink attr not found or has no values\n");
        state->som_index++;
        ret = ad_gpo_get_som_attrs_step(req);
        goto done;
    }

    raw_gplink_value = el[0].values[0].data;

    ret = sysdb_attrs_get_el(results[0], AD_AT_GPOPTIONS, &el);

    if (ret != EOK && ret != ENOENT) {
        DEBUG(SSSDBG_OP_FAILURE, "sysdb_attrs_get_el() failed\n");
        goto done;
    }

    if ((ret == ENOENT) || (el->num_values == 0)) {
        DEBUG(SSSDBG_TRACE_ALL,
              "gpoptions attr not found or has no value; defaults to 0\n");
        allow_enforced_only = 0;
    }  else {
        raw_gpoptions_value = el[0].values[0].data;
        allow_enforced_only = strtouint32((char *)raw_gpoptions_value, NULL, 10);
        if (errno != 0) {
            ret = errno;
            DEBUG(SSSDBG_OP_FAILURE,
                  "strtouint32 failed: [%d](%s)\n", ret, sss_strerror(ret));
            goto done;
        }
    }

    gp_som = state->som_list[state->som_index];
    ret = ad_gpo_populate_gplink_list(gp_som,
                                      gp_som->som_dn,
                                      (char *)raw_gplink_value,
                                      &gp_som->gplink_list,
                                      state->allow_enforced_only);

    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "ad_gpo_populate_gplink_list() failed\n");
        goto done;
    }

    if (allow_enforced_only) {
        state->allow_enforced_only = 1;
    }

    state->som_index++;
    ret = ad_gpo_get_som_attrs_step(req);

 done:

    if (ret == EOK) {
        tevent_req_done(req);
    } else if (ret != EAGAIN) {
        tevent_req_error(req, ret);
    }
}

int
ad_gpo_process_som_recv(struct tevent_req *req,
                        TALLOC_CTX *mem_ctx,
                        struct gp_som ***som_list)
{

    struct ad_gpo_process_som_state *state =
        tevent_req_data(req, struct ad_gpo_process_som_state);

    TEVENT_REQ_RETURN_ON_ERROR(req);
    *som_list = talloc_steal(mem_ctx, state->som_list);
    return EOK;
}

/* == ad_gpo_process_gpo_send/recv helpers ================================= */

/*
 * This function examines the gp_gplink objects in each gp_som object specified
 * in the input som_list, and populates the _candidate_gpos output parameter's
 * gpo_dn fields with prioritized list of GPO DNs. Prioritization ensures that:
 * - GPOs linked to an OU will be applied after GPOs linked to a Domain,
 *   which will be applied after GPOs linked to a Site.
 * - multiple GPOs linked to a single SOM are applied in their link order
 *   (i.e. 1st GPO linked to SOM is applied before 2nd GPO linked to SOM, etc).
 * - enforced GPOs are applied after unenforced GPOs.
 *
 * As such, the _candidate_gpos output's dn fields looks like (in link order):
 * [unenforced {Site, Domain, OU}; enforced {OU, Domain, Site}]
 *
 * Note that in the case of conflicting policy settings, GPOs appearing later
 * in the list will trump GPOs appearing earlier in the list. Therefore the
 * enforced GPOs are applied in revers order after the unenforced GPOs to
 * make sure the enforced setting form the highest level will be applied.
 *
 * GPO processing details can be found e.g. at
 * https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn581922(v%3Dws.11)
 */
static errno_t
ad_gpo_populate_candidate_gpos(TALLOC_CTX *mem_ctx,
                               struct gp_som **som_list,
                               struct gp_gpo ***_candidate_gpos,
                               int *_num_candidate_gpos)
{

    TALLOC_CTX *tmp_ctx = NULL;
    struct gp_som *gp_som = NULL;
    struct gp_gplink *gp_gplink = NULL;
    struct gp_gpo **candidate_gpos = NULL;
    int num_candidate_gpos = 0;
    const char **enforced_gpo_dns = NULL;
    const char **unenforced_gpo_dns = NULL;
    int gpo_dn_idx = 0;
    int num_enforced = 0;
    int enforced_idx = 0;
    int num_unenforced = 0;
    int unenforced_idx = 0;
    int i = 0;
    int j = 0;
    int ret;
    size_t som_count = 0;

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

    while (som_list[i]) {
        gp_som = som_list[i];
        j = 0;
        while (gp_som && gp_som->gplink_list && gp_som->gplink_list[j]) {
            gp_gplink = gp_som->gplink_list[j];
            if (gp_gplink == NULL) {
                DEBUG(SSSDBG_OP_FAILURE, "unexpected null gp_gplink\n");
                ret = EINVAL;
                goto done;
            }
            if (gp_gplink->enforced) {
                num_enforced++;
            } else {
                num_unenforced++;
            }
            j++;
        }
        i++;
    }
    som_count = i;

    num_candidate_gpos = num_enforced + num_unenforced;

    if (num_candidate_gpos == 0) {
        *_candidate_gpos = NULL;
        *_num_candidate_gpos = 0;
        ret = EOK;
        goto done;
    }

    enforced_gpo_dns = talloc_array(tmp_ctx, const char *, num_enforced + 1);
    if (enforced_gpo_dns == NULL) {
        ret = ENOMEM;
        goto done;
    }

    unenforced_gpo_dns = talloc_array(tmp_ctx, const char *, num_unenforced + 1);
    if (unenforced_gpo_dns == NULL) {
        ret = ENOMEM;
        goto done;
    }

    i = som_count -1 ;
    while (i >= 0) {
        gp_som = som_list[i];

        /* For unenforced_gpo_dns the most specific GPOs with the highest
         * priority should be the last. We start with the top-level SOM and go
         * down to the most specific one and add the unenforced following the
         * gplink_list where the GPO with the highest priority comes last. */
        j = 0;
        while (gp_som && gp_som->gplink_list && gp_som->gplink_list[j]) {
                gp_gplink = gp_som->gplink_list[j];

                if (!gp_gplink->enforced) {
                    unenforced_gpo_dns[unenforced_idx] =
                        talloc_steal(unenforced_gpo_dns, gp_gplink->gpo_dn);

                    if (unenforced_gpo_dns[unenforced_idx] == NULL) {
                        ret = ENOMEM;
                        goto done;
                    }
                    unenforced_idx++;
                }
                j++;
        }
        i--;
    }

    i = 0;
    while (som_list[i]) {
        gp_som = som_list[i];

        /* For enforced GPOs we start processing with the most specific SOM to
         * make sur enforced GPOs from higher levels override to lower level
         * ones. According to the 'Group Policy Inheritance' tab in the
         * Windows 'Goup Policy Management' utility in the same SOM the link
         * order is still observed and an enforced GPO with a lower link order
         * value still overrides an enforced GPO with a higher link order. */
        j = 0;
        while (gp_som && gp_som->gplink_list && gp_som->gplink_list[j]) {
            gp_gplink = gp_som->gplink_list[j];
            if (gp_gplink == NULL) {
                DEBUG(SSSDBG_OP_FAILURE, "unexpected null gp_gplink\n");
                ret = EINVAL;
                goto done;
            }

            if (gp_gplink->enforced) {
                enforced_gpo_dns[enforced_idx] =
                    talloc_steal(enforced_gpo_dns, gp_gplink->gpo_dn);
                if (enforced_gpo_dns[enforced_idx] == NULL) {
                    ret = ENOMEM;
                    goto done;
                }
                enforced_idx++;
            }
            j++;
        }
        i++;
    }
    enforced_gpo_dns[num_enforced] = NULL;
    unenforced_gpo_dns[num_unenforced] = NULL;

    candidate_gpos = talloc_array(tmp_ctx,
                                  struct gp_gpo *,
                                  num_candidate_gpos + 1);

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

    gpo_dn_idx = 0;
    for (i = 0; i < num_unenforced; i++) {
        candidate_gpos[gpo_dn_idx] = talloc_zero(candidate_gpos, struct gp_gpo);
        if (candidate_gpos[gpo_dn_idx] == NULL) {
            ret = ENOMEM;
            goto done;
        }
        candidate_gpos[gpo_dn_idx]->gpo_dn =
            talloc_steal(candidate_gpos[gpo_dn_idx], unenforced_gpo_dns[i]);

        if (candidate_gpos[gpo_dn_idx]->gpo_dn == NULL) {
            ret = ENOMEM;
            goto done;
        }
        DEBUG(SSSDBG_TRACE_FUNC,
              "candidate_gpos[%d]->gpo_dn: %s\n",
              gpo_dn_idx, candidate_gpos[gpo_dn_idx]->gpo_dn);
        gpo_dn_idx++;
    }

    for (i = 0; i < num_enforced; i++) {

        candidate_gpos[gpo_dn_idx] = talloc_zero(candidate_gpos, struct gp_gpo);
        if (candidate_gpos[gpo_dn_idx] == NULL) {
            ret = ENOMEM;
            goto done;
        }

        candidate_gpos[gpo_dn_idx]->gpo_dn =
            talloc_steal(candidate_gpos[gpo_dn_idx], enforced_gpo_dns[i]);
        if (candidate_gpos[gpo_dn_idx]->gpo_dn == NULL) {
            ret = ENOMEM;
            goto done;
        }

        DEBUG(SSSDBG_TRACE_FUNC,
              "candidate_gpos[%d]->gpo_dn: %s\n",
              gpo_dn_idx, candidate_gpos[gpo_dn_idx]->gpo_dn);
        gpo_dn_idx++;
    }

    candidate_gpos[gpo_dn_idx] = NULL;

    *_candidate_gpos = talloc_steal(mem_ctx, candidate_gpos);
    *_num_candidate_gpos = num_candidate_gpos;

    ret = EOK;

 done:
    talloc_free(tmp_ctx);
    return ret;
}

/*
 * This function parses the input_path into its components, replaces each
 * back slash ('\') with a forward slash ('/'), and populates the output params.
 *
 * The smb_server output is constructed by concatenating the following elements:
 * - SMB_STANDARD_URI ("smb://")
 * - server_hostname (which replaces domain_name in input path)
 * The smb_share and smb_path outputs are extracted from the input_path.
 *
 * Example: if input_path = "\\foo.com\SysVol\foo.com\..." and
 * server_hostname = "adserver.foo.com", then
 *   _smb_server = "smb://adserver.foo.com"
 *   _smb_share = "SysVol"
 *   _smb_path = "/foo.com/..."
 *
 * Note that the input_path must have at least four forward slash separators.
 * For example, input_path = "\\foo.com\SysVol" is not a valid input_path,
 * because it has only three forward slash separators.
 */
static errno_t
ad_gpo_extract_smb_components(TALLOC_CTX *mem_ctx,
                              char *server_hostname,
                              char *input_path,
                              const char **_smb_server,
                              const char **_smb_share,
                              const char **_smb_path)
{
    char *ptr;
    const char delim = '\\';
    int ret;
    int num_seps = 0;
    char *smb_path = NULL;
    char *smb_share = NULL;

    DEBUG(SSSDBG_TRACE_ALL, "input_path: %s\n", input_path);

    if (input_path == NULL ||
        *input_path == '\0' ||
        _smb_server == NULL ||
        _smb_share == NULL ||
        _smb_path == NULL) {
        ret = EINVAL;
        goto done;
    }

    ptr = input_path;
    while ((ptr = strchr(ptr, delim))) {
        num_seps++;
        if (num_seps == 3) {
            /* replace the slash before the share name with null string */

            *ptr = '\0';
            ptr++;
            smb_share = ptr;
            continue;
        } else if (num_seps == 4) {
            /* replace the slash after the share name with null string */
            *ptr = '\0';
            ptr++;
            smb_path = ptr;
            continue;
        }
        *ptr = '/';
        ptr++;
    }

    if (num_seps == 0) {
        ret = EINVAL;
        goto done;
    }

    if (smb_path == NULL)  {
        ret = EINVAL;
        goto done;
    }

    *_smb_server = talloc_asprintf(mem_ctx, "%s%s",
                                   SMB_STANDARD_URI,
                                   server_hostname);
    if (*_smb_server == NULL) {
        ret = ENOMEM;
        goto done;
    }

    *_smb_share = talloc_asprintf(mem_ctx, "/%s", smb_share);
    if (*_smb_share == NULL) {
        ret = ENOMEM;
        goto done;
    }

    *_smb_path = talloc_asprintf(mem_ctx, "/%s", smb_path);
    if (*_smb_path == NULL) {
        ret = ENOMEM;
        goto done;
    }

    ret = EOK;

 done:
    return ret;
}

/*
 * This function populates the _cse_guid_list output parameter by parsing the
 * input raw_machine_ext_names_value into an array of cse_guid strings.
 *
 * The raw_machine_ext_names_value is a single string in the following format:
 * "[{cse_guid_1}{tool_guid1}]...[{cse_guid_n}{tool_guid_n}]"
 */
static errno_t
ad_gpo_parse_machine_ext_names(TALLOC_CTX *mem_ctx,
                               char *raw_machine_ext_names_value,
                               const char ***_gpo_cse_guids,
                               int *_num_gpo_cse_guids)
{
    TALLOC_CTX *tmp_ctx = NULL;
    char *ptr;
    char *first;
    char *last;
    char *cse_guid;
    char *tool_guid;
    const char delim = ']';
    const char **gpo_cse_guids;
    int i;
    int ret;
    int num_gpo_cse_guids = 0;

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

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

    ptr = raw_machine_ext_names_value;
    while ((ptr = strchr(ptr, delim))) {
        ptr++;
        num_gpo_cse_guids++;
    }

    if (num_gpo_cse_guids == 0) {
        ret = EINVAL;
        goto done;
    }

    gpo_cse_guids = talloc_array(tmp_ctx, const char *, num_gpo_cse_guids + 1);
    if (gpo_cse_guids == NULL) {
        ret = ENOMEM;
        goto done;
    }

    ptr = raw_machine_ext_names_value;
    for (i = 0; i < num_gpo_cse_guids; i++) {
        first = ptr + 1;
        last = strchr(first, delim);
        if (last == NULL) {
            break;
        }
        *last = '\0';
        last++;
        cse_guid = first;
        first ++;
        tool_guid = strchr(first, '{');
        if (tool_guid == NULL) {
            break;
        }
        *tool_guid = '\0';
        gpo_cse_guids[i] = talloc_strdup(gpo_cse_guids, cse_guid);
        ptr = last;
    }
    gpo_cse_guids[i] = NULL;

    DEBUG(SSSDBG_TRACE_ALL, "num_gpo_cse_guids: %d\n", num_gpo_cse_guids);

    for (i = 0; i < num_gpo_cse_guids; i++) {
        DEBUG(SSSDBG_TRACE_ALL,
              "gpo_cse_guids[%d] is %s\n", i, gpo_cse_guids[i]);
    }

    *_gpo_cse_guids = talloc_steal(mem_ctx, gpo_cse_guids);
    *_num_gpo_cse_guids = num_gpo_cse_guids;
    ret = EOK;

 done:
    talloc_free(tmp_ctx);
    return ret;
}

enum ndr_err_code
ad_gpo_ndr_pull_security_descriptor(struct ndr_pull *ndr, int ndr_flags,
                                    struct security_descriptor *r);

/*
 * This function parses the input data blob and assigns the resulting
 * security_descriptor object to the _gpo_sd output parameter.
 */
static errno_t ad_gpo_parse_sd(TALLOC_CTX *mem_ctx,
                               uint8_t *data,
                               size_t length,
                               struct security_descriptor **_gpo_sd)
{

    struct ndr_pull *ndr_pull = NULL;
    struct security_descriptor sd;
    DATA_BLOB blob;
    enum ndr_err_code ndr_err;

    blob.data = data;
    blob.length = length;

    ndr_pull = ndr_pull_init_blob(&blob, mem_ctx);
    if (ndr_pull == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "ndr_pull_init_blob() failed.\n");
        return EINVAL;
    }

    ndr_err = ad_gpo_ndr_pull_security_descriptor(ndr_pull,
                                                  NDR_SCALARS|NDR_BUFFERS,
                                                  &sd);

    if (ndr_err != NDR_ERR_SUCCESS) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to pull security descriptor\n");
        return EINVAL;
    }

    *_gpo_sd = talloc_memdup(mem_ctx, &sd, sizeof(struct security_descriptor));

    return EOK;
}

/* == ad_gpo_process_gpo_send/recv implementation ========================== */

struct ad_gpo_process_gpo_state {
    struct ad_access_ctx *access_ctx;
    struct tevent_context *ev;
    struct sdap_id_op *sdap_op;
    struct dp_option *ad_options;
    struct sdap_options *opts;
    char *server_hostname;
    struct sss_domain_info *host_domain;
    int timeout;
    struct gp_gpo **candidate_gpos;
    int num_candidate_gpos;
    int gpo_index;
};

static errno_t ad_gpo_get_gpo_attrs_step(struct tevent_req *req);
static void ad_gpo_get_gpo_attrs_done(struct tevent_req *subreq);

/*
 * This function uses the input som_list to populate a prioritized list of
 * gp_gpo objects, prioritized based on SOM type, link order, and whether the
 * GPO is "enforced". This list represents the initial set of candidate GPOs
 * that might be applicable to the target. This list can not be expanded, but
 * it might be reduced based on subsequent filtering steps. The GPO object DNs
 * are used to retrieve certain LDAP attributes of each GPO object, that are
 * parsed into the various fields of the gp_gpo object.
 */
struct tevent_req *
ad_gpo_process_gpo_send(TALLOC_CTX *mem_ctx,
                        struct tevent_context *ev,
                        struct sdap_id_op *sdap_op,
                        struct sdap_options *opts,
                        char *server_hostname,
                        struct sss_domain_info *host_domain,
                        struct ad_access_ctx *access_ctx,
                        int timeout,
                        struct gp_som **som_list)
{
    struct tevent_req *req;
    struct ad_gpo_process_gpo_state *state;
    errno_t ret;

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

    state->ev = ev;
    state->sdap_op = sdap_op;
    state->ad_options = access_ctx->ad_options;
    state->opts = opts;
    state->server_hostname = server_hostname;
    state->host_domain = host_domain;
    state->access_ctx = access_ctx;
    state->timeout = timeout;
    state->gpo_index = 0;
    state->candidate_gpos = NULL;
    state->num_candidate_gpos = 0;

    ret = ad_gpo_populate_candidate_gpos(state,
                                         som_list,
                                         &state->candidate_gpos,
                                         &state->num_candidate_gpos);

    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to retrieve GPO List: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto immediately;
    }

    if (state->candidate_gpos == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "no gpos found\n");
        ret = ENOENT;
        goto immediately;
    }

    ret = ad_gpo_get_gpo_attrs_step(req);

immediately:

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

    return req;
}

static errno_t
ad_gpo_get_gpo_attrs_step(struct tevent_req *req)
{
    const char *attrs[] = AD_GPO_ATTRS;
    struct tevent_req *subreq;
    struct ad_gpo_process_gpo_state *state;

    state = tevent_req_data(req, struct ad_gpo_process_gpo_state);

    struct gp_gpo *gp_gpo = state->candidate_gpos[state->gpo_index];

    /* gp_gpo is NULL only after all GPOs have been processed */
    if (gp_gpo == NULL) return EOK;

    const char *gpo_dn = gp_gpo->gpo_dn;

    subreq = sdap_sd_search_send(state, state->ev,
                                 state->opts, sdap_id_op_handle(state->sdap_op),
                                 gpo_dn, SECINFO_DACL, attrs, state->timeout);

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

    tevent_req_set_callback(subreq, ad_gpo_get_gpo_attrs_done, req);
    return EAGAIN;
}

static errno_t
ad_gpo_sd_process_attrs(struct tevent_req *req,
                        char *smb_host,
                        struct sysdb_attrs *result);
void
ad_gpo_get_sd_referral_done(struct tevent_req *subreq);

static struct tevent_req *
ad_gpo_get_sd_referral_send(TALLOC_CTX *mem_ctx,
                            struct tevent_context *ev,
                            struct ad_access_ctx *access_ctx,
                            struct sdap_options *opts,
                            const char *referral,
                            struct sss_domain_info *host_domain,
                            int timeout);
errno_t
ad_gpo_get_sd_referral_recv(struct tevent_req *req,
                            TALLOC_CTX *mem_ctx,
                            char **_smb_host,
                            struct sysdb_attrs **_reply);

static void
ad_gpo_get_gpo_attrs_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_gpo_process_gpo_state *state;
    int ret;
    int dp_error;
    size_t num_results, refcount;
    struct sysdb_attrs **results;
    char **refs;

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

    ret = sdap_sd_search_recv(subreq, state,
                              &num_results, &results,
                              &refcount, &refs);
    talloc_zfree(subreq);

    if (ret != EOK) {
        ret = sdap_id_op_done(state->sdap_op, ret, &dp_error);

        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to get GPO attributes: [%d](%s)\n",
              ret, sss_strerror(ret));
        ret = ENOENT;
        goto done;
    }

    if ((num_results < 1) || (results == NULL)) {
        if (refcount == 1) {
            /* If we were redirected to a referral, process it.
             * There must be a single referral result here; if we get
             * more than one (or zero) it's a bug.
             */

            subreq = ad_gpo_get_sd_referral_send(state, state->ev,
                                                 state->access_ctx,
                                                 state->opts,
                                                 refs[0],
                                                 state->host_domain,
                                                 state->timeout);
            if (!subreq) {
                ret = ENOMEM;
                goto done;
            }

            tevent_req_set_callback(subreq, ad_gpo_get_sd_referral_done, req);
            ret = EAGAIN;
            goto done;

        } else {
            const char *gpo_dn = state->candidate_gpos[state->gpo_index]->gpo_dn;

            DEBUG(SSSDBG_OP_FAILURE,
                  "No attrs found for GPO [%s].\n", gpo_dn);
            ret = ENOENT;
            goto done;
        }
    } else if (num_results > 1) {
        DEBUG(SSSDBG_OP_FAILURE, "Received multiple replies\n");
        ret = ERR_INTERNAL;
        goto done;
    }

    ret = ad_gpo_sd_process_attrs(req, state->server_hostname, results[0]);

done:

   if (ret == EOK) {
       tevent_req_done(req);
   } else if (ret != EAGAIN) {
       tevent_req_error(req, ret);
   }
}

void
ad_gpo_get_sd_referral_done(struct tevent_req *subreq)
{
    errno_t ret;
    int dp_error;
    struct sysdb_attrs *reply;
    char *smb_host;

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

    ret = ad_gpo_get_sd_referral_recv(subreq, state, &smb_host, &reply);
    talloc_zfree(subreq);
    if (ret != EOK) {
        /* Terminate the sdap_id_op */
        ret = sdap_id_op_done(state->sdap_op, ret, &dp_error);

        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to get referred GPO attributes: [%d](%s)\n",
              ret, sss_strerror(ret));

        goto done;
    }

    /* Lookup succeeded. Process it */
    ret = ad_gpo_sd_process_attrs(req, smb_host, reply);

done:

   if (ret == EOK) {
       tevent_req_done(req);
   } else if (ret != EAGAIN) {
       tevent_req_error(req, ret);
   }
}

static bool machine_ext_names_is_blank(char *attr_value)
{
    char *ptr;

    if (attr_value == NULL) {
        return true;
    }

    ptr = attr_value;
    for (; *ptr != '\0'; ptr++) {
        if (!isspace(*ptr)) {
            return false;
        }
    }

    return true;
}

static errno_t
ad_gpo_missing_or_unreadable_attr(struct ad_gpo_process_gpo_state *state,
                                  struct tevent_req *req)
{
    bool ignore_unreadable = dp_opt_get_bool(state->ad_options,
                                             AD_GPO_IGNORE_UNREADABLE);

    if (ignore_unreadable) {
        /* If admins decided to skip GPOs with unreadable
         * attributes just log the SID of skipped GPO */
        DEBUG(SSSDBG_TRACE_FUNC,
              "Group Policy Container with DN [%s] has unreadable or missing "
              "attributes -> skipping this GPO "
              "(ad_gpo_ignore_unreadable = True)\n",
              state->candidate_gpos[state->gpo_index]->gpo_dn);
        state->gpo_index++;
        return ad_gpo_get_gpo_attrs_step(req);
    } else {
        /* Inform in logs and syslog that this GPO can
         * not be processed due to unreadable or missing
         * attributes and point to possible server side
         * and client side solutions. */
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Group Policy Container with DN [%s] is unreadable or has "
              "unreadable or missing attributes. In order to fix this "
              "make sure that this AD object has following attributes "
              "readable: %s, nTSecurityDescriptor, cn, gPCFileSysPath, "
              "gPCMachineExtensionNames, gPCFunctionalityVersion, flags. "
              "Alternatively if you do not have access to the server or can "
              "not change permissions on this object, you can use option "
              "ad_gpo_ignore_unreadable = True which will skip this GPO. "
              "See ad_gpo_ignore_unreadable in 'man sssd-ad' for details.\n",
              AD_AT_DISPLAY_NAME,
              state->candidate_gpos[state->gpo_index]->gpo_dn);
        sss_log(SSS_LOG_ERR,
                "Group Policy Container with DN [%s] is unreadable or has "
                "unreadable or missing attributes. In order to fix this "
                "make sure that this AD object has following attributes "
                "readable: %s, nTSecurityDescriptor, cn, gPCFileSysPath, "
                "gPCMachineExtensionNames, gPCFunctionalityVersion, flags. "
                "Alternatively if you do not have access to the server or can "
                "not change permissions on this object, you can use option "
                "ad_gpo_ignore_unreadable = True which will skip this GPO. "
                "See ad_gpo_ignore_unreadable in 'man sssd-ad' for details.\n",
                AD_AT_DISPLAY_NAME,
                state->candidate_gpos[state->gpo_index]->gpo_dn);
        return EFAULT;
    }
}

static errno_t
ad_gpo_sd_process_attrs(struct tevent_req *req,
                        char *smb_host,
                        struct sysdb_attrs *result)
{
    struct ad_gpo_process_gpo_state *state;
    struct gp_gpo *gp_gpo;
    int ret;
    struct ldb_message_element *el = NULL;
    const char *gpo_dpname = NULL;
    const char *gpo_guid = NULL;
    const char *raw_file_sys_path = NULL;
    char *file_sys_path = NULL;
    uint8_t *raw_machine_ext_names = NULL;

    state = tevent_req_data(req, struct ad_gpo_process_gpo_state);
    gp_gpo = state->candidate_gpos[state->gpo_index];

    /* retrieve AD_AT_DISPLAY_NAME */
    ret = sysdb_attrs_get_string(result, AD_AT_DISPLAY_NAME, &gpo_dpname);
    if (ret == ENOENT) {
        ret = ad_gpo_missing_or_unreadable_attr(state, req);
        goto done;
    } else if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "sysdb_attrs_get_string failed: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto done;
    }

    gp_gpo->gpo_dpname = talloc_steal(gp_gpo, gpo_dpname);
    if (gp_gpo->gpo_dpname == NULL) {
        ret = ENOMEM;
        goto done;
    }

    /* retrieve AD_AT_CN */
    ret = sysdb_attrs_get_string(result, AD_AT_CN, &gpo_guid);
    if (ret == ENOENT) {
        ret = ad_gpo_missing_or_unreadable_attr(state, req);
        goto done;
    } else if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "sysdb_attrs_get_string failed: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto done;
    }

    gp_gpo->gpo_guid = talloc_steal(gp_gpo, gpo_guid);
    if (gp_gpo->gpo_guid == NULL) {
        ret = ENOMEM;
        goto done;
    }

    DEBUG(SSSDBG_TRACE_ALL, "populating attrs for gpo_guid: %s\n",
          gp_gpo->gpo_guid);

    /* retrieve AD_AT_FILE_SYS_PATH */
    ret = sysdb_attrs_get_string(result,
                                 AD_AT_FILE_SYS_PATH,
                                 &raw_file_sys_path);

    if (ret == ENOENT) {
        ret = ad_gpo_missing_or_unreadable_attr(state, req);
        goto done;
    } else if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "sysdb_attrs_get_string failed: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto done;
    }

    file_sys_path = talloc_strdup(gp_gpo, raw_file_sys_path);

    ret = ad_gpo_extract_smb_components(gp_gpo, smb_host,
                                        file_sys_path, &gp_gpo->smb_server,
                                        &gp_gpo->smb_share, &gp_gpo->smb_path);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "unable to extract smb components from file_sys_path: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto done;
    }

    DEBUG(SSSDBG_TRACE_ALL, "smb_server: %s\n", gp_gpo->smb_server);
    DEBUG(SSSDBG_TRACE_ALL, "smb_share: %s\n", gp_gpo->smb_share);
    DEBUG(SSSDBG_TRACE_ALL, "smb_path: %s\n", gp_gpo->smb_path);

    /* retrieve AD_AT_FUNC_VERSION */
    ret = sysdb_attrs_get_int32_t(result, AD_AT_FUNC_VERSION,
                                  &gp_gpo->gpo_func_version);
    if (ret == ENOENT) {
        /* If this attribute is missing we can skip the GPO. It will
         * be filtered out according to MS-GPOL:
         * https://msdn.microsoft.com/en-us/library/cc232538.aspx */
        DEBUG(SSSDBG_TRACE_ALL, "GPO with GUID %s is missing attribute "
              AD_AT_FUNC_VERSION " and will be skipped.\n", gp_gpo->gpo_guid);
        state->gpo_index++;
        ret = ad_gpo_get_gpo_attrs_step(req);
        goto done;
    } else if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "sysdb_attrs_get_int32_t failed: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto done;
    }

    DEBUG(SSSDBG_TRACE_ALL, "gpo_func_version: %d\n",
                            gp_gpo->gpo_func_version);

    /* retrieve AD_AT_FLAGS */
    ret = sysdb_attrs_get_int32_t(result, AD_AT_FLAGS,
                                  &gp_gpo->gpo_flags);
    if (ret == ENOENT) {
        ret = ad_gpo_missing_or_unreadable_attr(state, req);
        goto done;
    } else if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE,
              "sysdb_attrs_get_int32_t failed: [%d](%s)\n",
              ret, sss_strerror(ret));
        goto done;
    }

    DEBUG(SSSDBG_TRACE_ALL, "gpo_flags: %d\n", gp_gpo->gpo_flags);

    /* retrieve AD_AT_NT_SEC_DESC */
    ret = sysdb_attrs_get_el(result, AD_AT_NT_SEC_DESC, &el);
    if (ret != EOK && ret != ENOENT) {
        DEBUG(SSSDBG_OP_FAILURE, "sysdb_attrs_get_el() failed\n");
        goto done;
    }
    if ((ret == ENOENT) || (el->num_values == 0)) {
        DEBUG(SSSDBG_OP_FAILURE,
              "nt_sec_desc attribute not found or has no value\n");
        ret = ad_gpo_missing_or_unreadable_attr(state, req);
        goto done;
    }

    ret = ad_gpo_parse_sd(gp_gpo, el[0].values[0].data, el[0].values[0].length,
                          &gp_gpo->gpo_sd);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "ad_gpo_parse_sd() failed\n");
        goto done;
    }

    /* retrieve AD_AT_MACHINE_EXT_NAMES */
    ret = sysdb_attrs_get_el(result, AD_AT_MACHINE_EXT_NAMES, &el);
    if (ret != EOK && ret != ENOENT) {
        DEBUG(SSSDBG_OP_FAILURE, "sysdb_attrs_get_el() failed\n");
        goto done;
    }

    if ((ret == ENOENT) || (el->num_values == 0)
            || machine_ext_names_is_blank((char *) el[0].values[0].data)) {
        /*
         * if gpo has no machine_ext_names (which is perfectly valid: it could
         * have only user_ext_names, for example), we continue to next gpo
         */
        DEBUG(SSSDBG_TRACE_ALL,
              "machine_ext_names attribute not found or has no value\n");
        state->gpo_index++;
    } else {
        raw_machine_ext_names = el[0].values[0].data;

        ret = ad_gpo_parse_machine_ext_names(gp_gpo,
                                             (char *)raw_machine_ext_names,
                                             &gp_gpo->gpo_cse_guids,
                                             &gp_gpo->num_gpo_cse_guids);
        if (ret != EOK) {
            DEBUG(SSSDBG_OP_FAILURE,
                  "ad_gpo_parse_machine_ext_names() failed\n");
            goto done;
        }

        state->gpo_index++;
    }

    ret = ad_gpo_get_gpo_attrs_step(req);

 done:

    return ret;
}

int
ad_gpo_process_gpo_recv(struct tevent_req *req,
                        TALLOC_CTX *mem_ctx,
                        struct gp_gpo ***candidate_gpos,
                        int *num_candidate_gpos)
{
    struct ad_gpo_process_gpo_state *state =
        tevent_req_data(req, struct ad_gpo_process_gpo_state);

    TEVENT_REQ_RETURN_ON_ERROR(req);

    *candidate_gpos = talloc_steal(mem_ctx, state->candidate_gpos);
    *num_candidate_gpos = state->num_candidate_gpos;
    return EOK;
}

/* == ad_gpo_process_cse_send/recv helpers ================================= */
static errno_t
create_cse_send_buffer(TALLOC_CTX *mem_ctx,
                       const char *smb_server,
                       const char *smb_share,
                       const char *smb_path,
                       const char *smb_cse_suffix,
                       int cached_gpt_version,
                       struct io_buffer **io_buf)
{
    struct io_buffer *buf;
    size_t rp;
    int smb_server_length;
    int smb_share_length;
    int smb_path_length;
    int smb_cse_suffix_length;

    smb_server_length = strlen(smb_server);
    smb_share_length = strlen(smb_share);
    smb_path_length = strlen(smb_path);
    smb_cse_suffix_length = strlen(smb_cse_suffix);

    buf = talloc(mem_ctx, struct io_buffer);
    if (buf == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc failed.\n");
        return ENOMEM;
    }

    buf->size = 5 * sizeof(uint32_t);
    buf->size += smb_server_length + smb_share_length + smb_path_length +
        smb_cse_suffix_length;

    DEBUG(SSSDBG_TRACE_ALL, "buffer size: %zu\n", buf->size);

    buf->data = talloc_size(buf, buf->size);
    if (buf->data == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_size failed.\n");
        talloc_free(buf);
        return ENOMEM;
    }

    rp = 0;
    /* cached_gpt_version */
    SAFEALIGN_SET_UINT32(&buf->data[rp], cached_gpt_version, &rp);

    /* smb_server */
    SAFEALIGN_SET_UINT32(&buf->data[rp], smb_server_length, &rp);
    safealign_memcpy(&buf->data[rp], smb_server, smb_server_length, &rp);

    /* smb_share */
    SAFEALIGN_SET_UINT32(&buf->data[rp], smb_share_length, &rp);
    safealign_memcpy(&buf->data[rp], smb_share, smb_share_length, &rp);

    /* smb_path */
    SAFEALIGN_SET_UINT32(&buf->data[rp], smb_path_length, &rp);
    safealign_memcpy(&buf->data[rp], smb_path, smb_path_length, &rp);

    /* smb_cse_suffix */
    SAFEALIGN_SET_UINT32(&buf->data[rp], smb_cse_suffix_length, &rp);
    safealign_memcpy(&buf->data[rp], smb_cse_suffix, smb_cse_suffix_length, &rp);

    *io_buf = buf;
    return EOK;
}

static errno_t
ad_gpo_parse_gpo_child_response(uint8_t *buf,
                                ssize_t size,
                                uint32_t *_sysvol_gpt_version,
                                uint32_t *_result)
{

    int ret;
    size_t p = 0;
    uint32_t sysvol_gpt_version;
    uint32_t result;

    /* sysvol_gpt_version */
    SAFEALIGN_COPY_UINT32_CHECK(&sysvol_gpt_version, buf + p, size, &p);

    /* operation result code */
    SAFEALIGN_COPY_UINT32_CHECK(&result, buf + p, size, &p);

    *_sysvol_gpt_version = sysvol_gpt_version;
    *_result = result;

    ret = EOK;
    return ret;
}

/* == ad_gpo_process_cse_send/recv implementation ========================== */

struct ad_gpo_process_cse_state {
    struct tevent_context *ev;
    struct sss_domain_info *domain;
    int gpo_timeout_option;
    const char *gpo_dpname;
    const char *gpo_guid;
    const char *smb_path;
    const char *smb_cse_suffix;
    const char *gpo_cache_path;
    pid_t child_pid;
    uint8_t *buf;
    ssize_t len;
    struct child_io_fds *io;
};

static errno_t gpo_fork_child(struct tevent_req *req);
static void gpo_cse_step(struct tevent_req *subreq);
static void gpo_cse_done(struct tevent_req *subreq);

/*
 * This cse-specific function (GP_EXT_GUID_SECURITY) sends the input smb uri
 * components and cached_gpt_version to the gpo child, which, in turn,
 * will download the GPT.INI file and policy files (as needed) and store
 * them in the GPO_CACHE directory. Note that if the send_to_child input is
 * false, this function simply completes the request.
 */
struct tevent_req *
ad_gpo_process_cse_send(TALLOC_CTX *mem_ctx,
                        struct tevent_context *ev,
                        bool send_to_child,
                        struct sss_domain_info *domain,
                        const char *gpo_guid,
                        const char *gpo_dpname,
                        const char *smb_server,
                        const char *smb_share,
                        const char *smb_path,
                        const char *smb_cse_suffix,
                        int cached_gpt_version,
                        int gpo_timeout_option)
{
    struct tevent_req *req;
    struct tevent_req *subreq;
    struct ad_gpo_process_cse_state *state;
    struct io_buffer *buf = NULL;
    errno_t ret;

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

    if (!send_to_child) {
        /*
         * if we don't need to talk to child (b/c cache timeout is still valid),
         * we simply complete the request
         */
        ret = EOK;
        goto immediately;
    }

    state->ev = ev;
    state->buf = NULL;
    state->len = 0;
    state->domain = domain;
    state->gpo_timeout_option = gpo_timeout_option;
    state->gpo_guid = gpo_guid;
    state->gpo_dpname = gpo_dpname;
    state->smb_path = smb_path;
    state->smb_cse_suffix = smb_cse_suffix;
    state->io = talloc(state, struct child_io_fds);
    if (state->io == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc failed.\n");
        ret = ENOMEM;
        goto immediately;
    }

    state->gpo_cache_path =
        talloc_asprintf(state, "%s%s", GPO_CACHE_PATH, state->smb_path);
    if (state->gpo_cache_path == NULL) {
        ret = ENOMEM;
        goto immediately;
    }

    state->io->write_to_child_fd = -1;
    state->io->read_from_child_fd = -1;
    talloc_set_destructor((void *) state->io, child_io_destructor);

    /* prepare the data to pass to child */
    ret = create_cse_send_buffer(state, smb_server, smb_share, smb_path,
                                 smb_cse_suffix, cached_gpt_version, &buf);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "create_cse_send_buffer failed.\n");
        goto immediately;
    }

    ret = gpo_fork_child(req);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "gpo_fork_child failed.\n");
        goto immediately;
    }

    subreq = write_pipe_send(state, ev, buf->data, buf->size,
                             state->io->write_to_child_fd);
    if (subreq == NULL) {
        ret = ENOMEM;
        goto immediately;
    }
    tevent_req_set_callback(subreq, gpo_cse_step, req);

    return req;

immediately:

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

    return req;
}

static void gpo_cse_step(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_gpo_process_cse_state *state;
    int ret;

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

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

    PIPE_FD_CLOSE(state->io->write_to_child_fd);

    subreq = read_pipe_send(state, state->ev, state->io->read_from_child_fd);

    if (subreq == NULL) {
        tevent_req_error(req, ENOMEM);
        return;
    }
    tevent_req_set_callback(subreq, gpo_cse_done, req);
}

static void gpo_cse_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_gpo_process_cse_state *state;
    uint32_t sysvol_gpt_version = -1;
    uint32_t child_result;
    time_t now;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct ad_gpo_process_cse_state);
    int ret;

    ret = read_pipe_recv(subreq, state, &state->buf, &state->len);
    talloc_zfree(subreq);
    if (ret != EOK) {
        tevent_req_error(req, ret);
        return;
    }

    PIPE_FD_CLOSE(state->io->read_from_child_fd);

    ret = ad_gpo_parse_gpo_child_response(state->buf, state->len,
                                          &sysvol_gpt_version, &child_result);
    if (ret != EOK) {
        if (ret == EINVAL) {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "ad_gpo_parse_gpo_child_response failed: [%d][%s]. "
                  "Broken GPO data received from AD. Check AD child logs for "
                  "more information.\n",
                  ret, sss_strerror(ret));
        } else {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "ad_gpo_parse_gpo_child_response failed: [%d][%s]\n",
                  ret, sss_strerror(ret));
        }

        tevent_req_error(req, ret);
        return;
    } else if (child_result != 0){
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Error in gpo_child: [%d][%s]\n",
              child_result, strerror(child_result));
        tevent_req_error(req, child_result);
        return;
    }

    now = time(NULL);
    DEBUG(SSSDBG_TRACE_FUNC, "sysvol_gpt_version: %d\n", sysvol_gpt_version);
    ret = sysdb_gpo_store_gpo(state->domain, state->gpo_dpname, state->gpo_guid,
                              state->gpo_cache_path, sysvol_gpt_version,
                              state->gpo_timeout_option, now);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "Unable to store gpo cache entry: [%d](%s}\n",
              ret, sss_strerror(ret));
        tevent_req_error(req, ret);
        return;
    }

    tevent_req_done(req);
    return;
}

int ad_gpo_process_cse_recv(struct tevent_req *req)
{
    TEVENT_REQ_RETURN_ON_ERROR(req);
    return EOK;
}

static errno_t
gpo_fork_child(struct tevent_req *req)
{
    int pipefd_to_child[2] = PIPE_INIT;
    int pipefd_from_child[2] = PIPE_INIT;
    pid_t pid;
    errno_t ret;
    const char **extra_args;
    int c = 0;
    struct ad_gpo_process_cse_state *state;

    state = tevent_req_data(req, struct ad_gpo_process_cse_state);

    extra_args = talloc_array(state, const char *, 2);

    extra_args[c] = talloc_asprintf(extra_args, "--chain-id=%lu",
                                    sss_chain_id_get());
    if (extra_args[c] == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "talloc_asprintf failed.\n");
        ret = ENOMEM;
        goto fail;
    }
    c++;

    extra_args[c] = NULL;

    ret = pipe(pipefd_from_child);
    if (ret == -1) {
        ret = errno;
        DEBUG(SSSDBG_CRIT_FAILURE,
              "pipe (from) failed [%d][%s].\n", errno, strerror(errno));
        goto fail;
    }
    ret = pipe(pipefd_to_child);
    if (ret == -1) {
        ret = errno;
        DEBUG(SSSDBG_CRIT_FAILURE,
              "pipe (to) failed [%d][%s].\n", errno, strerror(errno));
        goto fail;
    }

    pid = fork();

    if (pid == 0) { /* child */
        exec_child_ex(state,
                      pipefd_to_child, pipefd_from_child,
                      GPO_CHILD, GPO_CHILD_LOG_FILE, extra_args, false,
                      STDIN_FILENO, AD_GPO_CHILD_OUT_FILENO);

        /* We should never get here */
        DEBUG(SSSDBG_CRIT_FAILURE, "BUG: Could not exec gpo_child:\n");
    } else if (pid > 0) { /* parent */
        state->child_pid = pid;
        state->io->read_from_child_fd = pipefd_from_child[0];
        PIPE_FD_CLOSE(pipefd_from_child[1]);
        state->io->write_to_child_fd = pipefd_to_child[1];
        PIPE_FD_CLOSE(pipefd_to_child[0]);
        sss_fd_nonblocking(state->io->read_from_child_fd);
        sss_fd_nonblocking(state->io->write_to_child_fd);

        ret = child_handler_setup(state->ev, pid, NULL, NULL, NULL);
        if (ret != EOK) {
            DEBUG(SSSDBG_CRIT_FAILURE,
                  "Could not set up child signal handler\n");
            goto fail;
        }
    } else { /* error */
        ret = errno;
        DEBUG(SSSDBG_CRIT_FAILURE,
              "fork failed [%d][%s].\n", errno, strerror(errno));
        goto fail;
    }

    return EOK;

fail:
    PIPE_CLOSE(pipefd_from_child);
    PIPE_CLOSE(pipefd_to_child);
    return ret;
}

struct ad_gpo_get_sd_referral_state {
    struct tevent_context *ev;
    struct ad_access_ctx *access_ctx;
    struct sdap_options *opts;
    struct sss_domain_info *host_domain;
    struct sss_domain_info *ref_domain;
    struct sdap_id_conn_ctx *conn;
    struct sdap_id_op *ref_op;
    int timeout;
    char *gpo_dn;
    char *smb_host;


    struct sysdb_attrs *reply;
};

static void
ad_gpo_get_sd_referral_conn_done(struct tevent_req *subreq);

static struct tevent_req *
ad_gpo_get_sd_referral_send(TALLOC_CTX *mem_ctx,
                            struct tevent_context *ev,
                            struct ad_access_ctx *access_ctx,
                            struct sdap_options *opts,
                            const char *referral,
                            struct sss_domain_info *host_domain,
                            int timeout)
{
    errno_t ret;
    struct tevent_req *req;
    struct ad_gpo_get_sd_referral_state *state;
    struct tevent_req *subreq;
    LDAPURLDesc *lud = NULL;

    req = tevent_req_create(mem_ctx, &state,
                            struct ad_gpo_get_sd_referral_state);
    if (!req) return NULL;

    state->ev = ev;
    state->access_ctx = access_ctx;
    state->opts = opts;
    state->host_domain = host_domain;
    state->timeout = timeout;

    /* Parse the URL for the domain */
    ret = ldap_url_parse(referral, &lud);
    if (ret != LDAP_SUCCESS) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Failed to parse referral URI (%s)!\n", referral);
        ret = EINVAL;
        goto done;
    }

    state->gpo_dn = talloc_strdup(state, lud->lud_dn);
    if (!state->gpo_dn) {
        DEBUG(SSSDBG_OP_FAILURE,
              "Could not copy referral DN (%s)!\n", lud->lud_dn);
        ldap_free_urldesc(lud);
        ret = ENOMEM;
        goto done;
    }

    /* Active Directory returns the domain name as the hostname
     * in these referrals, so we can use that to look up the
     * necessary connection.
     */
    state->ref_domain = find_domain_by_name(state->host_domain,
                                            lud->lud_host, true);
    if (!state->ref_domain) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Could not find domain matching [%s]\n",
              lud->lud_host);
        ldap_free_urldesc(lud);
        ret = EIO;
        goto done;
    }

    ldap_free_urldesc(lud);
    lud = NULL;

    state->conn = ad_get_dom_ldap_conn(state->access_ctx->ad_id_ctx,
                                       state->ref_domain);
    if (!state->conn) {
        DEBUG(SSSDBG_OP_FAILURE,
              "No connection for %s\n", state->ref_domain->name);
        ret = EINVAL;
        goto done;
    }

    /* Start an ID operation for the referral */
    state->ref_op = sdap_id_op_create(state, state->conn->conn_cache);
    if (!state->ref_op) {
        DEBUG(SSSDBG_OP_FAILURE, "sdap_id_op_create failed.\n");
        ret = ENOMEM;
        goto done;
    }

    /* Establish the sdap_id_op connection */
    subreq = sdap_id_op_connect_send(state->ref_op, state, &ret);
    if (subreq == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "sdap_id_op_connect_send failed: %d(%s).\n",
                                  ret, sss_strerror(ret));
        goto done;
    }
    tevent_req_set_callback(subreq, ad_gpo_get_sd_referral_conn_done, req);

done:

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

static void
ad_gpo_get_sd_referral_search_done(struct tevent_req *subreq);

static void
ad_gpo_get_sd_referral_conn_done(struct tevent_req *subreq)
{
    errno_t ret;
    int dp_error;
    const char *attrs[] = AD_GPO_ATTRS;
    LDAPURLDesc *lud = NULL;

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

    ret = sdap_id_op_connect_recv(subreq, &dp_error);
    talloc_zfree(subreq);
    if (ret != EOK) {
        if (dp_error == DP_ERR_OFFLINE) {
            DEBUG(SSSDBG_TRACE_FUNC,
                  "Backend is marked offline, retry later!\n");
            tevent_req_done(req);
        } else {
            DEBUG(SSSDBG_MINOR_FAILURE,
                  "Cross-realm GPO processing failed to connect to " \
                   "referred LDAP server: (%d)[%s]\n",
                   ret, sss_strerror(ret));
            tevent_req_error(req, ret);
        }
        return;
    }

    /*
     * Save the hostname we have connected to. We'll need this later for
     * performing the smb connection. The GPO referral URL can't be directly used
     * because the user might have forced the DC to use (ad_server option)
     */
    ret = ldap_url_parse(state->conn->service->uri, &lud);
    if (ret != LDAP_SUCCESS) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Failed to parse service URI (%s)!\n",
              state->conn->service->uri);
        tevent_req_error(req, EINVAL);
        return;
    }

    state->smb_host = talloc_strdup(state, lud->lud_host);
    ldap_free_urldesc(lud);
    lud = NULL;
    if (tevent_req_nomem(state->smb_host, req)) {
        return;
    }

    /* Request the referred GPO data */
    subreq = sdap_sd_search_send(state, state->ev, state->opts,
                                 sdap_id_op_handle(state->ref_op),
                                 state->gpo_dn,
                                 SECINFO_DACL,
                                 attrs,
                                 state->timeout);
    if (subreq == NULL) {
        DEBUG(SSSDBG_OP_FAILURE, "sdap_sd_search_send failed.\n");
        tevent_req_error(req, ENOMEM);
        return;
    }
    tevent_req_set_callback(subreq, ad_gpo_get_sd_referral_search_done, req);

}

static void
ad_gpo_get_sd_referral_search_done(struct tevent_req *subreq)
{
    errno_t ret;
    int dp_error;
    size_t num_results, num_refs;
    struct sysdb_attrs **results = NULL;
    char **refs;
    struct tevent_req *req =
            tevent_req_callback_data(subreq, struct tevent_req);
    struct ad_gpo_get_sd_referral_state *state =
            tevent_req_data(req, struct ad_gpo_get_sd_referral_state);

    ret = sdap_sd_search_recv(subreq, NULL,
                              &num_results, &results,
                              &num_refs, &refs);
    talloc_zfree(subreq);
    if (ret != EOK) {
        ret = sdap_id_op_done(state->ref_op, ret, &dp_error);

        DEBUG(SSSDBG_OP_FAILURE,
              "Unable to get GPO attributes: [%d](%s)\n",
              ret, sss_strerror(ret));
        ret = ENOENT;
        goto done;

    }

    if ((num_results < 1) || (results == NULL)) {
        /* TODO:
         * It's strictly possible for the referral search to return
         * another referral value here, but it shouldn't actually
         * happen with Active Directory. Properly handling (and
         * limiting) the referral chain would be fairly complex, so
         * we will do it later if it ever becomes necessary.
         */
        DEBUG(SSSDBG_OP_FAILURE,
              "No attrs found for referred GPO [%s].\n", state->gpo_dn);
        ret = ENOENT;
        goto done;

    } else if (num_results > 1) {
        DEBUG(SSSDBG_OP_FAILURE, "Received multiple replies\n");
        ret = ERR_INTERNAL;
        goto done;
    }

    state->reply = talloc_steal(state, results[0]);

done:
   talloc_free(results);

   if (ret == EOK) {
       tevent_req_done(req);
   } else if (ret != EAGAIN) {
       tevent_req_error(req, ret);
   }
}

errno_t
ad_gpo_get_sd_referral_recv(struct tevent_req *req,
                            TALLOC_CTX *mem_ctx,
                            char **_smb_host,
                            struct sysdb_attrs **_reply)
{
    struct ad_gpo_get_sd_referral_state *state =
                tevent_req_data(req, struct ad_gpo_get_sd_referral_state);

    TEVENT_REQ_RETURN_ON_ERROR(req);

    *_smb_host = talloc_steal(mem_ctx, state->smb_host);
    *_reply = talloc_steal(mem_ctx, state->reply);

    return EOK;
}
