/* administration.c  --  Functions needed for administration tasks
 *
 *  GPLv2 only - Copyright (C) 2008 - 2012
 *               David Sommerseth <dazo@users.sourceforge.net>
 *
 *  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; version 2
 *  of the License.
 *
 *  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, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 */

/**
 * @file   authentication.c
 * @author David Sommerseth <dazo@users.sourceforge.net>
 * @date   2008-12-03
 *
 * @brief  Functions used for authentication of administration sessions.
 *
 */

#include <string.h>
#include <unistd.h>
#include <assert.h>

#include <libxml/tree.h>

/**
 * @{
 */
#ifndef DRIVERAPIVERSION
# define DRIVERAPIVERSION 2
#endif
/**
 * @}
 */

#include <sqlite3.h>

#include <eurephia_nullsafe.h>
#include <eurephia_context.h>
#include <eurephia_log.h>
#include <eurephia_xml.h>
#include <eurephia_values.h>
#include <eurephiadb_session_struct.h>
#include <eurephiadb_mapping.h>
#include <passwd.h>

#ifndef DRIVER_MODE
#define DRIVER_MODE
#endif
#include <eurephiadb_driver.h>

#include "../sqlite.h"

#if (DRIVERAPIVERSION > 1) || defined(DOXYGEN)
/*
 *  API Version 2 functions
 *
 */


/**
 * Authenticate a user for the administration interface.  This interface do not
 * require any certificate validation and is intended for administration utilities
 * for eurephia.  The eurephia context type must be either ECTX_ADMIN_CONSOLE or
 * ECTX_ADMIN_WEB.
 *
 * @param ctx         eurephiaCTX - context used for administration task
 * @param req_access  String containing the requested administration access level
 * @param uname       username of the user being authenticated
 * @param pwd         password from the user
 *
 * @return Returns an eurephia ResultMsg XML document with the result.  On fatal errors, NULL is returned
 */
