/*
 * Copyright (C) NGINX, Inc.
 * Copyright (C) Valentin V. Bartenev
 */

#include <nxt_main.h>
#include <nxt_conf.h>
#include "nxt_tests.h"


#define UIDMAP 1
#define GIDMAP 2


typedef struct {
    nxt_int_t         map_type;
    nxt_str_t         map_data;
    nxt_int_t         setid;
    nxt_credential_t  creds;
    nxt_uid_t         unit_euid;
    nxt_gid_t         unit_egid;
    nxt_int_t         result;
    nxt_str_t         errmsg;
} nxt_clone_creds_testcase_t;

typedef struct {
    nxt_clone_creds_testcase_t  *tc;
} nxt_clone_creds_ctx_t;


nxt_int_t nxt_clone_test_mappings(nxt_task_t *task, nxt_mp_t *mp,
    nxt_clone_creds_ctx_t *ctx, nxt_clone_creds_testcase_t *tc);
void nxt_cdecl nxt_clone_test_log_handler(nxt_uint_t level, nxt_log_t *log,
    const char *fmt, ...);
nxt_int_t nxt_clone_test_map_assert(nxt_task_t *task,
    nxt_clone_creds_testcase_t *tc, nxt_clone_credential_map_t *map);
static nxt_int_t nxt_clone_test_parse_map(nxt_task_t *task,
    nxt_str_t *map_str, nxt_clone_credential_map_t *map);


nxt_log_t *test_log;

static nxt_gid_t gids[] = {1000, 10000, 60000};

