/*
    SSSD

    Authors:
        Stephen Gallagher <sgallagh@redhat.com>

    Copyright (C) 2012 Red Hat

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

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

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

#include <security/pam_modules.h>
#include <syslog.h>

#include "src/util/util.h"
#include "src/providers/data_provider.h"
#include "src/providers/backend.h"
#include "src/providers/ad/ad_access.h"
#include "providers/ad/ad_gpo.h"
#include "src/providers/ad/ad_common.h"
#include "src/providers/ldap/sdap_access.h"

/*
 * More advanced format can be used to restrict the filter to a specific
 * domain or a specific forest. This format is KEYWORD:NAME:FILTER
 *
 *  KEYWORD can be one of DOM or FOREST
 *      KEYWORD can be missing
 *  NAME is a label.
 *      - if KEYWORD equals DOM or missing completely, the filter is applied
 *        for users from domain named NAME only
 *      - if KEYWORD equals FOREST, the filter is applied on users from
 *        forest named NAME only
 *  examples of valid filters are:
 *      apply filter on domain called dom1 only:
 *          dom1:(memberOf=cn=admins,ou=groups,dc=dom1,dc=com)
 *      apply filter on domain called dom2 only:
 *          DOM:dom2:(memberOf=cn=admins,ou=groups,dc=dom2,dc=com)
 *      apply filter on forest called EXAMPLE.COM only:
 *          FOREST:EXAMPLE.COM:(memberOf=cn=admins,ou=groups,dc=example,dc=com)
 *
 * If any of the extended formats are used, the filter MUST be enclosed
 * already.
 */

/* From least specific */
#define AD_FILTER_GENERIC 0x01
#define AD_FILTER_FOREST  0x02
#define AD_FILTER_DOMAIN  0x04

#define KW_FOREST "FOREST"
#define KW_DOMAIN "DOM"

/* parse filter in the format domain_name:filter */
static errno_t
parse_sub_filter(TALLOC_CTX *mem_ctx, const char *full_filter,
                 char **filter, char **sub_name, int *flags,
                 const int flagconst)
{
    char *specdelim;

    specdelim = strchr(full_filter, ':');
    if (specdelim == NULL) return EINVAL;

    /* Make sure the filter is already enclosed in brackets */
    if (*(specdelim+1) != '(') return EINVAL;

    *sub_name = talloc_strndup(mem_ctx, full_filter, specdelim - full_filter);
    *filter = talloc_strdup(mem_ctx, specdelim+1);
    if (*sub_name == NULL || *filter == NULL) return ENOMEM;

    *flags = flagconst;
    return EOK;
}

static inline errno_t
parse_dom_filter(TALLOC_CTX *mem_ctx, const char *dom_filter,
                 char **filter, char **domname, int *flags)
{
    return parse_sub_filter(mem_ctx, dom_filter, filter, domname,
                            flags, AD_FILTER_DOMAIN);
}

static inline errno_t
parse_forest_filter(TALLOC_CTX *mem_ctx, const char *forest_filter,
                    char **filter, char **forest_name, int *flags)
{
    return parse_sub_filter(mem_ctx, forest_filter, filter, forest_name,
                            flags, AD_FILTER_FOREST);
}


static errno_t
parse_filter(TALLOC_CTX *mem_ctx, const char *full_filter,
             char **filter, char **spec, int *flags)
{
    char *kwdelim, *specdelim;

    if (filter == NULL || spec == NULL || flags == NULL) return EINVAL;

    kwdelim = strchr(full_filter, ':');
    if (kwdelim != NULL) {
        specdelim = strchr(kwdelim+1, ':');

        if (specdelim == NULL) {
            /* There is a single keyword. Treat it as a domain name */
            return parse_dom_filter(mem_ctx, full_filter, filter, spec, flags);
        } else if (strncmp(full_filter, "DOM", kwdelim-full_filter) == 0) {
            /* The format must be DOM:domain_name:filter */
            if (specdelim && specdelim-kwdelim <= 1) {
                /* Check if there is some domain_name */
                return EINVAL;
            }

            return parse_dom_filter(mem_ctx, kwdelim + 1, filter, spec, flags);
        } else if (strncmp(full_filter, "FOREST", kwdelim-full_filter) == 0) {
            /* The format must be FOREST:forest_name:filter */
            if (specdelim && specdelim-kwdelim <= 1) {
                /* Check if there is some domain_name */
                return EINVAL;
            }

            return parse_forest_filter(mem_ctx, kwdelim + 1,
                                       filter, spec, flags);
        }

        /* Malformed option */
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Keyword in filter [%s] did not match expected format\n",
               full_filter);
        return EINVAL;
    }

    /* No keyword. Easy. */
    *filter = talloc_strdup(mem_ctx, full_filter);
    if (*filter == NULL) return ENOMEM;

    *spec = NULL;
    *flags = AD_FILTER_GENERIC;
    return EOK;
}