static xmlDoc *auth_user(eurephiaCTX *ctx, const char *req_access, const char *uname, const char *pwd) {
        xmlDoc *res_d = NULL;
        xmlNode *info_n = NULL;
        dbresult *res = NULL;
        char *crpwd = NULL, *dbpwd = NULL;
        char *activated = NULL, *deactivated = NULL, *blid = NULL, *uid = NULL;
        int access = 0;
        char interface;

        DEBUG(ctx, 21, "Function call: auth_user(ctx, '%s, '%s', 'xxxxxxxx')", req_access, uname);

        assert(ctx != NULL);

        switch( ctx->context_type ) {
        case ECTX_ADMIN_CONSOLE:
                interface = 'C';
                break;
        case ECTX_ADMIN_WEB:
                interface = 'W';
                break;
        default:
                eurephia_log(ctx, LOG_ERROR, 0, "Wrong eurephia context type (0x%04x)", ctx->context_type);
                return NULL;
        }

        if( (strlen_nullsafe(uname) < 4) || (strlen_nullsafe(pwd) < 4) ) {
                eurephia_log(ctx, LOG_WARNING, 0,
                             "Username and/or password is either null or less than 4 bytes");
                return eurephiaXML_ResultMsg(ctx, exmlERROR, NULL, "Username or password is too short");
        }

        //
        // Authenticate user and password
        //
        res = sqlite_query(ctx,
                           "SELECT activated, deactivated, bl.blid, "
                           "       password, uid "
                           "  FROM openvpn_users ou"
                           "  LEFT JOIN openvpn_blacklist bl USING (username)"
                           " WHERE ou.username = '%q'",
                           uname);

        if( sqlite_query_status(res) != dbSUCCESS ) {
                xmlNode *err_n = NULL;

                eurephia_log(ctx, LOG_FATAL, 0, "Could not authenticate user against the database");
                err_n = sqlite_log_error_xml(ctx, res);
                res_d = eurephiaXML_ResultMsg(ctx, exmlERROR, err_n, "Authentication failed");
                xmlFreeNode(err_n);
                sqlite_free_results(res);
                return res_d;
        }

        if( sqlite_get_numtuples(res) == 1 ) {
                activated   = sqlite_get_value(res, 0, 0);
                deactivated = sqlite_get_value(res, 0, 1);
                blid        = sqlite_get_value(res, 0, 2);
                dbpwd       = sqlite_get_value(res, 0, 3);
                uid         = strdup_nullsafe(sqlite_get_value(res, 0, 4));

                if( blid != NULL ) {
                        eurephia_log(ctx, LOG_WARNING, 0,
                                     "User account '%s' is BLACKLISTED.  You have no access.", uname);
                        sqlite_free_results(res);
                        return eurephiaXML_ResultMsg(ctx, exmlERROR, NULL, "Authentication failed");
                }

                if( activated == NULL ) {
                        eurephia_log(ctx, LOG_WARNING, 0, "User account '%s' is not yet activated.", uname);
                        sqlite_free_results(res);
                        return eurephiaXML_ResultMsg(ctx, exmlERROR, NULL, "Authentication failed");
                }

                if( deactivated != NULL ) {
                        eurephia_log(ctx, LOG_WARNING, 0, "User account '%s' is deactivated.", uname);
                        sqlite_free_results(res);
                        return eurephiaXML_ResultMsg(ctx, exmlERROR, NULL, "Authentication failed");
                }

                if( dbpwd == NULL ) {
                        eurephia_log(ctx, LOG_WARNING, 0, "Authentication failed. DB error.");
                        sqlite_free_results(res);
                        return eurephiaXML_ResultMsg(ctx, exmlERROR, NULL, "Authentication failed");
                } else {
                        int pwdok = 0;
                        // Verify the password
                        crpwd = eurephia_pwd_crypt(ctx, pwd, dbpwd);
                        pwdok = ((crpwd != NULL) && (strcmp(crpwd, dbpwd) == 0) ? 1 : 0);
                        memset(crpwd, 0, strlen_nullsafe(crpwd));
                        memset(dbpwd, 0, strlen_nullsafe(dbpwd));
                        free_nullsafe(ctx, crpwd);
                        if( pwdok == 0 ) {
                                eurephia_log(ctx, LOG_WARNING, 0, "Authentication failed.");
                                sleep(2);
                                sqlite_free_results(res);
                                return eurephiaXML_ResultMsg(ctx, exmlERROR, NULL, "Authentication failed");
                        }
                }
                sqlite_free_results(res);

                // Check if access level is granted
                // (SQLite do not handle advanced joins so well, so we need to
                //  do this check with an extra query)
                res = sqlite_query(ctx,
                                   "SELECT (count(*) = 1) AS access "
                                   "  FROM eurephia_adminaccess"
                                   " WHERE uid = '%q' AND interface = '%c' AND access = '%q'",
                                   uid, interface, req_access);

                if( sqlite_query_status(res) != dbSUCCESS ) {
                        xmlNode *err_n = NULL;

                        eurephia_log(ctx, LOG_FATAL, 0, "Could not check access level");
                        err_n = sqlite_log_error_xml(ctx, res);
                        res_d = eurephiaXML_ResultMsg(ctx, exmlERROR, err_n,"Failed to validate access level");
                        xmlFreeNode(err_n);
                        sqlite_free_results(res);
                        return res_d;
                }
                access = atoi_nullsafe(sqlite_get_value(res, 0, 0));
                sqlite_free_results(res);

                if( access == 0 ) {
                        eurephia_log(ctx, LOG_WARNING, 0,
                                     "User account '%s' is lacking privileges for this operation", uname);
                        return eurephiaXML_ResultMsg(ctx, exmlERROR, NULL, "Authentication failed");
                }
        } else {
                eurephia_log(ctx, LOG_WARNING, 0, "Authentication failed. No unique records found.");
                sqlite_free_results(res);
                sleep(2);
                return eurephiaXML_ResultMsg(ctx, exmlERROR, NULL, "Authentication failed");
        }

        // If we reach this place, authentication was successful.  Return users uid
        info_n = xmlNewNode(NULL, (xmlChar *) "UserAccount");
        assert( info_n != NULL );
        xmlNewProp(info_n, (xmlChar *) "uid", (xmlChar *) uid);
        res_d =  eurephiaXML_ResultMsg(ctx, exmlRESULT, info_n, "Successful authentication");
        xmlFreeNode(info_n);
        free_nullsafe(ctx, uid);
        return res_d;
}


