/*
    SSSD

    SELinux-related utility functions

    Authors:
        Jan Zeleny <jzeleny@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 "ipa_selinux_helpers.h"
#include "util/sss_utf8.h"
#include "db/sysdb_selinux.h"

static bool match_entity(struct ldb_message_element *values,
                         struct ldb_message_element *sought_values)
{
    int i, j;

    for (i = 0; i < values->num_values; i++) {
        for (j = 0; j < sought_values->num_values; j++) {
            if (values->values[i].length != sought_values->values[j].length) {
                continue;
            }

            if (strncasecmp((char *)values->values[i].data,
                            (char *)sought_values->values[j].data,
                            values->values[i].length) == 0)
                return true;
        }
    }

    return false;
}

bool sss_selinux_match(struct sysdb_attrs *usermap,
                       struct sysdb_attrs *user,
                       struct sysdb_attrs *host,
                       uint32_t *_priority)
{
    struct ldb_message_element *users_el = NULL;
    struct ldb_message_element *usercat = NULL;
    struct ldb_message_element *hosts_el = NULL;
    struct ldb_message_element *hostcat = NULL;
    struct ldb_message_element *dn;
    struct ldb_message_element *memberof;
    int i;
    uint32_t priority = 0;
    bool matched_name;
    bool matched_group;
    bool matched_category;
    errno_t ret;

    if (usermap == NULL) {
        DEBUG(SSSDBG_MINOR_FAILURE, "NULL given as usermap! Skipping ...\n");
        return false;
    }

    /* Search for user and host related elements */
    for (i = 0; i < usermap->num; i++) {
        if (!strcasecmp(usermap->a[i].name, SYSDB_ORIG_MEMBER_USER)) {
            users_el = &usermap->a[i];
        } else if (!strcasecmp(usermap->a[i].name, SYSDB_ORIG_MEMBER_HOST)) {
            hosts_el = &usermap->a[i];
        } else if (!strcasecmp(usermap->a[i].name, SYSDB_USER_CATEGORY)) {
            usercat = &usermap->a[i];
        } else if (!strcasecmp(usermap->a[i].name, SYSDB_HOST_CATEGORY)) {
            hostcat = &usermap->a[i];
        }
    }

    if (user) {
        ret = sysdb_attrs_get_el(user, SYSDB_ORIG_DN, &dn);
        if (ret != EOK) {
            DEBUG(SSSDBG_MINOR_FAILURE, "User does not have origDN\n");
            return false;
        }
        ret = sysdb_attrs_get_el(user, SYSDB_ORIG_MEMBEROF, &memberof);
        if (ret != EOK) {
            DEBUG(SSSDBG_TRACE_ALL,
                  "User does not have orig memberof, "
                   "therefore it can't match to any rule\n");
            return false;
        }

        /**
         * The rule won't match if user category != "all" and user map doesn't
         * contain neither user nor any of his groups in memberUser attribute
         */
        matched_category = false;
        if (usercat != NULL) {
            for (i = 0; i < usercat->num_values; i++) {
                if (strcasecmp((char *)usercat->values[i].data, "all") == 0) {
                    matched_category = true;
                    break;
                }
            }
        }

        if (!matched_category) {
            if (users_el == NULL) {
                DEBUG(SSSDBG_TRACE_ALL, "No users specified in the rule!\n");
                return false;
            } else {
                matched_name = match_entity(users_el, dn);
                matched_group = match_entity(users_el, memberof);
                if (matched_name) {
                    priority |= SELINUX_PRIORITY_USER_NAME;
                } else if (matched_group) {
                    priority |= SELINUX_PRIORITY_USER_GROUP;
                } else {
                    DEBUG(SSSDBG_TRACE_ALL, "User did not match\n");
                    return false;
                }
            }
        } else {
            priority |= SELINUX_PRIORITY_USER_CAT;
        }
    }

    if (host) {
        ret = sysdb_attrs_get_el(host, SYSDB_ORIG_DN, &dn);
        if (ret != EOK) {
            DEBUG(SSSDBG_MINOR_FAILURE, "Host does not have origDN\n");
            return false;
        }
        ret = sysdb_attrs_get_el(host, SYSDB_ORIG_MEMBEROF, &memberof);
        if (ret != EOK) {
            DEBUG(SSSDBG_TRACE_ALL,
                  "Host does not have orig memberof, "
                   "therefore it can't match to any rule\n");
            return false;
        }

        /**
         * The rule won't match if host category != "all" and user map doesn't
         * contain neither host nor any of its groups in memberHost attribute
         */
        matched_category = false;
        if (hostcat != NULL) {
            for (i = 0; i < hostcat->num_values; i++) {
                if (strcasecmp((char *)hostcat->values[i].data, "all") == 0) {
                    matched_category = true;
                    break;
                }
            }
        }
        if (!matched_category) {
            if (hosts_el == NULL) {
                DEBUG(SSSDBG_TRACE_ALL, "No users specified in the rule!\n");
                return false;
            } else {
                matched_name = match_entity(hosts_el, dn);
                matched_group = match_entity(hosts_el, memberof);
                if (matched_name) {
                    priority |= SELINUX_PRIORITY_HOST_NAME;
                } else if (matched_group) {
                    priority |= SELINUX_PRIORITY_HOST_GROUP;
                } else {
                    DEBUG(SSSDBG_TRACE_ALL, "Host did not match\n");
                    return false;
                }
            }
        } else {
            priority |= SELINUX_PRIORITY_HOST_CAT;
        }
    }

    if (_priority != NULL) {
        *_priority = priority;
    }

    return true;
}

errno_t sss_selinux_extract_user(TALLOC_CTX *mem_ctx,
                                 struct sss_domain_info *domain,
                                 const char *username,
                                 struct sysdb_attrs **_user_attrs)
{
    TALLOC_CTX *tmp_ctx;
    const char **attrs;
    struct sysdb_attrs *user_attrs;
    struct ldb_message *user_msg;

    errno_t ret;

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

    attrs = talloc_array(tmp_ctx, const char *, 3);
    if (attrs == NULL) {
        DEBUG(SSSDBG_CRIT_FAILURE, "talloc_array failed.\n");
        ret = ENOMEM;
        goto done;
    }
    attrs[0] = SYSDB_ORIG_DN;
    attrs[1] = SYSDB_ORIG_MEMBEROF;
    attrs[2] = NULL;

    ret = sysdb_search_user_by_name(tmp_ctx, domain, username, attrs,
                                    &user_msg);
    if (ret != EOK) {
        DEBUG(SSSDBG_OP_FAILURE, "sysdb_search_user_by_name failed.\n");
        goto done;
    }

    user_attrs = talloc_zero(tmp_ctx, struct sysdb_attrs);
    if (user_attrs == NULL) {
        ret = ENOMEM;
        goto done;
    }
    user_attrs->a = talloc_steal(user_attrs, user_msg->elements);
    user_attrs->num = user_msg->num_elements;

    *_user_attrs = talloc_steal(mem_ctx, user_attrs);
    ret = EOK;

done:
    talloc_free(tmp_ctx);
    return ret;
}

const char *sss_selinux_map_get_seuser(struct sysdb_attrs *usermap)
{
    int i;
    const uint8_t *name;
    const uint8_t *template = (const uint8_t *)SYSDB_SELINUX_USER;

    for (i = 0; i < usermap->num; i++) {
        name = (const uint8_t *)usermap->a[i].name;
        if (sss_utf8_case_eq(name, template) == 0) {
            return (const char *)usermap->a[i].values[0].data;
        }
    }

    return NULL;
}