static errno_t
ad_parse_access_filter(TALLOC_CTX *mem_ctx,
                       struct sss_domain_info *dom,
                       const char *filter_list,
                       char **_filter)
{
    char **filters;
    int nfilters;
    errno_t ret;
    char *best_match;
    int best_flags;
    char *filter;
    char *spec;
    int flags;
    TALLOC_CTX *tmp_ctx;
    int i = 0;

    if (_filter == NULL) return EINVAL;

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

    if (filter_list == NULL) {
        *_filter = NULL;
        ret = EOK;
        goto done;
    }

    ret = split_on_separator(tmp_ctx, filter_list, '?', true, true,
                             &filters, &nfilters);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE,
              "Cannot parse the list of ad_access_filters\n");
        goto done;
    }

    best_match = NULL;
    best_flags = 0;
    for (i=0; i < nfilters; i++) {
        ret = parse_filter(tmp_ctx, filters[i], &filter, &spec, &flags);
        if (ret != EOK) {
            /* Skip the faulty filter. At worst, the user won't be
             * allowed access */
            DEBUG(SSSDBG_MINOR_FAILURE, "Access filter [%s] could not be "
                  "parsed, skipping\n", filters[i]);
            continue;
        }

        if (flags & AD_FILTER_DOMAIN && strcasecmp(spec, dom->name) != 0) {
            /* If the filter specifies a domain, it must match the
             * domain the user comes from
             */
            continue;
        }

        if (flags & AD_FILTER_FOREST && strcasecmp(spec, dom->forest) != 0) {
            /* If the filter specifies a forest, it must match the
             * forest the user comes from
             */
            continue;
        }

        if (flags > best_flags) {
            best_flags = flags;
            best_match = filter;
        }
    }

    ret = EOK;
    /* Make sure the result is enclosed in brackets */
    *_filter = sdap_get_access_filter(mem_ctx, best_match);
done:
    talloc_free(tmp_ctx);
    return ret;
}

struct ad_access_state {
    struct tevent_context *ev;
    struct ad_access_ctx *ctx;
    struct pam_data *pd;
    struct be_ctx *be_ctx;
    struct sss_domain_info *domain;

    char *filter;
    struct sdap_id_conn_ctx **clist;
    int cindex;
};

static errno_t
ad_sdap_access_step(struct tevent_req *req, struct sdap_id_conn_ctx *conn);
static void
ad_sdap_access_done(struct tevent_req *req);

static struct tevent_req *
ad_access_send(TALLOC_CTX *mem_ctx,
               struct tevent_context *ev,
               struct be_ctx *be_ctx,
               struct sss_domain_info *domain,
               struct ad_access_ctx *ctx,
               struct pam_data *pd)
{
    struct tevent_req *req;
    struct ad_access_state *state;
    errno_t ret;

    req = tevent_req_create(mem_ctx, &state, struct ad_access_state);
    if (req == NULL) {
        return NULL;
    }

    state->ev = ev;
    state->ctx = ctx;
    state->pd = pd;
    state->be_ctx = be_ctx;
    state->domain = domain;

    ret = ad_parse_access_filter(state, domain, ctx->sdap_access_ctx->filter,
                                 &state->filter);
    if (ret != EOK) {
        DEBUG(SSSDBG_CRIT_FAILURE, "Could not determine the best filter\n");
        ret = ERR_ACCESS_DENIED;
        goto done;
    }

    state->clist = ad_gc_conn_list(state, ctx->ad_id_ctx, domain);
    if (state->clist == NULL) {
        ret = ENOMEM;
        goto done;
    }

    ret = ad_sdap_access_step(req, state->clist[state->cindex]);
    if (ret != EOK) {
        goto done;
    }

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

        tevent_req_post(req, ev);
    }
    return req;
}

static errno_t
ad_sdap_access_step(struct tevent_req *req, struct sdap_id_conn_ctx *conn)
{
    struct tevent_req *subreq;
    struct ad_access_state *state;
    struct sdap_access_ctx *req_ctx;

    state = tevent_req_data(req, struct ad_access_state);

    req_ctx = talloc(state, struct sdap_access_ctx);
    if (req_ctx == NULL) {
        return ENOMEM;
    }
    req_ctx->id_ctx = state->ctx->sdap_access_ctx->id_ctx;
    req_ctx->filter = state->filter;
    memcpy(&req_ctx->access_rule,
           state->ctx->sdap_access_ctx->access_rule,
           sizeof(int) * LDAP_ACCESS_LAST);

    subreq = sdap_access_send(state, state->ev, state->be_ctx,
                              state->domain, req_ctx,
                              conn, state->pd);
    if (req == NULL) {
        talloc_free(req_ctx);
        return ENOMEM;
    }
    tevent_req_set_callback(subreq, ad_sdap_access_done, req);
    return EOK;
}