/**
 * Validates a session key, to see if it still is valid (not auto-logged out or invalid session key)
 * and to check if they have access to a different access level. The eurephia context type must be
 * either ECTX_ADMIN_CONSOLE or ECTX_ADMIN_WEB.
 *
 * @param ctx        eurephiaCTX
 * @param sesskey    String containing the session key to validate
 * @param req_access String containing the required administration access level
 *
 * @return Returns an eurephia ResultMsg XML document with the result.  On fatal errors, NULL is returned
 */
static xmlDoc *auth_session(eurephiaCTX *ctx, const char *sesskey, const char *req_access) {
        dbresult *res = NULL;
        int valid = 0, access = 0, expire_time = 0;
        char interface;
        xmlDoc *ret_d = NULL;
        xmlNode *err_n = NULL;

        DEBUG(ctx, 21, "Function call: auth_session(ctx, '%s, '%s')", sesskey, req_access);
        assert( (ctx != NULL) && (sesskey != NULL) );

        switch( ctx->context_type ) {
        case ECTX_ADMIN_CONSOLE:
                interface = 'C';
                break;
        case ECTX_ADMIN_WEB:
                interface = 'W';
                break;
        default:
                eurephia_log(ctx, LOG_ERROR, 0, "Wrong eurephia context type (0x%04x)", ctx->context_type);
                return NULL;
        }

        // Check if the session is still valid (not expired) and that this session are allowed to access
        // the requested access level.
        expire_time = (60 * atoi_nullsafe(defaultValue(eGet_value(ctx->dbc->config,
                                                                  "eurephiadmin_autologout"),
                                                       "10")
                                          )
                       );
        res = sqlite_query(ctx,
                           "SELECT (strftime('%%s',CURRENT_TIMESTAMP)-strftime('%%s',last_action)) > %i AS exp,"
                           "       (access IS NOT NULL) AS access"
                           "  FROM eurephia_adminlog"
                           "  LEFT JOIN eurephia_adminaccess USING(uid,interface)"
                           " WHERE status IN (1,2)"
                           "       AND sessionkey = '%q'"
                           "       AND access = '%q'"
                           "       AND interface = '%c'",
                           expire_time, sesskey, req_access, interface);

        if( sqlite_query_status(res) != dbSUCCESS ) {
                eurephia_log(ctx, LOG_FATAL, 0, "Could not validate session");
                err_n = sqlite_log_error_xml(ctx, res);
                ret_d = eurephiaXML_ResultMsg(ctx, exmlERROR, NULL, "Session authentication failed");
                sqlite_free_results(res);
                xmlFreeNode(err_n);
                return ret_d;
        }

        valid  = (atoi_nullsafe(sqlite_get_value(res, 0, 0)) == 0);
        access = (atoi_nullsafe(sqlite_get_value(res, 0, 1)) == 1);
        sqlite_free_results(res);

        // If still valid, update last_action
        if( valid && access ) {
                res = sqlite_query(ctx,
                                   "UPDATE eurephia_adminlog"
                                   "   SET last_action = CURRENT_TIMESTAMP, status = 2"
                                   " WHERE sessionkey = '%q'", sesskey);
                if( sqlite_query_status(res) != dbSUCCESS ) {
                        eurephia_log(ctx, LOG_ERROR, 0, "Could not register session activity");
                        err_n = sqlite_log_error_xml(ctx, res);
                }
                sqlite_free_results(res);

        } else {
                // If not valid, register session as auto-logged out
                res = sqlite_query(ctx,
                                   "UPDATE eurephia_adminlog"
                                   "   SET logout = CURRENT_TIMESTAMP, status = %i"
                                   " WHERE sessionkey = '%q'",
                                   (access ? 4 : 5), sesskey);
                if( sqlite_query_status(res) != dbSUCCESS ) {
                        eurephia_log(ctx, LOG_ERROR, 0, "Could not register old session as logged out");
                        err_n = sqlite_log_error_xml(ctx, res);
                }
                sqlite_free_results(res);

                // Delete session variables
                res = sqlite_query(ctx, "DELETE FROM openvpn_sessions WHERE sessionkey = '%q'",
                                   sesskey);
                if( sqlite_query_status(res) != dbSUCCESS ) {
                        eurephia_log(ctx, LOG_ERROR, 0,
                                     "Could not delete session variables (%s))", sesskey);
                        sqlite_log_error(ctx, res);
                } else if( !access ) {
                        eurephia_log(ctx, LOG_WARNING, 0, "User account is lacking privileges");
                }
                sqlite_free_results(res);
        }

        if (valid && access) {
                ret_d = eurephiaXML_ResultMsg(ctx, exmlRESULT, err_n, "Session authenticated");
        } else {
                ret_d = eurephiaXML_ResultMsg(ctx, exmlERROR, err_n, "Session authentication failed");
        }
        if( err_n != NULL ) {
                xmlFreeNode(err_n);
        }
        return ret_d;
}