static nxt_clone_creds_testcase_t testcases[] = {
    {
        /*
         * Unprivileged unit
         *
         * if no uid mapping and app creds and unit creds are the same,
         * then we automatically add a map for the creds->uid.
         * Then, child process can safely setuid(creds->uid) in
         * the new namespace.
         */
        UIDMAP,
        nxt_string(""),
        0,
        {"nobody", 65534, 65534, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        UIDMAP,
        nxt_string(""),
        0,
        {"johndoe", 10000, 10000, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        UIDMAP,
        nxt_string("[{\"container\": 1000, \"host\": 1000, \"size\": 1}]"),
        0,
        {"johndoe", 1000, 1000, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        UIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 1000, \"size\": 1}]"),
        0,
        {"root", 0, 0, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        UIDMAP,
        nxt_string("[{\"container\": 65534, \"host\": 1000, \"size\": 1}]"),
        0,
        {"nobody", 65534, 0, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        UIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 1000, \"size\": 1},"
                   " {\"container\": 1000, \"host\": 2000, \"size\": 1}]"),
        0,
        {"root", 0, 0, 0, NULL},
        1000, 1000,
        NXT_ERROR,
        nxt_string("\"uidmap\" field has 2 entries but unprivileged unit has "
                   "a maximum of 1 map.")
    },
    {
        UIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 1000, \"size\": 1},"
                   " {\"container\": 1000, \"host\": 2000, \"size\": 1}]"),
        1, /* privileged */
        {"root", 0, 0, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        UIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 1000, \"size\": 1000},"
                   " {\"container\": 1000, \"host\": 2000, \"size\": 1000}]"),
        1, /* privileged */
        {"johndoe", 500, 0, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        UIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 1000, \"size\": 1000},"
                   " {\"container\": 1000, \"host\": 2000, \"size\": 1000}]"),
        1, /* privileged */
        {"johndoe", 1000, 0, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        UIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 1000, \"size\": 1000},"
                   " {\"container\": 1000, \"host\": 2000, \"size\": 1000}]"),
        1, /* privileged */
        {"johndoe", 1500, 0, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        UIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 1000, \"size\": 1000},"
                   " {\"container\": 1000, \"host\": 2000, \"size\": 1000}]"),
        1, /* privileged */
        {"johndoe", 1999, 0, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        UIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 1000, \"size\": 1000},"
                   " {\"container\": 1000, \"host\": 2000, \"size\": 1000}]"),
        1, /* privileged */
        {"johndoe", 2000, 0, 0, NULL},
        1000, 1000,
        NXT_ERROR,
        nxt_string("\"uidmap\" field has no \"container\" entry for user "
                   "\"johndoe\" (uid 2000)")
    },
    {
        /*
         * Unprivileged unit
         *
         * if no gid mapping and app creds and unit creds are the same,
         * then we automatically add a map for the creds->base_gid.
         * Then, child process can safely setgid(creds->base_gid) in
         * the new namespace.
         */
        GIDMAP,
        nxt_string("[]"),
        0,
        {"nobody", 65534, 65534, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        /*
         * Unprivileged unit
         *
         * Inside the new namespace, we can have any gid but it
         * should map to parent gid (in this case 1000) in parent
         * namespace.
         */
        GIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 1000, \"size\": 1}]"),
        0,
        {"root", 0, 0, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        GIDMAP,
        nxt_string("[{\"container\": 65534, \"host\": 1000, \"size\": 1}]"),
        0,
        {"nobody", 65534, 65534, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        /*
         * Unprivileged unit
         *
         * There's no mapping for "johndoe" (gid 1000) inside the namespace.
         */
        GIDMAP,
        nxt_string("[{\"container\": 65535, \"host\": 1000, \"size\": 1}]"),
        0,
        {"johndoe", 1000, 1000, 0, NULL},
        1000, 1000,
        NXT_ERROR,
        nxt_string("\"gidmap\" field has no \"container\" entry for "
                    "gid 1000.")
    },
    {
        GIDMAP,
        nxt_string("[{\"container\": 1000, \"host\": 1000, \"size\": 2}]"),
        0,
        {"johndoe", 1000, 1000, 0, NULL},
        1000, 1000,
        NXT_ERROR,
        nxt_string("\"gidmap\" field has an entry with \"size\": 2, but "
                    "for unprivileged unit it must be 1.")
    },
    {
        GIDMAP,
        nxt_string("[{\"container\": 1000, \"host\": 1001, \"size\": 1}]"),
        0,
        {"johndoe", 1000, 1000, 0, NULL},
        1000, 1000,
        NXT_ERROR,
        nxt_string("\"gidmap\" field has an entry for host gid 1001 but "
                    "unprivileged unit can only map itself (gid 1000) "
                    "into child namespaces.")
    },
    {
        GIDMAP,
        nxt_string("[{\"container\": 1000, \"host\": 1000, \"size\": 1}]"),
        0,
        {"johndoe", 1000, 1000, 3, gids},
        1000, 1000,
        NXT_ERROR,
        nxt_string("unprivileged unit disallow supplementary groups for "
                    "new namespace (user \"johndoe\" has 3 groups).")
    },

    /* privileged unit */

    /* not root with capabilities */
    {
        GIDMAP,
        nxt_string("[]"),
        1,
        {"johndoe", 1000, 1000, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        GIDMAP,
        nxt_string(""),
        1,
        {"johndoe", 1000, 1000, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        /* missing gid of {"user": "nobody"} */
        GIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 1000, \"size\": 1}]"),
        1,
        {"nobody", 65534, 65534, 0, NULL},
        1000, 1000,
        NXT_ERROR,
        nxt_string("\"gidmap\" field has no \"container\" entry for "
                    "gid 65534.")
    },
    {
        /* solves the previous by mapping 65534 gids */
        GIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 1000, \"size\": 65535}]"),
        1,
        {"nobody", 65534, 65534, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        /* solves by adding a separate mapping */
        GIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 1000, \"size\": 1},"
                   " {\"container\": 65534, \"host\": 1000, \"size\": 1}]"),
        1,
        {"nobody", 65534, 65534, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        /*
         * Map a big range
         */
        GIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 0, \"size\": 200000}]"),
        1,
        {"johndoe", 100000, 100000, 0, NULL},
        1000, 1000,
        NXT_OK,
        nxt_string("")
    },
    {
        /*
         * Validate if supplementary groups are mapped
         */
        GIDMAP,
        nxt_string("[]"),
        1,
        {"johndoe", 1000, 1000, 3, gids},
        1000, 1000,
        NXT_ERROR,
        nxt_string("\"gidmap\" field has no entries but user \"johndoe\" "
                   "has 3 suplementary groups."),
    },
    {
        GIDMAP,
        nxt_string("[{\"container\": 0, \"host\": 0, \"size\": 1}]"),
        1,
        {"johndoe", 1000, 1000, 3, gids},
        1000, 1000,
        NXT_ERROR,
        nxt_string("\"gidmap\" field has no \"container\" entry for "
                   "gid 1000."),
    },
    {
        GIDMAP,
        nxt_string("[{\"container\": 1000, \"host\": 0, \"size\": 1}]"),
        1,
        {"johndoe", 1000, 1000, 3, gids},
        1000, 1000,
        NXT_ERROR,
        nxt_string("\"gidmap\" field has missing suplementary gid mappings "
                   "(found 1 out of 3)."),
    },
    {
        GIDMAP,
        nxt_string("[{\"container\": 1000, \"host\": 0, \"size\": 1},"
                   " {\"container\": 10000, \"host\": 10000, \"size\": 1}]"),
        1,
        {"johndoe", 1000, 1000, 3, gids},
        1000, 1000,
        NXT_ERROR,
        nxt_string("\"gidmap\" field has missing suplementary gid mappings "
                   "(found 2 out of 3)."),
    },
    {
        /*
         * Fix all mappings
         */
        GIDMAP,
        nxt_string("[{\"container\": 1000, \"host\": 0, \"size\": 1},"
                   "{\"container\": 10000, \"host\": 10000, \"size\": 1},"
                   " {\"container\": 60000, \"host\": 60000, \"size\": 1}]"),
        1,
        {"johndoe", 1000, 1000, 3, gids},
        1000, 1000,
        NXT_OK,
        nxt_string(""),
    },
};


void nxt_cdecl
nxt_clone_test_log_handler(nxt_uint_t level, nxt_log_t *log,
    const char *fmt, ...)
{
    u_char                      *p, *end;
    va_list                     args;
    nxt_clone_creds_ctx_t       *ctx;
    nxt_clone_creds_testcase_t  *tc;
    u_char                      msg[NXT_MAX_ERROR_STR];

    p = msg;
    end = msg + NXT_MAX_ERROR_STR;

    ctx = log->ctx;
    tc = ctx->tc;

    va_start(args, fmt);
    p = nxt_vsprintf(p, end, fmt, args);
    va_end(args);

    *p++ = '\0';

    if (tc->result == NXT_OK && level == NXT_LOG_DEBUG) {
        return;
    }

    if (tc->errmsg.length == 0) {
        nxt_log_error(NXT_LOG_ERR, &nxt_main_log, "unexpected log: %s", msg);
        return;
    }

    if (!nxt_str_eq(&tc->errmsg, msg, (nxt_uint_t) (p - msg - 1))) {
        nxt_log_error(NXT_LOG_ERR, &nxt_main_log,
                      "error log mismatch: got [%s] but wants [%V]",
                      msg, &tc->errmsg);
        return;
    }
}


nxt_int_t
nxt_clone_creds_test(nxt_thread_t *thr)
{
    nxt_mp_t               *mp;
    nxt_int_t              ret;
    nxt_uint_t             count, i;
    nxt_task_t             *task;
    nxt_runtime_t          rt;
    nxt_clone_creds_ctx_t  ctx;

    nxt_log_t nxt_clone_creds_log = {
        NXT_LOG_INFO,
        0,
        nxt_clone_test_log_handler,
        NULL,
        &ctx
    };

    nxt_thread_time_update(thr);

    thr->runtime = &rt;

    task = thr->task;

    mp = nxt_mp_create(1024, 128, 256, 32);
    if (mp == NULL) {
        return NXT_ERROR;
    }

    rt.mem_pool = mp;

    test_log = task->log;
    task->log = &nxt_clone_creds_log;
    task->thread = thr;

    count = sizeof(testcases)/sizeof(nxt_clone_creds_testcase_t);

    for (i = 0; i < count; i++) {
        ret = nxt_clone_test_mappings(task, mp, &ctx, &testcases[i]);

        if (ret != NXT_OK) {
            goto fail;
        }
    }

    ret = NXT_OK;

    nxt_log_error(NXT_LOG_NOTICE, test_log, "clone creds test passed");

fail:
    task->log = test_log;
    nxt_mp_destroy(mp);

    return ret;
}


nxt_int_t
nxt_clone_test_mappings(nxt_task_t *task, nxt_mp_t *mp,
    nxt_clone_creds_ctx_t *ctx, nxt_clone_creds_testcase_t *tc)
{
    nxt_int_t                   ret;
    nxt_runtime_t               *rt;
    nxt_clone_credential_map_t  map;

    rt = task->thread->runtime;

    map.size = 0;

    if (tc->map_data.length > 0) {
        ret = nxt_clone_test_parse_map(task, &tc->map_data, &map);
        if (ret != NXT_OK) {
            return NXT_ERROR;
        }
    }

    rt->capabilities.setid = tc->setid;

    nxt_euid = tc->unit_euid;
    nxt_egid = tc->unit_egid;

    ctx->tc = tc;

    if (nxt_clone_test_map_assert(task, tc, &map) != NXT_OK) {
        return NXT_ERROR;
    }

    if (tc->setid && nxt_euid != 0) {
        /*
         * Running as root should have the same behavior as
         * passing Linux capabilities.
         */

        nxt_euid = 0;
        nxt_egid = 0;

        if (nxt_clone_test_map_assert(task, tc, &map) != NXT_OK) {
            return NXT_ERROR;
        }
    }

    return NXT_OK;
}


nxt_int_t
nxt_clone_test_map_assert(nxt_task_t *task, nxt_clone_creds_testcase_t *tc,
    nxt_clone_credential_map_t *map)
{
    nxt_int_t ret;

    if (tc->map_type == UIDMAP) {
        ret = nxt_clone_vldt_credential_uidmap(task, map, &tc->creds);
    } else {
        ret = nxt_clone_vldt_credential_gidmap(task, map, &tc->creds);
    }

    if (ret != tc->result) {
        nxt_log_error(NXT_LOG_ERR, &nxt_main_log,
                      "return %d instead of %d (map: %V)", ret, tc->result,
                      &tc->map_data);

        return NXT_ERROR;
    }

    return NXT_OK;
}


static nxt_int_t
nxt_clone_test_parse_map(nxt_task_t *task, nxt_str_t *map_str,
    nxt_clone_credential_map_t *map)
{
    nxt_uint_t        i;
    nxt_runtime_t     *rt;
    nxt_conf_value_t  *array, *obj, *value;

    static const nxt_str_t  host_name = nxt_string("host");
    static const nxt_str_t  cont_name = nxt_string("container");
    static const nxt_str_t  size_name = nxt_string("size");

    rt = task->thread->runtime;

    array = nxt_conf_json_parse_str(rt->mem_pool, map_str);
    if (array == NULL) {
        return NXT_ERROR;
    }

    map->size = nxt_conf_array_elements_count(array);

    if (map->size == 0) {
        return NXT_OK;
    }

    map->map = nxt_mp_alloc(rt->mem_pool,
                            map->size * sizeof(nxt_clone_map_entry_t));

    if (map->map == NULL) {
        return NXT_ERROR;
    }

    for (i = 0; i < map->size; i++) {
        obj = nxt_conf_get_array_element(array, i);

        value = nxt_conf_get_object_member(obj, &host_name, NULL);
        map->map[i].host = nxt_conf_get_number(value);

        value = nxt_conf_get_object_member(obj, &cont_name, NULL);
        map->map[i].container = nxt_conf_get_number(value);

        value = nxt_conf_get_object_member(obj, &size_name, NULL);
        map->map[i].size = nxt_conf_get_number(value);
    }

    return NXT_OK;
}