static void
ad_gpo_access_done(struct tevent_req *subreq);

static void
ad_sdap_access_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_access_state *state;
    errno_t ret;

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

    ret = sdap_access_recv(subreq);
    talloc_zfree(subreq);

    if (ret != EOK) {
        switch (ret) {
        case ERR_ACCOUNT_EXPIRED:
            tevent_req_error(req, ret);
            return;

        case ERR_ACCESS_DENIED:
            /* Retry on ACCESS_DENIED, too, to make sure that we don't
             * miss out any attributes not present in GC
             * FIXME - this is slow. We should retry only if GC failed
             * and LDAP succeeded after the first ACCESS_DENIED
             */
            break;

        default:
            break;
        }

        /* If possible, retry with LDAP */
        state->cindex++;
        if (state->clist[state->cindex] == NULL) {
            DEBUG(SSSDBG_OP_FAILURE,
                  "Error retrieving access check result: %s\n",
                  sss_strerror(ret));
            tevent_req_error(req, ret);
            return;
        }

        ret = ad_sdap_access_step(req, state->clist[state->cindex]);
        if (ret != EOK) {
            tevent_req_error(req, ret);
            return;
        }

        /* Another check in progress */

        return;
    }

    switch (state->ctx->gpo_access_control_mode) {
    case GPO_ACCESS_CONTROL_DISABLED:
        /* do not evaluate gpos; mark request done */
        tevent_req_done(req);
        return;
    case GPO_ACCESS_CONTROL_PERMISSIVE:
    case GPO_ACCESS_CONTROL_ENFORCING:
        /* continue on to evaluate gpos */
        break;
    default:
        tevent_req_error(req, EINVAL);
        return;
    }

    subreq = ad_gpo_access_send(state,
                                state->be_ctx->ev,
                                state->domain,
                                state->ctx,
                                state->pd->user,
                                state->pd->service);

    if (!subreq) {
        tevent_req_error(req, ENOMEM);
        return;
    }

    tevent_req_set_callback(subreq, ad_gpo_access_done, req);

}

static void
ad_gpo_access_done(struct tevent_req *subreq)
{
    struct tevent_req *req;
    struct ad_access_state *state;
    errno_t ret;
    enum gpo_access_control_mode mode;

    req = tevent_req_callback_data(subreq, struct tevent_req);
    state = tevent_req_data(req, struct ad_access_state);
    mode = state->ctx->gpo_access_control_mode;

    ret = ad_gpo_access_recv(subreq);
    talloc_zfree(subreq);

    if (ret == EOK) {
        DEBUG(SSSDBG_TRACE_FUNC, "GPO-based access control successful.\n");
        tevent_req_done(req);
    } else {
        DEBUG(SSSDBG_OP_FAILURE, "GPO-based access control failed.\n");
        if (mode == GPO_ACCESS_CONTROL_ENFORCING) {
            tevent_req_error(req, ret);
        } else {
            DEBUG(SSSDBG_OP_FAILURE,
                  "Ignoring error: [%d](%s); GPO-based access control failed, "
                  "but GPO is not in enforcing mode.\n",
                  ret, sss_strerror(ret));
            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.");
            tevent_req_done(req);
        }
    }
}

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

    return EOK;
}

struct ad_pam_access_handler_state {
    struct pam_data *pd;
};

static void ad_pam_access_handler_done(struct tevent_req *subreq);

struct tevent_req *
ad_pam_access_handler_send(TALLOC_CTX *mem_ctx,
                           struct ad_access_ctx *access_ctx,
                           struct pam_data *pd,
                           struct dp_req_params *params)
{
    struct ad_pam_access_handler_state *state;
    struct tevent_req *subreq;
    struct tevent_req *req;

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

    state->pd = pd;

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

    tevent_req_set_callback(subreq, ad_pam_access_handler_done, req);

    return req;

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

    return req;
}

static void ad_pam_access_handler_done(struct tevent_req *subreq)
{
    struct ad_pam_access_handler_state *state;
    struct tevent_req *req;
    errno_t ret;

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

    ret = ad_access_recv(subreq);
    talloc_free(subreq);
    switch (ret) {
    case EOK:
        state->pd->pam_status = PAM_SUCCESS;
        break;
    case ERR_ACCESS_DENIED:
        state->pd->pam_status = PAM_PERM_DENIED;
        break;
    case ERR_ACCOUNT_EXPIRED:
        state->pd->pam_status = PAM_ACCT_EXPIRED;
        break;
    default:
        state->pd->pam_status = PAM_SYSTEM_ERR;
        break;
    }

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

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

    state = tevent_req_data(req, struct ad_pam_access_handler_state);

    TEVENT_REQ_RETURN_ON_ERROR(req);

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

    return EOK;
}