/**
 * Registers the user as logged in after a successful authentication.  The user must
 * be registered as logged in to have a valid session.
 *
 * @param ctx eurephiaCTX
 * @param uid Numeric value if the user ID the session belongs to
 * @param sesskey String containing the session key
 *
 * @return Returns an eurephia ResultMsg XML document with the result.  On fatal errors, NULL is returned
 */
static xmlDoc *register_login(eurephiaCTX *ctx, const int uid, const char *sesskey) {
        xmlDoc *ret_d = NULL;
        dbresult *res = NULL;
        char interface;

        DEBUG(ctx, 21, "Function call: register_login(ctx, %i, '%s')", uid, sesskey);
        assert( ctx != NULL );

        if( (sesskey == NULL) || (uid < 1) ) {
                return eurephiaXML_ResultMsg(ctx, exmlERROR, NULL, "Invalid data for login registration");
        }

        switch( ctx->context_type ) {
        case ECTX_ADMIN_CONSOLE:
                interface = 'C'; break;
        case ECTX_ADMIN_WEB:
                interface = 'W'; break;
        default:
                eurephia_log(ctx, LOG_ERROR, 0, "Wrong eurephia context type (0x%04x)", ctx->context_type);
                return NULL;
        }

        // Register login into eurephia_adminlog ... uid, login, interface, sessionkey
        res = sqlite_query(ctx,
                           "INSERT INTO eurephia_adminlog "
                           "       (uid, interface, status, login, last_action, sessionkey) "
                           "VALUES ('%i','%c',1,CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, '%q')",
                           uid, interface, sesskey);
        if( sqlite_query_status(res) == dbSUCCESS ) {
                ret_d = eurephiaXML_ResultMsg(ctx, exmlRESULT, NULL, "Session is registered as logged in");
        } else {
                xmlNode *err_n = NULL;

                eurephia_log(ctx, LOG_FATAL, 0, "Failed to register the session in the database");
                err_n = sqlite_log_error_xml(ctx, res);
                ret_d = eurephiaXML_ResultMsg(ctx, exmlERROR, err_n,
                                              "Failed to register the session in the database");
                xmlFreeNode(err_n);
        }
        sqlite_free_results(res);
        return ret_d;
}


/**
 * Registers a session as logged out.  This will require the user to do a new authentication
 * on next access via the administration interface
 *
 * @param ctx eurephiaCTX
 * @param sessionkey String containing the session key
 *
 * @return Returns an eurephia ResultMsg XML document with the result.  On fatal errors, NULL is returned
 */
static xmlDoc *register_logout(eurephiaCTX *ctx, const char *sessionkey) {
        dbresult *res = NULL;
        xmlDoc *ret_d = NULL;
        xmlNode *err_n = NULL;

        DEBUG(ctx, 21, "Function call: register_logout(ctx, '%s')", sessionkey);
        assert((ctx != NULL) && (sessionkey != NULL));

        if( (ctx->context_type != ECTX_ADMIN_CONSOLE) && (ctx->context_type != ECTX_ADMIN_WEB) ) {
                eurephia_log(ctx, LOG_CRITICAL, 0,
                             "eurephia admin function call attempted with wrong context type");
                return NULL;
        }

        // Update session as logged out
        res = sqlite_query(ctx,
                           "UPDATE eurephia_adminlog "
                           "   SET logout = CURRENT_TIMESTAMP, status = 3"
                           " WHERE sessionkey = '%q'",
                           sessionkey);
        if( (sqlite_query_status(res) != dbSUCCESS) || (sqlite_get_affected_rows(res) == 0) ) {
                eurephia_log(ctx, LOG_FATAL, 0,
                             "Failed to register the session as logged out (updated %i rows)",
                             sqlite_get_affected_rows(res));
                if( sqlite_query_status(res) == dbERROR ) {
                        err_n = sqlite_log_error_xml(ctx, res);
                }
                ret_d = eurephiaXML_ResultMsg(ctx, exmlERROR, NULL,
                                              "Failed to register the session as logged out");
                xmlFreeNode(err_n);
                goto exit;
        }
        sqlite_free_results(res);

        // Delete session variables
        res = sqlite_query(ctx, "DELETE FROM openvpn_sessions WHERE sessionkey = '%q'", sessionkey);
        if( (sqlite_query_status(res) == dbSUCCESS) && (sqlite_get_affected_rows(res) > 0) ) {
                ret_d = eurephiaXML_ResultMsg(ctx, exmlRESULT, NULL, "Session is logged out");
        } else {
                eurephia_log(ctx, LOG_ERROR, 0,
                             "Could not delete session variables (%s))", sessionkey);
                if( sqlite_query_status(res) == dbERROR ) {
                        err_n = sqlite_log_error_xml(ctx, res);
                }
                ret_d = eurephiaXML_ResultMsg(ctx, exmlERROR, err_n,
                                              "Could not delete session variables (%s)", sessionkey);
                xmlFreeNode(err_n);
        }

 exit:
        sqlite_free_results(res);
        return ret_d;
}


