/*
 *	PROGRAM:	Security data base manager
 *	MODULE:		security.epp
 *	DESCRIPTION:	Security routines
 *
 * The contents of this file are subject to the Interbase Public
 * License Version 1.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy
 * of the License at http://www.Inprise.com/IPL.html
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * The Original Code was created by Inprise Corporation
 * and its predecessors. Portions created by Inprise Corporation are
 * Copyright (C) Inprise Corporation.
 *
 * All Rights Reserved.
 * Contributor(s): ______________________________________.
 */

#include "firebird.h"
#include "../common/classes/alloc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "../jrd/ibase.h"
#include "../auth/SecurityDatabase/LegacyHash.h"
#include "../common/enc_proto.h"
#include "../yvalve/gds_proto.h"
#include "../common/isc_proto.h"
#include "../utilities/gsec/gsec.h"
#include "../common/utils_proto.h"
#include "../common/classes/init.h"
#include "../common/classes/UserBlob.h"
#include "../common/config/config_file.h"
#include "../auth/SecurityDatabase/LegacyManagement.h"
#include "../common/classes/ImplementHelper.h"
#include "../common/classes/ClumpletWriter.h"
#include "../common/StatusHolder.h"
#include "../common/security.h"
#include "firebird/Interface.h"

// Here we use version-independent symbolic link (or copy) of actual database
DATABASE database = STATIC FILENAME "security.fdb";

static Firebird::GlobalPtr<Firebird::Mutex> execLineMutex;	// protects various gpre generated structures

static bool grantRevokeAdmin(ISC_STATUS* isc_status, FB_API_HANDLE database, FB_API_HANDLE trans,
							 Firebird::IUser* user)
{
	if (!user->admin()->entered())
	{
		return true;
	}

	Firebird::string userName(user->userName()->get());
	for (unsigned i = 0; i < userName.length(); ++i)
	{
		if (userName[i] == '"')
		{
			userName.insert(i++, 1, '"');
		}
	}

	Firebird::string sql;
	sql.printf((user->admin()->get() ? "GRANT %s TO \"%s\"" : "REVOKE %s FROM \"%s\""),
			ADMIN_ROLE, userName.c_str());
	isc_dsql_execute_immediate(isc_status, &database, &trans, sql.length(), sql.c_str(), SQL_DIALECT_V6, NULL);

	if (isc_status[1] && user->admin()->get() == 0)
	{
		isc_req_handle request = 0;

		FOR (TRANSACTION_HANDLE trans REQUEST_HANDLE request) R IN RDB$USER_PRIVILEGES
				WITH R.RDB$USER EQ user->userName()->get()
				 AND R.RDB$RELATION_NAME EQ 'RDB$ADMIN'
				 AND R.RDB$PRIVILEGE EQ 'M'
			sql.printf("REVOKE " ADMIN_ROLE " FROM \"%s\" GRANTED BY \"%s\"",
				userName.c_str(), R.RDB$GRANTOR);
		END_FOR

		if (request)
		{
			ISC_STATUS_ARRAY s;
			if (isc_release_request(s, &request) != FB_SUCCESS)
			{
				return false;
			}
		}

		isc_dsql_execute_immediate(isc_status, &database, &trans, sql.length(), sql.c_str(), SQL_DIALECT_V6, NULL);
	}

	return isc_status[1] == 0;
}


const static unsigned int INIT_KEY = ((~0) - 1);
static unsigned int secDbKey = INIT_KEY;

