//
// Copyright (c) 2017-2025, Manticore Software LTD (https://manticoresearch.com)
// Copyright (c) 2011-2016, Andrew Aksyonoff
// Copyright (c) 2011-2016, Sphinx Technologies Inc
// All rights reserved
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License. You should have
// received a copy of the GPL license along with this program; if you
// did not, you can find it at http://www.gnu.org/
//

#include "sphinxutils.h"

#include "searchdsql.h"
#include "client_session.h"
#include "searchdssl.h"
#include "auth_common.h"
#include "auth_proto_mysql.h"
#include "auth_log.h"
#include "auth.h"

static void Crypt ( BYTE * pBuf, const BYTE * pS1, const BYTE * pS2, int iLen )
{
	const BYTE * pEnd = pS1 + iLen;
	while ( pS1<pEnd )
	{
		*pBuf++= *pS1++ ^ *pS2++;
	}
}

/////////////////////////////////////////////////////////////////////////////
/// SphinxQL

MySQLAuth_t GetMySQLAuth()
{
	MySQLAuth_t tAuth;

	// Fill scramble auth data with printable ASCII bytes.
	// Connector/J treats mysql_native_password challenge data as a string internally;
	// arbitrary high-bit bytes make it compute a token that does not match the server.
	static constexpr char dChallengeAlphabet[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
	for ( int i = 0; i < tAuth.m_dScramble.GetLength() - 1; ++i )
		tAuth.m_dScramble[i] = dChallengeAlphabet [ sphRand() % ( sizeof(dChallengeAlphabet) - 1 ) ];
	tAuth.m_dScramble.Last()  = 0;

	return tAuth;
}

static bool CheckPwd ( const MySQLAuth_t & tSalt, const AuthUserCred_t & tEntry, const VecTraits_T<BYTE> & dClientHash )
{
	if ( dClientHash.GetLength()!=HASH20_SIZE )
		return false;

	const HASH20_t & tSha1 = tEntry.m_tPwdSha1;
	HASH20_t tSha2 = CalcBinarySHA1 ( tSha1.data(), tSha1.size() );

	SHA1_c tSaltSha2Calc;
	tSaltSha2Calc.Init();
	tSaltSha2Calc.Update ( (const BYTE *)tSalt.m_dScramble.Begin(), tSalt.m_dScramble.GetLength()-1 );
	tSaltSha2Calc.Update ( tSha2.data(), tSha2.size() );
	HASH20_t tSha3 = tSaltSha2Calc.FinalHash();

	CSphFixedVector<BYTE> dRes ( tSha1.size() );
	Crypt ( dRes.Begin(), tSha3.data(), tSha1.data(), dRes.GetLength() );

	return SecretEqual ( dRes, dClientHash );
}

bool CheckAuth ( const MySQLAuth_t & tAuth, const CSphString & sUser, const VecTraits_T<BYTE> & dClientHash, CSphString & sError )
{
	if ( !IsAuthEnabled() )
		return true;

	AuthUsersPtr_t pUsers = GetAuth();
	const AuthUserCred_t * pUser = pUsers->m_hUserToken ( sUser );
	if ( !pUser )
	{
		sError.SetSprintf ( "Access denied for user '%s' (using password: %s)", sUser.cstr(), ( dClientHash.IsEmpty() ? "NO" : "YES" ) );
		AuthLog().AuthFailure ( sUser, AccessMethod_e::SQL, session::szClientName(), "user does not exist" );
		return false;
	}

	if ( dClientHash.IsEmpty() )
	{
		sError.SetSprintf ( "Access denied for user '%s' (using password: NO)", sUser.cstr() );
		AuthLog().AuthFailure ( sUser, AccessMethod_e::SQL, session::szClientName(), "no password provided" );
		return false;
	}

	if ( dClientHash.GetLength()!=HASH20_SIZE || !CheckPwd ( tAuth, *pUser, dClientHash ) )
	{
		sError.SetSprintf ( "Access denied for user '%s' (using password: YES)", sUser.cstr() );
		AuthLog().AuthFailure ( sUser, AccessMethod_e::SQL, session::szClientName(), "invalid password" );
		return false;
	} else
	{
		AuthLog().AuthSuccess ( sUser, AccessMethod_e::SQL, session::szClientName() );
		return true;
	}
}

bool ReplicationResolveUser ( const SqlStmt_t & tStmt, const CSphString & sSessionUser, CSphString & sRplUser, CSphString & sError );

static CSphString GetCallPermTarget ( const SqlStmt_t & tStmt )
{
	CSphString sProc { tStmt.m_sCallProc };
	sProc.ToUpper();

	int iIndexArg = -1;
	if ( sProc=="PQ" )
		iIndexArg = 0;
	else if ( sProc=="SUGGEST" || sProc=="QSUGGEST" || sProc=="SNIPPETS" || sProc=="KEYWORDS" || sProc=="AUTOCOMPLETE" )
		iIndexArg = 1;

	if ( iIndexArg<0 || iIndexArg>=tStmt.m_dInsertValues.GetLength() )
		return "";

	const auto & tIndexArg = tStmt.m_dInsertValues[iIndexArg];
	if ( tIndexArg.m_iType!=SqlInsert_t::QUOTED_STRING )
		return "";

	CSphString sIndex { tIndexArg.m_sVal };
	SqlParser_SplitClusterIndex ( sIndex, nullptr );
	return sIndex;
}


static bool IsMysqlCompatSelectColumn ( const CSphString & sExpr )
{
	// Keep this list narrow. Unknown sysvars and expressions must still require read permission.
	const char * dSafeSysvars[] =
	{
		"@@version_comment",
		"@@character_set_client",
		"@@character_set_connection",
		"@@max_allowed_packet",
		"@@lower_case_table_names",
		"@@autocommit"
	};

	for ( const char * szSysvar : dSafeSysvars )
		if ( sExpr==szSysvar )
			return true;

	return false;
}


static bool IsMysqlCompatSelect ( const SqlStmt_t & tStmt )
{
	if ( !tStmt.m_sIndex.IsEmpty() || tStmt.m_tQuery.m_dItems.IsEmpty() )
		return false;

	for ( const CSphQueryItem & tItem : tStmt.m_tQuery.m_dItems )
		if ( tItem.m_eAggrFunc!=SPH_AGGR_NONE || !IsMysqlCompatSelectColumn ( tItem.m_sExpr ) )
			return false;

	return true;
}


static bool DenyInternalAuthStorageTarget ( const CSphString & sUser, const CSphString & sTarget, CSphString & sError )
{
	if ( !strstr ( sTarget.cstr(), GetPrefixAuth().cstr() ) )
		return false;

	sError.SetSprintf ( "Permission denied for user '%s'", sUser.cstr() );
	return true;
}


static bool IsSessionOnlySetExtra ( const SqlStmt_t & tStmt )
{
	if ( tStmt.m_eSet!=SET_EXTRA )
		return false;

	if ( !tStmt.m_dInsertValues.GetLength() || tStmt.m_dInsertValues.GetLength()!=tStmt.m_dInsertSchema.GetLength() )
		return false;

	for ( const SqlInsert_t & tValue : tStmt.m_dInsertValues )
		if ( !( tValue.m_iType & 1 ) )
			return false;

	return true;
}


static bool SqlCheckStmtPerms ( const CSphString & sUser, const SqlStmt_t & tStmt, CSphString & sError )
{
	// handle SQL query
	switch ( tStmt.m_eStmt )
	{
	case STMT_PARSE_ERROR: return true;
	case STMT_DUMMY: return true; // harmless MySQL compatibility commands, e.g. SET NAMES

	case STMT_SELECT:
	case STMT_DESCRIBE:
	case STMT_SHOW_CREATE_TABLE:
	case STMT_SHOW_INDEX_STATUS:
	case STMT_SHOW_AGENT_STATUS:
	case STMT_SHOW_FEDERATED_INDEX_STATUS:
	case STMT_FACET:
	case STMT_SHOW_INDEX_SETTINGS:
	case STMT_EXPLAIN:
	case STMT_SHOW_TABLE_INDEXES:
	{
		const CSphString & sTarget = tStmt.m_sIndex.IsEmpty() ? tStmt.m_tQuery.m_sIndexes : tStmt.m_sIndex;
		if ( DenyInternalAuthStorageTarget ( sUser, sTarget, sError ) )
			return false;

		return CheckPerms ( sUser, AuthAction_e::READ, sTarget, false, sError );
	}

	case STMT_CALL:
	{
		auto sTarget = GetCallPermTarget ( tStmt );
		return CheckPerms ( sUser, AuthAction_e::READ, sTarget, false, sError );
	}

	// special read actions without index
	case STMT_SHOW_WARNINGS:
	case STMT_SHOW_META:
	case STMT_SHOW_TABLES:
	case STMT_SHOW_PROFILE:
	case STMT_SHOW_PLAN:
	case STMT_SHOW_SCROLL:
		return CheckPerms ( sUser, AuthAction_e::READ, tStmt.m_sIndex, true, sError );

	case STMT_SELECT_COLUMNS:
		if ( IsMysqlCompatSelect ( tStmt ) )
			return true;
		return CheckPerms ( sUser, AuthAction_e::READ, tStmt.m_sIndex, true, sError );

	case STMT_INSERT:
	case STMT_REPLACE:
	case STMT_DELETE:
	case STMT_UPDATE:
	case STMT_ATTACH_INDEX:
	case STMT_FLUSH_RTINDEX:
	case STMT_FLUSH_RAMCHUNK:
	case STMT_TRUNCATE_RTINDEX:
	case STMT_OPTIMIZE_INDEX:
	case STMT_FLUSH_INDEX:
	case STMT_ALTER_KLIST_TARGET:
	case STMT_FREEZE:
	case STMT_UNFREEZE:
		if ( DenyInternalAuthStorageTarget ( sUser, tStmt.m_sIndex, sError ) )
			return false;

		return CheckPerms ( sUser, AuthAction_e::WRITE, tStmt.m_sIndex, false, sError );

	case STMT_SET:
		if ( tStmt.m_eSet==SET_LOCAL || IsSessionOnlySetExtra ( tStmt ) )
			return true;
		return CheckPerms ( sUser, AuthAction_e::WRITE, tStmt.m_sIndex, true, sError );

	// special write actions without index
	case STMT_BEGIN:
	case STMT_COMMIT:
	case STMT_ROLLBACK:
	case STMT_SYSFILTERS:
	case STMT_KILL:
	case STMT_FLUSH_HOSTNAMES:
	case STMT_FLUSH_LOGS:
		return CheckPerms ( sUser, AuthAction_e::WRITE, tStmt.m_sIndex, true, sError );

	case STMT_CREATE_TABLE:
	case STMT_CREATE_TABLE_LIKE:
	case STMT_DROP_TABLE:
	case STMT_CREATE_FUNCTION:
	case STMT_DROP_FUNCTION:
	case STMT_ALTER_ADD:
	case STMT_ALTER_DROP:
	case STMT_ALTER_MODIFY:
	case STMT_CREATE_PLUGIN:
	case STMT_DROP_PLUGIN:
	case STMT_ALTER_RECONFIGURE:
	case STMT_RELOAD_PLUGINS:
	case STMT_RELOAD_INDEX:
	case STMT_RELOAD_INDEXES:
	case STMT_ALTER_INDEX_SETTINGS:
	case STMT_IMPORT_TABLE:
	case STMT_ALTER_REBUILD_SI:
		return CheckPerms ( sUser, AuthAction_e::SCHEMA, tStmt.m_sIndex, false, sError );

	case STMT_JOIN_CLUSTER:
	case STMT_CLUSTER_CREATE:
	case STMT_CLUSTER_DELETE:
	case STMT_CLUSTER_ALTER_ADD:
	case STMT_CLUSTER_ALTER_DROP:
	case STMT_CLUSTER_ALTER_UPDATE:
	{
		CSphString sAuthUser;
		if ( !ReplicationResolveUser ( tStmt, sUser, sAuthUser, sError ) )
			return false;
		return CheckPerms ( sAuthUser, AuthAction_e::REPLICATION, tStmt.m_sCluster, false, sError );
	}


	case STMT_SHOW_VARIABLES:
	case STMT_SHOW_COLLATION:
	case STMT_SHOW_CHARACTER_SET:
		return true;

	case STMT_SHOW_STATUS:
	case STMT_SHOW_DATABASES:
	case STMT_SHOW_PLUGINS:
	case STMT_SHOW_THREADS:
	case STMT_SHOW_SETTINGS:
	case STMT_SHOW_LOCKS:
		return CheckPerms ( sUser, AuthAction_e::SCHEMA, tStmt.m_sIndex, true, sError );

	case STMT_RELOAD_AUTH:
	case STMT_DEBUG:
	case STMT_SHOW_USERS:
		 return CheckPerms ( sUser, AuthAction_e::ADMIN, tStmt.m_sIndex, true, sError );

	case STMT_SHOW_PERMISSIONS:
	case STMT_SHOW_USAGE:
	{
		if ( !tStmt.m_dCallStrings.GetLength() )
			return true;

		// SHOW PERMISSIONS/USAGE FOR '<user>' is an admin-only path.
		if ( !CheckPerms ( sUser, AuthAction_e::ADMIN, tStmt.m_sIndex, true, sError ) )
			return false;

		const CSphString & sTargetUser = tStmt.m_dCallStrings[0];
		AuthUsersPtr_t pUsers = GetAuth();
		if ( !pUsers->m_hUserToken ( sTargetUser ) )
		{
			sError.SetSprintf ( "user '%s' not found", sTargetUser.cstr() );
			return false;
		}

		return true;
	}

	case STMT_SHOW_TOKEN:
	case STMT_SET_PASSWORD:
	case STMT_TOKEN:
	{
		const CSphString sTargetUser = ( tStmt.m_dCallStrings.GetLength() ? tStmt.m_dCallStrings[0] : sUser );
		if ( sTargetUser==sUser )
			return true;
		return CheckPerms ( sUser, AuthAction_e::ADMIN, tStmt.m_sIndex, true, sError );
	}

	case STMT_GRANT:
	case STMT_REVOKE:
	case STMT_CREATE_USER:
	case STMT_DROP_USER:
		return CheckPerms ( sUser, AuthAction_e::ADMIN, tStmt.m_sIndex, true, sError );

	default:
		break;
	}

	// FIXME!!! skip buddy for that failed query
	sError.SetSprintf ( "Permission denied for user '%s'", sUser.cstr() );
	return false;
}


bool SqlCheckPerms ( const CSphString & sUser, const CSphVector<SqlStmt_t> & dStmt, CSphString & sError )
{
	if ( !IsAuthEnabled() )
		return true;

	for ( const SqlStmt_t & tStmt : dStmt )
		if ( !SqlCheckStmtPerms ( sUser, tStmt, sError ) )
			return false;

	return true;
}

bool SqlSkipBuddy()
{
	auto pSession = session::Info().GetClientSession();
	if ( !pSession )
		return false;

	if ( pSession->m_bAuthAllowBuddy )
		return false;

	if ( pSession->m_bAuthErrorSkipBuddy )
		return true;

	switch ( pSession->m_eLastStmt )
	{
	case STMT_SHOW_USERS:
	case STMT_SHOW_USAGE:
	case STMT_SHOW_PERMISSIONS:
	case STMT_SHOW_TOKEN:
	case STMT_SET_PASSWORD:
	case STMT_TOKEN:
	case STMT_CREATE_USER:
	case STMT_DROP_USER:
	case STMT_GRANT:
	case STMT_REVOKE:
		return true;
	default:
		return false;
	}
}

static bool HasAdminPerm ( const CSphString & sUser )
{
	AuthUsersPtr_t pUsers = GetAuth();
	const UserPerms_t * pPerms = pUsers->m_hUserPerms ( sUser );
	if ( !pPerms || !pPerms->GetLength() )
		return false;

	for ( const auto & tPerm : *pPerms )
	{
		if ( tPerm.m_eAction==AuthAction_e::ADMIN )
			return tPerm.m_bAllow;
	}

	return false;
}

static bool DumpUserPerms ( const CSphString & sUser, const UserPerms_t & dPerms, RowBuffer_i & tOut )
{
	for ( const auto & tPerm : dPerms )
	{
		tOut.PutString ( sUser );
		tOut.PutString ( GetActionName ( tPerm.m_eAction ) );
		tOut.PutString ( tPerm.m_sTarget );
		tOut.PutString ( tPerm.m_bAllow ? "1" : "0" );
		tOut.PutString ( tPerm.m_sBudget );
		if ( !tOut.Commit () )
			return false;
	}

	return true;
}

static bool DumpUserUsage ( const CSphString & sUser, RowBuffer_i & tOut )
{
	tOut.PutString ( sUser );
	tOut.PutString ( "" );
	tOut.PutString ( "" );
	tOut.PutString ( "" );
	return tOut.Commit ();
}

void HandleMysqlShowPerms ( RowBuffer_i & tOut, const CSphString * pTargetUser )
{
	tOut.HeadBegin ();
	tOut.HeadColumn ( "username" );
	tOut.HeadColumn ( "action" );
	tOut.HeadColumn ( "target" );
	tOut.HeadColumn ( "allow" );
	tOut.HeadColumn ( "budget" );
	if ( !tOut.HeadEnd () )
		return;

	if ( IsAuthEnabled() )
	{
		const CSphString & sSessionUser = session::GetUser();
		AuthUsersPtr_t pUsers = GetAuth();

		if ( pTargetUser )
		{
			assert ( HasAdminPerm ( sSessionUser ) );
			const UserPerms_t * pPerms = pUsers->m_hUserPerms ( *pTargetUser );
			if ( pPerms && !DumpUserPerms ( *pTargetUser, *pPerms, tOut ) )
				return;

		} else if ( !HasAdminPerm ( sSessionUser ) )
		{
			const UserPerms_t * pPerms = pUsers->m_hUserPerms ( sSessionUser );
			if ( pPerms && !DumpUserPerms ( sSessionUser, *pPerms, tOut ) )
				return;

		} else
		{
			for ( const auto & tUser : pUsers->m_hUserPerms )
			{
				if ( !DumpUserPerms ( tUser.first, tUser.second, tOut ) )
					return;
			}
		}
	}

	tOut.Eof ( false );
}

void HandleMysqlShowUsage ( RowBuffer_i & tOut, const CSphString * pTargetUser )
{
	tOut.HeadBegin ();
	tOut.HeadColumn ( "username" );
	tOut.HeadColumn ( "queries_per_min" );
	tOut.HeadColumn ( "queries_per_day" );
	tOut.HeadColumn ( "last_login" );
	if ( !tOut.HeadEnd () )
		return;

	if ( IsAuthEnabled() )
	{
		const CSphString & sSessionUser = session::GetUser();
		AuthUsersPtr_t pUsers = GetAuth();

		if ( pTargetUser )
		{
			assert ( HasAdminPerm ( sSessionUser ) );
			if ( pUsers->m_hUserToken ( *pTargetUser ) && !DumpUserUsage ( *pTargetUser, tOut ) )
				return;
		} else if ( !HasAdminPerm ( sSessionUser ) )
		{
			if ( pUsers->m_hUserToken ( sSessionUser ) && !DumpUserUsage ( sSessionUser, tOut ) )
				return;
		} else
		{
			for ( const auto & tUser : pUsers->m_hUserToken )
			{
				if ( !DumpUserUsage ( tUser.first, tOut ) )
					return;
			}
		}
	}

	tOut.Eof ( false );
}

void HandleMysqlShowUsers ( RowBuffer_i & tOut )
{
	tOut.HeadBegin ();
	tOut.HeadColumn ( "username" );
	if ( !tOut.HeadEnd () )
		return;

	if ( IsAuthEnabled() )
	{
		AuthUsersPtr_t pUsers = GetAuth();
		for ( const auto & tUser : pUsers->m_hUserToken )
		{
			tOut.PutString ( tUser.first );
			if ( !tOut.Commit () )
				return;
		}
	}

	tOut.Eof ( false );
}

void HandleMysqlShowToken ( const CSphString & sUser, RowBuffer_i & tOut )
{
	tOut.HeadBegin ();
	tOut.HeadColumn ( "username" );
	tOut.HeadColumn ( "token" );
	if ( !tOut.HeadEnd () )
		return;

	if ( IsAuthEnabled() )
	{
		AuthUsersPtr_t pUsers = GetAuth();
		const AuthUserCred_t * pUser = pUsers->m_hUserToken ( sUser );

		if ( pUser )
		{
			tOut.PutString ( sUser );
			tOut.PutString ( BinToHex ( pUser->m_dBearerSha256 ) );

			if ( !tOut.Commit () )
				return;
		}
	}

	tOut.Eof ( false );

}