/**
 * @copydoc eDBadminAuthenticate()
 */
xmlDoc *eDBadminAuthenticate(eurephiaCTX *ctx, xmlDoc *qryxml) {
        xmlDoc *res_d = NULL;
        xmlNode *qry_n = NULL;
        char *mode = NULL;
        int type = 0;

        DEBUG(ctx, 20, "Function call: eDBadminAuthenticate(ctx, xmlDoc)");
        assert( (ctx != NULL) && (qryxml != NULL) );

        if( (ctx->context_type != ECTX_ADMIN_CONSOLE) && (ctx->context_type != ECTX_ADMIN_WEB) ) {
                eurephia_log(ctx, LOG_CRITICAL, 0,
                             "eurephia admin function call attempted with wrong context type");
                return NULL;
        }

        qry_n = eurephiaXML_getRoot(ctx, qryxml, "Authenticate", 1);
        if( qry_n != NULL ) {
                type = 1;
                goto accept;
        }

        qry_n = eurephiaXML_getRoot(ctx, qryxml, "Register", 1);
        if( qry_n != NULL ) {
                type = 2;
                goto accept;
        }

        eurephia_log(ctx, LOG_ERROR, 0, "Could not find a valid XML request for eDBadminAuthenticate()");
        return NULL;

 accept:
        mode = xmlGetAttrValue(qry_n->properties, "mode");
        if( mode == NULL ) {
                eurephia_log(ctx, LOG_ERROR, 0, "Invalid authentication request");
                return NULL;
        }

        switch( type ) {
        case 1: // Authenticate tag
                if( strcmp(mode, "user") == 0 ) {
                        // const char *req_access, const char *uname, const char *pwd
                        const char *reqacc = NULL, *uname = NULL, *pwd = NULL;
                        uname  = xmlGetNodeContent(qry_n, "username");
                        pwd    = xmlGetNodeContent(qry_n, "password");
                        reqacc = xmlGetNodeContent(qry_n, "accesslevel");
                        res_d  = auth_user(ctx, reqacc, uname, pwd);
                } else if ( strcmp(mode, "session") == 0 ) {
                        const char *sesskey = NULL, *reqacc = NULL;
                        sesskey = xmlGetNodeContent(qry_n, "sessionkey");
                        reqacc  = xmlGetNodeContent(qry_n, "accesslevel");
                        res_d   = auth_session(ctx, sesskey, reqacc);
                }
                break;

        case 2: // Register tag
                if( strcmp(mode, "login") == 0 ) {
                        const char *sesskey = NULL;
                        unsigned int uid = 0;

                        uid     = atoi_nullsafe(xmlGetAttrValue(qry_n->properties, "uid"));
                        sesskey = xmlExtractContent(qry_n);
                        res_d   = register_login(ctx, uid, sesskey);
                } else if( strcmp(mode, "logout") == 0 ) {
                        const char *sesskey = NULL;

                        sesskey = xmlExtractContent(qry_n);
                        res_d   = register_logout(ctx, sesskey);
                }
                break;

        default:
                eurephia_log(ctx, LOG_FATAL, 0, "The unthinkable has just happened (type %i)", type);
                res_d = NULL;
                break;
        }
        return res_d;
}

#endif