namespace Auth {

SecurityDatabaseManagement::SecurityDatabaseManagement(Firebird::IPluginConfig* par)
	: database(0), transaction(0)
{
	Firebird::LocalStatus s;
	Firebird::CheckStatusWrapper statusWrapper(&s);
	config.assignRefNoIncr(par->getFirebirdConf(&statusWrapper));
	check(&s);
}

void SecurityDatabaseManagement::start(Firebird::CheckStatusWrapper* st, Firebird::ILogonInfo* logonInfo)
{
	try
	{
		st->init();

		if (secDbKey == INIT_KEY)
		{
			secDbKey = config->getKey("SecurityDatabase");
		}
		const char* secDbName = config->asString(secDbKey);

		if (!(secDbName && secDbName[0]))
		{
			Firebird::Arg::Gds(isc_secdb_name).raise();
		}

		Firebird::ClumpletWriter dpb(Firebird::ClumpletReader::dpbList, MAX_DPB_SIZE);
		dpb.insertByte(isc_dpb_sec_attach, TRUE);
		dpb.insertString(isc_dpb_config, Auth::ParsedList::getNonLoopbackProviders(secDbName));

		unsigned int authBlockSize;
		const unsigned char* authBlock = logonInfo->authBlock(&authBlockSize);

		if (authBlockSize)
			dpb.insertBytes(isc_dpb_auth_block, authBlock, authBlockSize);
		else
		{
			const char* logon = logonInfo->name();
			if (logon && logon[0])
				dpb.insertString(isc_dpb_trusted_auth, logon, fb_strlen(logon));
		}

		const char* role = logonInfo->role();
		if (role && role[0])
			dpb.insertString(isc_dpb_sql_role_name, role, fb_strlen(role));

		ISC_STATUS_ARRAY status;
		if (isc_attach_database(status, 0, secDbName, &database,
								dpb.getBufferLength(), reinterpret_cast<const char*>(dpb.getBuffer())))
		{
			Firebird::status_exception::raise(status);
		}

		if (isc_start_transaction(status, &transaction, 1, &database, 0, NULL))
		{
			Firebird::status_exception::raise(status);
		}
	}
	catch (const Firebird::Exception& ex)
	{
		ex.stuffException(st);
	}
}

void SecurityDatabaseManagement::commit(Firebird::CheckStatusWrapper* st)
{
	try
	{
		st->init();

		ISC_STATUS_ARRAY status;
		if (transaction)
		{
			if (isc_commit_transaction(status, &transaction))
			{
				Firebird::status_exception::raise(status);
			}
		}
	}
	catch (const Firebird::Exception& ex)
	{
		ex.stuffException(st);
	}
}

void SecurityDatabaseManagement::rollback(Firebird::CheckStatusWrapper* st)
{
	try
	{
		st->init();

		ISC_STATUS_ARRAY status;
		if (transaction)
		{
			if (isc_rollback_transaction(status, &transaction))
			{
				Firebird::status_exception::raise(status);
			}
		}
	}
	catch (const Firebird::Exception& ex)
	{
		ex.stuffException(st);
	}
}

int SecurityDatabaseManagement::release()
{
	if (--refCounter == 0)
	{
		ISC_STATUS_ARRAY status;
		if (transaction)
			isc_rollback_transaction(status, &transaction);
		if (database)
			isc_detach_database(status, &database);

		delete this;
		return 0;
	}

	return 1;
}

#define STR_STORE(to, from) strStore(to, from, sizeof(to))

static inline void strStore(char* to, const char* from, size_t len)
{
	to[--len] = '\0';
	strncpy(to, from, len);
}

int SecurityDatabaseManagement::execute(Firebird::CheckStatusWrapper* st, Firebird::IUser* user,
	Firebird::IListUsers* callback)
{
/*************************************
 *
 *	S E C U R I T Y _ e x e c _ l i n e
 *
 **************************************
 *
 * Functional description
 *	Process a command line for the security data base manager.
 *	This is used to add and delete users from the user information
 *	database (security2.fdb).   It also displays information
 *	about current users and allows modification of current
 *	users' parameters.
 *	Returns 0 on success, otherwise returns a Gsec message number
 *	and the status vector containing the error info.
 *	The syntax is:
 *
 *	Adding a new user:
 *
 *	    gsec -add <name> [ <parameter> ... ]    -- command line
 *	    add <name> [ <parameter> ... ]          -- interactive
 *
 *	Deleting a current user:
 *
 *	    gsec -delete <name>     -- command line
 *	    delete <name>           -- interactive
 *
 *	Displaying all current users:
 *
 *	    gsec -display           -- command line
 *	    display                 -- interactive
 *
 *	Displaying one user:
 *
 *	    gsec -display <name>    -- command line
 *	    display <name>          -- interactive
 *
 *	Modifying a user's parameters:
 *
 *	    gsec -modify <name> <parameter> [ <parameter> ... ] -- command line
 *	    modify <name> <parameter> [ <parameter> ... ]       -- interactive
 *
 *	Get help:
 *
 *	    gsec -help              -- command line
 *	    ?                       -- interactive
 *	    help                    -- interactive
 *
 *	Quit interactive session:
 *
 *	    quit                    -- interactive
 *
 *	where <parameter> can be one of:
 *
 *	    -uid <uid>
 *	    -gid <gid>
 *	    -fname <firstname>
 *	    -mname <middlename>
 *	    -lname <lastname>
 *
 **************************************/
	int ret = 0;

	try
	{
		ISC_STATUS_ARRAY isc_status;
		fb_utils::init_status(isc_status);
		st->init();

		Firebird::MutexLockGuard guard(execLineMutex, FB_FUNCTION);

		SCHAR encrypted1[MAX_LEGACY_PASSWORD_LENGTH + 2];
		Firebird::string encrypted2;
		bool found;

		// check for non-printable characters in user name
		for (const TEXT* p = user->userName()->get(); *p; p++)
		{
			if (!isprint(*p))
			{
				return GsecMsg75;  // Add special error message for this case ?
			}
		}

		isc_req_handle request = 0;
		isc_req_handle request2 = 0;

		switch (user->operation())
		{
		case Firebird::IUser::OP_USER_DROP_MAP:
		case Firebird::IUser::OP_USER_SET_MAP:
			{
				Firebird::string sql;
				sql.printf("ALTER ROLE " ADMIN_ROLE " %s AUTO ADMIN MAPPING",
					user->operation() == Firebird::IUser::OP_USER_SET_MAP ? "SET" : "DROP");
				isc_dsql_execute_immediate(isc_status, &database, &transaction, sql.length(), sql.c_str(), 1, NULL);
				if (isc_status[1] != 0)
				{
					ret = GsecMsg97;
				}
			}
			break;

		case Firebird::IUser::OP_USER_ADD:
			// this checks the "entered" flags for each parameter (except the name)
			// and makes all non-entered parameters null valued

			STORE (TRANSACTION_HANDLE transaction REQUEST_HANDLE request) U IN PLG$VIEW_USERS USING
				STR_STORE(U.PLG$USER_NAME, user->userName()->get());

				U.PLG$UID.NULL = ISC_TRUE;
				U.PLG$GID.NULL = ISC_TRUE;
				U.PLG$GROUP_NAME.NULL = ISC_TRUE;
				if (user->attributes()->entered())
				{
					ConfigFile attr(ConfigFile::USE_TEXT, user->attributes()->get());
					const ConfigFile::Parameter* p;

					if ((p = attr.findParameter("uid")) && p->value.hasData())
					{
						U.PLG$UID = p->asInteger();
						U.PLG$UID.NULL = ISC_FALSE;
					}

					if ((p = attr.findParameter("gid")) && p->value.hasData())
					{
						U.PLG$GID = p->asInteger();
						U.PLG$GID.NULL = ISC_FALSE;
					}

					if ((p = attr.findParameter("groupName")) && p->value.hasData())
					{
						STR_STORE(U.PLG$GROUP_NAME, p->value.c_str());
						U.PLG$GROUP_NAME.NULL = ISC_FALSE;
					}
				}

				if (user->password()->entered())
				{
					ENC_crypt(encrypted1, sizeof encrypted1, user->password()->get(), LEGACY_PASSWORD_SALT);
					LegacyHash::hash(encrypted2, user->userName()->get(), &encrypted1[2]);
					STR_STORE(U.PLG$PASSWD, encrypted2.c_str());
					U.PLG$PASSWD.NULL = ISC_FALSE;
				}
				else
					U.PLG$PASSWD.NULL = ISC_TRUE;
				if (user->firstName()->entered())
				{
					STR_STORE(U.PLG$FIRST_NAME, user->firstName()->get());
					U.PLG$FIRST_NAME.NULL = ISC_FALSE;
				}
				else
					U.PLG$FIRST_NAME.NULL = ISC_TRUE;
				if (user->middleName()->entered())
				{
					STR_STORE(U.PLG$MIDDLE_NAME, user->middleName()->get());
					U.PLG$MIDDLE_NAME.NULL = ISC_FALSE;
				}
				else
					U.PLG$MIDDLE_NAME.NULL = ISC_TRUE;
				if (user->lastName()->entered())
				{
					STR_STORE(U.PLG$LAST_NAME, user->lastName()->get());
					U.PLG$LAST_NAME.NULL = ISC_FALSE;
				}
				else
					U.PLG$LAST_NAME.NULL = ISC_TRUE;
			END_STORE
			ON_ERROR
				ret = GsecMsg19;	// gsec - add record error
			END_ERROR;
			if (ret == 0 && !grantRevokeAdmin(isc_status, database, transaction, user))
			{
				ret = GsecMsg19;	// gsec - add record error
			}
			break;

		case Firebird::IUser::OP_USER_MODIFY:
			// this updates an existing record, replacing all fields that are
			// entered, and for those that were specified but not entered, it
			// changes the current value to the null value

			found = false;
			FOR (TRANSACTION_HANDLE transaction REQUEST_HANDLE request) U IN PLG$VIEW_USERS
					WITH U.PLG$USER_NAME EQ user->userName()->get()
			{
				found = true;
				MODIFY U USING
					if (user->attributes()->entered())
					{
						U.PLG$UID.NULL = ISC_TRUE;
						U.PLG$GID.NULL = ISC_TRUE;
						U.PLG$GROUP_NAME.NULL = ISC_TRUE;

						ConfigFile attr(ConfigFile::USE_TEXT, user->attributes()->get());
						const ConfigFile::Parameter* p;

						if ((p = attr.findParameter("uid")) && p->value.hasData())
						{
							U.PLG$UID = p->asInteger();
							U.PLG$UID.NULL = ISC_FALSE;
						}

						if ((p = attr.findParameter("gid")) && p->value.hasData())
						{
							U.PLG$GID = p->asInteger();
							U.PLG$GID.NULL = ISC_FALSE;
						}

						if ((p = attr.findParameter("groupName")) && p->value.hasData())
						{
							STR_STORE(U.PLG$GROUP_NAME, p->value.c_str());
							U.PLG$GROUP_NAME.NULL = ISC_FALSE;
						}
					}
					else if (user->attributes()->specified())
					{
						U.PLG$UID.NULL = ISC_TRUE;
						U.PLG$GID.NULL = ISC_TRUE;
						U.PLG$GROUP_NAME.NULL = ISC_TRUE;
					}

					if (user->password()->entered())
					{
						ENC_crypt(encrypted1, sizeof encrypted1, user->password()->get(), LEGACY_PASSWORD_SALT);
						LegacyHash::hash(encrypted2, user->userName()->get(), &encrypted1[2]);
						STR_STORE(U.PLG$PASSWD, encrypted2.c_str());
						U.PLG$PASSWD.NULL = ISC_FALSE;
					}
					else if (user->password()->specified())
						U.PLG$PASSWD.NULL = ISC_TRUE;

					if (user->firstName()->entered())
					{
						STR_STORE(U.PLG$FIRST_NAME, user->firstName()->get());
						U.PLG$FIRST_NAME.NULL = ISC_FALSE;
					}
					else if (user->firstName()->specified())
						U.PLG$FIRST_NAME.NULL = ISC_TRUE;

					if (user->middleName()->entered())
					{
						STR_STORE(U.PLG$MIDDLE_NAME, user->middleName()->get());
						U.PLG$MIDDLE_NAME.NULL = ISC_FALSE;
					}
					else if (user->middleName()->specified())
						U.PLG$MIDDLE_NAME.NULL = ISC_TRUE;

					if (user->lastName()->entered())
					{
						STR_STORE(U.PLG$LAST_NAME, user->lastName()->get());
						U.PLG$LAST_NAME.NULL = ISC_FALSE;
					}
					else if (user->lastName()->specified())
						U.PLG$LAST_NAME.NULL = ISC_TRUE;
				END_MODIFY
				ON_ERROR
					ret = GsecMsg20;
				END_ERROR;
			}
			END_FOR
			ON_ERROR
				ret = GsecMsg21;
			END_ERROR;

			if (!ret && !found)
				ret = GsecMsg22;

			if (ret == 0 && !grantRevokeAdmin(isc_status, database, transaction, user))
			{
				ret = GsecMsg21;
			}
			break;

		case Firebird::IUser::OP_USER_DELETE:
			// looks up the specified user record and deletes it

			found = false;
			// Do not allow SYSDBA user to be deleted
			if (!fb_utils::stricmp(user->userName()->get(), SYSDBA_USER_NAME))
				ret = GsecMsg23;
			else
			{
				FOR (TRANSACTION_HANDLE transaction REQUEST_HANDLE request) U IN PLG$VIEW_USERS
						WITH U.PLG$USER_NAME EQ user->userName()->get()
				{
					found = true;
					ERASE U
					ON_ERROR
						ret = GsecMsg23;	// gsec - delete record error
					END_ERROR;
				}
				END_FOR
				ON_ERROR
					ret = GsecMsg24;	// gsec - find/delete record error
				END_ERROR;
			}

			if (!ret && !found)
				ret = GsecMsg22;	// gsec - record not found for user:

			user->admin()->set(st, 0);
			check(st);
			user->admin()->setEntered(st, 1);
			check(st);
			if (ret == 0 && !grantRevokeAdmin(isc_status, database, transaction, user))
			{
				ret = GsecMsg24;
			}
			break;

		case Firebird::IUser::OP_USER_DISPLAY:
			// gets either the desired record, or all records, and displays them

			found = false;
			if (!user->userName()->entered())
			{
				Firebird::LocalStatus s2;
				Firebird::CheckStatusWrapper statusWrapper2(&s2);
				Firebird::CheckStatusWrapper* s = st;

				FOR (TRANSACTION_HANDLE transaction REQUEST_HANDLE request) U IN PLG$VIEW_USERS
					try
					{
						{
							Firebird::string attr, a1, a2, a3;

							if (!U.PLG$UID.NULL)
								a1.printf("Uid=%d\n", U.PLG$UID);

							if (!U.PLG$GID.NULL)
								a2.printf("Gid=%d\n", U.PLG$GID);

							if (!U.PLG$GROUP_NAME.NULL)
								a3.printf("GroupName=%s\n", U.PLG$GROUP_NAME);

							attr = a1 + a2 + a3;
							user->attributes()->set(s, attr.c_str());
							check(s);
							user->attributes()->setEntered(s, attr.hasData() ? 1 : 0);
							check(s);
						}

						user->userName()->set(s, U.PLG$USER_NAME);
						check(s);
						user->userName()->setEntered(s, U.PLG$USER_NAME.NULL ? 0 : 1);
						check(s);
						user->password()->set(s, "");
						check(s);
						user->password()->setEntered(s, 0);
						check(s);
						user->firstName()->set(s, U.PLG$FIRST_NAME);
						check(s);
						user->firstName()->setEntered(s, U.PLG$FIRST_NAME.NULL ? 0 : 1);
						check(s);
						user->middleName()->set(s, U.PLG$MIDDLE_NAME);
						check(s);
						user->middleName()->setEntered(s, U.PLG$MIDDLE_NAME.NULL ? 0 : 1);
						check(s);
						user->lastName()->set(s, U.PLG$LAST_NAME);
						check(s);
						user->lastName()->setEntered(s, U.PLG$LAST_NAME.NULL ? 0 : 1);
						check(s);

						user->admin()->set(s, 0);
						check(s);
						user->admin()->setEntered(s, 1);
						check(s);

						FOR (TRANSACTION_HANDLE transaction REQUEST_HANDLE request2)
							 P IN RDB$USER_PRIVILEGES
							 WITH P.RDB$USER EQ U.PLG$USER_NAME AND
								  P.RDB$RELATION_NAME EQ 'RDB$ADMIN' AND
								  P.RDB$PRIVILEGE EQ 'M'
						{
							user->admin()->set(s, 1);
						}
						END_FOR
						check(s);

						callback->list(s, user);
						check(s);

						found = true;
					}
					catch (const Firebird::Exception& ex)
					{
					  	ex.stuffException(s);
					  	s = &statusWrapper2;
					}
				END_FOR
				ON_ERROR
					ret = GsecMsg28;	// gsec - find/display record error
				END_ERROR;

				// real error raise - out of gpre's FOR loop
				check(st);
			}
			else
			{
				Firebird::string attr, a1, a2, a3;
				Firebird::LocalStatus s2;
				Firebird::CheckStatusWrapper statusWrapper2(&s2);
				Firebird::CheckStatusWrapper* s = st;

				FOR (TRANSACTION_HANDLE transaction REQUEST_HANDLE request) U IN PLG$VIEW_USERS
						WITH U.PLG$USER_NAME EQ user->userName()->get()

					try
					{
						if (!U.PLG$UID.NULL)
							a1.printf("Uid=%d\n", U.PLG$UID);

						if (!U.PLG$GID.NULL)
							a2.printf("Gid=%d\n", U.PLG$GID);

						if (!U.PLG$GROUP_NAME.NULL)
							a3.printf("GroupName=%s\n", U.PLG$GROUP_NAME);

						attr = a1 + a2 + a3;
						user->attributes()->set(s, attr.c_str());
						check(s);
						user->attributes()->setEntered(s, attr.hasData() ? 1 : 0);
						check(s);

						user->userName()->set(s, U.PLG$USER_NAME);
						check(s);
						user->userName()->setEntered(s, U.PLG$USER_NAME.NULL ? 0 : 1);
						check(s);
						user->password()->set(s, "");
						check(s);
						user->password()->setEntered(s, 0);
						check(s);
						user->firstName()->set(s, U.PLG$FIRST_NAME);
						check(s);
						user->firstName()->setEntered(s, U.PLG$FIRST_NAME.NULL ? 0 : 1);
						check(s);
						user->middleName()->set(s, U.PLG$MIDDLE_NAME);
						check(s);
						user->middleName()->setEntered(s, U.PLG$MIDDLE_NAME.NULL ? 0 : 1);
						check(s);
						user->lastName()->set(s, U.PLG$LAST_NAME);
						check(s);
						user->lastName()->setEntered(s, U.PLG$LAST_NAME.NULL ? 0 : 1);
						check(s);

						user->admin()->set(s, 0);
						check(s);
						user->admin()->setEntered(s, 1);
						check(s);

						FOR (TRANSACTION_HANDLE transaction REQUEST_HANDLE request2)
							 P IN RDB$USER_PRIVILEGES
							 WITH P.RDB$USER EQ U.PLG$USER_NAME AND
								  P.RDB$RELATION_NAME EQ 'RDB$ADMIN' AND
								  P.RDB$PRIVILEGE EQ 'M'
						{
							user->admin()->set(s, 1);
						}
						END_FOR
						check(s);

						callback->list(s, user);
						check(s);

						found = true;
					}
					catch (const Firebird::Exception& ex)
					{
					  	ex.stuffException(s);
					  	s = &statusWrapper2;
					}
				END_FOR
				ON_ERROR
					ret = GsecMsg28;	// gsec - find/display record error
				END_ERROR;

				// real error raise - out of gpre's FOR loop
				check(st);
			}
			break;

		default:
			ret = GsecMsg16;		// gsec - error in switch specifications
			break;
		}

		if (request)
		{
			ISC_STATUS_ARRAY s;
			if (isc_release_request(s, &request) != FB_SUCCESS)
			{
				if (! ret)
				{
					ret = GsecMsg94;	// error releasing request in security database
				}
			}
		}

		if (request2)
		{
			ISC_STATUS_ARRAY s;
			if (isc_release_request(s, &request2) != FB_SUCCESS)
			{
				if (! ret)
				{
					ret = GsecMsg94;	// error releasing request in security database
				}
			}
		}

		fb_utils::setIStatus(st, isc_status);
	}
	catch (const Firebird::Exception& ex)
	{
		ex.stuffException(st);
	}

	return ret;
}

} // namespace Auth

// register plugin
static Firebird::SimpleFactory<Auth::SecurityDatabaseManagement> factory;

extern "C" FB_DLL_EXPORT void FB_PLUGIN_ENTRY_POINT(Firebird::IMaster* master)
{
	Firebird::CachedMasterInterface::set(master);
	Firebird::PluginManagerInterfacePtr()->registerPluginFactory(
		Firebird::IPluginManager::TYPE_AUTH_USER_MANAGEMENT, "Legacy_UserManager", &factory);
	Firebird::getUnloadDetector()->registerMe();
}
