21 changes: 21 additions & 0 deletions src/database/database-files.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
// for player files

#include "database.h"
#include <unordered_map>

class PlayerDatabaseFiles : public PlayerDatabase
{
Expand All @@ -41,3 +42,23 @@ class PlayerDatabaseFiles : public PlayerDatabase

std::string m_savedir;
};

class AuthDatabaseFiles : public AuthDatabase
{
public:
AuthDatabaseFiles(const std::string &savedir);
virtual ~AuthDatabaseFiles() = default;

virtual bool getAuth(const std::string &name, AuthEntry &res);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be a const method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried, and it's tricky. Any call will also call verifyDatabase, which will potentially create the database, flag everything as m_initialized etc.

Yes, I could mark any such things (the statements, too, are changed during reads until a reset is called) as volatile, but then I'd have to back-patch this into the map and player databases too; none of which, by the way, have const methods. Is this really that critical?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not critical at all, just something I noticed.

virtual bool saveAuth(const AuthEntry &authEntry);
virtual bool createAuth(AuthEntry &authEntry);
virtual bool deleteAuth(const std::string &name);
virtual void listNames(std::vector<std::string> &res);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

virtual void reload();

private:
std::unordered_map<std::string, AuthEntry> m_auth_list;
std::string m_savedir;
bool readAuthFile();
bool writeAuthFile();
};
167 changes: 167 additions & 0 deletions src/database/database-sqlite3.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -606,3 +606,170 @@ void PlayerDatabaseSQLite3::listPlayers(std::vector<std::string> &res)

sqlite3_reset(m_stmt_player_list);
}

/*
* Auth database
*/

AuthDatabaseSQLite3::AuthDatabaseSQLite3(const std::string &savedir) :
Database_SQLite3(savedir, "auth"), AuthDatabase()
{
}

AuthDatabaseSQLite3::~AuthDatabaseSQLite3()
{
FINALIZE_STATEMENT(m_stmt_read)
FINALIZE_STATEMENT(m_stmt_write)
FINALIZE_STATEMENT(m_stmt_create)
FINALIZE_STATEMENT(m_stmt_delete)
FINALIZE_STATEMENT(m_stmt_list_names)
FINALIZE_STATEMENT(m_stmt_read_privs)
FINALIZE_STATEMENT(m_stmt_write_privs)
FINALIZE_STATEMENT(m_stmt_delete_privs)
FINALIZE_STATEMENT(m_stmt_last_insert_rowid)
}

void AuthDatabaseSQLite3::createDatabase()
{
assert(m_database); // Pre-condition

SQLOK(sqlite3_exec(m_database,
"CREATE TABLE IF NOT EXISTS `auth` ("
"`id` INTEGER PRIMARY KEY AUTOINCREMENT,"
"`name` VARCHAR(32) UNIQUE,"
"`password` VARCHAR(512),"
"`last_login` INTEGER"
");",
NULL, NULL, NULL),
"Failed to create auth table");

SQLOK(sqlite3_exec(m_database,
"CREATE TABLE IF NOT EXISTS `user_privileges` ("
"`id` INTEGER,"
"`privilege` VARCHAR(32),"
"PRIMARY KEY (id, privilege)"
"CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES auth (id) ON DELETE CASCADE"
");",
NULL, NULL, NULL),
"Failed to create auth privileges table");
}

void AuthDatabaseSQLite3::initStatements()
{
PREPARE_STATEMENT(read, "SELECT id, name, password, last_login FROM auth WHERE name = ?");
PREPARE_STATEMENT(write, "UPDATE auth set name = ?, password = ?, last_login = ? WHERE id = ?");
PREPARE_STATEMENT(create, "INSERT INTO auth (name, password, last_login) VALUES (?, ?, ?)");
PREPARE_STATEMENT(delete, "DELETE FROM auth WHERE name = ?");

PREPARE_STATEMENT(list_names, "SELECT name FROM auth ORDER BY name DESC");

PREPARE_STATEMENT(read_privs, "SELECT privilege FROM user_privileges WHERE id = ?");
PREPARE_STATEMENT(write_privs, "INSERT OR IGNORE INTO user_privileges (id, privilege) VALUES (?, ?)");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't REPLACE INTO have the same effect?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a NOOP is better in this case than rewriting the row as there is no other column to alter than the two in the query

PREPARE_STATEMENT(delete_privs, "DELETE FROM user_privileges WHERE id = ?");

PREPARE_STATEMENT(last_insert_rowid, "SELECT last_insert_rowid()");
}

bool AuthDatabaseSQLite3::getAuth(const std::string &name, AuthEntry &res)
{
verifyDatabase();
str_to_sqlite(m_stmt_read, 1, name);
if (sqlite3_step(m_stmt_read) != SQLITE_ROW) {
sqlite3_reset(m_stmt_read);
return false;
}
res.id = sqlite_to_uint(m_stmt_read, 0);
res.name = sqlite_to_string(m_stmt_read, 1);
res.password = sqlite_to_string(m_stmt_read, 2);
res.last_login = sqlite_to_int64(m_stmt_read, 3);
sqlite3_reset(m_stmt_read);

int64_to_sqlite(m_stmt_read_privs, 1, res.id);
while (sqlite3_step(m_stmt_read_privs) == SQLITE_ROW) {
res.privileges.emplace_back(sqlite_to_string(m_stmt_read_privs, 0));
}
sqlite3_reset(m_stmt_read_privs);

return true;
}

bool AuthDatabaseSQLite3::saveAuth(const AuthEntry &authEntry)
{
beginSave();

str_to_sqlite(m_stmt_write, 1, authEntry.name);
str_to_sqlite(m_stmt_write, 2, authEntry.password);
int64_to_sqlite(m_stmt_write, 3, authEntry.last_login);
int64_to_sqlite(m_stmt_write, 4, authEntry.id);
sqlite3_vrfy(sqlite3_step(m_stmt_write), SQLITE_DONE);
sqlite3_reset(m_stmt_write);

writePrivileges(authEntry);

endSave();
return true;
}

bool AuthDatabaseSQLite3::createAuth(AuthEntry &authEntry)
{
beginSave();

// id autoincrements
str_to_sqlite(m_stmt_create, 1, authEntry.name);
str_to_sqlite(m_stmt_create, 2, authEntry.password);
int64_to_sqlite(m_stmt_create, 3, authEntry.last_login);
sqlite3_vrfy(sqlite3_step(m_stmt_create), SQLITE_DONE);
sqlite3_reset(m_stmt_create);

// obtain id and write back to original authEntry
sqlite3_step(m_stmt_last_insert_rowid);
authEntry.id = sqlite_to_uint(m_stmt_last_insert_rowid, 0);
sqlite3_reset(m_stmt_last_insert_rowid);

writePrivileges(authEntry);

endSave();
return true;
}

bool AuthDatabaseSQLite3::deleteAuth(const std::string &name)
{
verifyDatabase();

str_to_sqlite(m_stmt_delete, 1, name);
sqlite3_vrfy(sqlite3_step(m_stmt_delete), SQLITE_DONE);
int changes = sqlite3_changes(m_database);
sqlite3_reset(m_stmt_delete);

// privileges deleted by foreign key on delete cascade
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice :)


return changes > 0;
}

void AuthDatabaseSQLite3::listNames(std::vector<std::string> &res)
{
verifyDatabase();

while (sqlite3_step(m_stmt_list_names) == SQLITE_ROW) {
res.push_back(sqlite_to_string(m_stmt_list_names, 0));
}
sqlite3_reset(m_stmt_list_names);
}

void AuthDatabaseSQLite3::reload()
{
// noop for SQLite
}

void AuthDatabaseSQLite3::writePrivileges(const AuthEntry &authEntry)
{
int64_to_sqlite(m_stmt_delete_privs, 1, authEntry.id);
sqlite3_vrfy(sqlite3_step(m_stmt_delete_privs), SQLITE_DONE);
sqlite3_reset(m_stmt_delete_privs);
for (const std::string &privilege : authEntry.privileges) {
int64_to_sqlite(m_stmt_write_privs, 1, authEntry.id);
str_to_sqlite(m_stmt_write_privs, 2, privilege);
sqlite3_vrfy(sqlite3_step(m_stmt_write_privs), SQLITE_DONE);
sqlite3_reset(m_stmt_write_privs);
}
}
41 changes: 41 additions & 0 deletions src/database/database-sqlite3.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ class Database_SQLite3 : public Database
return (u32) sqlite3_column_int(s, iCol);
}

inline s64 sqlite_to_int64(sqlite3_stmt *s, int iCol)
{
return (s64) sqlite3_column_int64(s, iCol);
}

inline u64 sqlite_to_uint64(sqlite3_stmt *s, int iCol)
{
return (u64) sqlite3_column_int64(s, iCol);
}

inline float sqlite_to_float(sqlite3_stmt *s, int iCol)
{
return (float) sqlite3_column_double(s, iCol);
Expand Down Expand Up @@ -191,3 +201,34 @@ class PlayerDatabaseSQLite3 : private Database_SQLite3, public PlayerDatabase
sqlite3_stmt *m_stmt_player_metadata_remove = nullptr;
sqlite3_stmt *m_stmt_player_metadata_add = nullptr;
};

class AuthDatabaseSQLite3 : private Database_SQLite3, public AuthDatabase
{
public:
AuthDatabaseSQLite3(const std::string &savedir);
virtual ~AuthDatabaseSQLite3();

virtual bool getAuth(const std::string &name, AuthEntry &res);
virtual bool saveAuth(const AuthEntry &authEntry);
virtual bool createAuth(AuthEntry &authEntry);
virtual bool deleteAuth(const std::string &name);
virtual void listNames(std::vector<std::string> &res);
virtual void reload();

protected:
virtual void createDatabase();
virtual void initStatements();

private:
virtual void writePrivileges(const AuthEntry &authEntry);

sqlite3_stmt *m_stmt_read = nullptr;
sqlite3_stmt *m_stmt_write = nullptr;
sqlite3_stmt *m_stmt_create = nullptr;
sqlite3_stmt *m_stmt_delete = nullptr;
sqlite3_stmt *m_stmt_list_names = nullptr;
sqlite3_stmt *m_stmt_read_privs = nullptr;
sqlite3_stmt *m_stmt_write_privs = nullptr;
sqlite3_stmt *m_stmt_delete_privs = nullptr;
sqlite3_stmt *m_stmt_last_insert_rowid = nullptr;
};
23 changes: 23 additions & 0 deletions src/database/database.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

#pragma once

#include <set>
#include <string>
#include <vector>
#include "irr_v3d.h"
Expand Down Expand Up @@ -61,3 +62,25 @@ class PlayerDatabase
virtual bool removePlayer(const std::string &name) = 0;
virtual void listPlayers(std::vector<std::string> &res) = 0;
};

struct AuthEntry
{
u64 id;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just an implementation detail of the sqlite db, but I guess it can't be avoided to have it in here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably could, now that names are UNIQUE, but I've been asked to keep a numerical id anyway.

It may make a difference for future PostgreSQL implementations.

std::string name;
std::string password;
std::vector<std::string> privileges;
s64 last_login;
};

class AuthDatabase
{
public:
virtual ~AuthDatabase() = default;

virtual bool getAuth(const std::string &name, AuthEntry &res) = 0;
virtual bool saveAuth(const AuthEntry &authEntry) = 0;
virtual bool createAuth(AuthEntry &authEntry) = 0;
virtual bool deleteAuth(const std::string &name) = 0;
virtual void listNames(std::vector<std::string> &res) = 0;
virtual void reload() = 0;
};
5 changes: 5 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ static void set_allowed_options(OptionList *allowed_options)
_("Migrate from current map backend to another (Only works when using minetestserver or with --server)"))));
allowed_options->insert(std::make_pair("migrate-players", ValueSpec(VALUETYPE_STRING,
_("Migrate from current players backend to another (Only works when using minetestserver or with --server)"))));
allowed_options->insert(std::make_pair("migrate-auth", ValueSpec(VALUETYPE_STRING,
_("Migrate from current auth backend to another (Only works when using minetestserver or with --server)"))));
allowed_options->insert(std::make_pair("terminal", ValueSpec(VALUETYPE_FLAG,
_("Feature an interactive terminal (Only works when using minetestserver or with --server)"))));
#ifndef SERVER
Expand Down Expand Up @@ -840,6 +842,9 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
if (cmd_args.exists("migrate-players"))
return ServerEnvironment::migratePlayersDatabase(game_params, cmd_args);

if (cmd_args.exists("migrate-auth"))
return ServerEnvironment::migrateAuthDatabase(game_params, cmd_args);

if (cmd_args.exists("terminal")) {
#if USE_CURSES
bool name_ok = true;
Expand Down
1 change: 1 addition & 0 deletions src/script/lua_api/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
set(common_SCRIPT_LUA_API_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/l_areastore.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_auth.cpp
Copy link
Member

@SmallJoker SmallJoker May 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, but I have not been able to successfully build on Android yet, so I can't test this.

I see that not all unit test files are listed. Is this on purpose?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably it compiled fine on Android so nobody noticed that they were forgotten - or it's on purpose and the documentation is missing. 🤷‍♂️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

afaik the missing unit test files are on purpose

${CMAKE_CURRENT_SOURCE_DIR}/l_base.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_craft.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_env.cpp
Expand Down
216 changes: 216 additions & 0 deletions src/script/lua_api/l_auth.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/*
Minetest
Copyright (C) 2018 bendeutsch, Ben Deutsch <ben@bendeutsch.de>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
You should have received a copy of the GNU Lesser 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.
*/

#include "lua_api/l_auth.h"
#include "lua_api/l_internal.h"
#include "common/c_converter.h"
#include "common/c_content.h"
#include "cpp_api/s_base.h"
#include "server.h"
#include "environment.h"
#include "database/database.h"
#include <algorithm>

// common start: ensure auth db
AuthDatabase *ModApiAuth::getAuthDb(lua_State *L)
{
ServerEnvironment *server_environment =
dynamic_cast<ServerEnvironment *>(getEnv(L));
if (!server_environment)
return nullptr;
return server_environment->getAuthDatabase();
}

void ModApiAuth::pushAuthEntry(lua_State *L, const AuthEntry &authEntry)
{
lua_newtable(L);
int table = lua_gettop(L);
// id
lua_pushnumber(L, authEntry.id);
lua_setfield(L, table, "id");
// name
lua_pushstring(L, authEntry.name.c_str());
lua_setfield(L, table, "name");
// password
lua_pushstring(L, authEntry.password.c_str());
lua_setfield(L, table, "password");
// privileges
lua_newtable(L);
int privtable = lua_gettop(L);
for (const std::string &privs : authEntry.privileges) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be auto

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue that in this case, writing out the actual type aids clarity (the '.c_str' method is used two lines below).

lua_pushboolean(L, true);
lua_setfield(L, privtable, privs.c_str());
}
lua_setfield(L, table, "privileges");
// last_login
lua_pushnumber(L, authEntry.last_login);
lua_setfield(L, table, "last_login");

lua_pushvalue(L, table);
}

// auth_read(name)
int ModApiAuth::l_auth_read(lua_State *L)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions need to be documented.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, they're exposed via a core.auth object, as per a previous request. In addition, they're not supposed to be accessed by mod authors… I mean, I never thought of them as… huh.

Maybe it does make sense for mods to access this? Even though it exposes (hashed) passwords? Then documentation is in order. Otherwise, how do I let this be used by builtin/game/auth.lua, but nothing else?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to restrict it to builtin only, then you can copy the functions to a new table and delete them afterwards (i.e. = nil in the core table). Indeed, probably it's best to keep them away from mods, since the auth system is already available to them.

Copy link
Contributor Author

@bendeutsch bendeutsch Jun 1, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll do that. Where should the documentation of the (then private) object+methods go? Is l_auth.{h,cpp} ok?

Update: Done, it's local core_auth now. Still seems to work. I've looked at the other script/lua_api files and could not find much more docs than what I have. Any suggestions?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bendeutsch The function arguments are already mentioned there in the comments. When the functions are unavailable to modders, this does not seem to need more documentation.

{
NO_MAP_LOCK_REQUIRED;
AuthDatabase *auth_db = getAuthDb(L);
if (!auth_db)
return 0;
AuthEntry authEntry;
const char *name = luaL_checkstring(L, 1);
bool success = auth_db->getAuth(std::string(name), authEntry);
if (!success)
return 0;

pushAuthEntry(L, authEntry);
return 1;
}

// auth_save(table)
int ModApiAuth::l_auth_save(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
AuthDatabase *auth_db = getAuthDb(L);
if (!auth_db)
return 0;
luaL_checktype(L, 1, LUA_TTABLE);
int table = 1;
AuthEntry authEntry;
bool success;
success = getintfield(L, table, "id", authEntry.id);
success = success && getstringfield(L, table, "name", authEntry.name);
success = success && getstringfield(L, table, "password", authEntry.password);
lua_getfield(L, table, "privileges");
if (lua_istable(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2)) {
authEntry.privileges.emplace_back(
lua_tostring(L, -2)); // the key, not the value
lua_pop(L, 1);
}
} else {
success = false;
}
lua_pop(L, 1); // the table
success = success && getintfield(L, table, "last_login", authEntry.last_login);

if (!success) {
lua_pushboolean(L, false);
return 1;
}

lua_pushboolean(L, auth_db->saveAuth(authEntry));
return 1;
}

// auth_create(table)
int ModApiAuth::l_auth_create(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
AuthDatabase *auth_db = getAuthDb(L);
if (!auth_db)
return 0;
luaL_checktype(L, 1, LUA_TTABLE);
int table = 1;
AuthEntry authEntry;
bool success;
// no meaningful id field, we assume
success = getstringfield(L, table, "name", authEntry.name);
success = success && getstringfield(L, table, "password", authEntry.password);
lua_getfield(L, table, "privileges");
if (lua_istable(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2)) {
authEntry.privileges.emplace_back(
lua_tostring(L, -2)); // the key, not the value
lua_pop(L, 1);
}
} else {
success = false;
}
lua_pop(L, 1); // the table
success = success && getintfield(L, table, "last_login", authEntry.last_login);

if (!success)
return 0;

if (auth_db->createAuth(authEntry)) {
pushAuthEntry(L, authEntry);
return 1;
}

return 0;
}

// auth_delete(name)
int ModApiAuth::l_auth_delete(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
AuthDatabase *auth_db = getAuthDb(L);
if (!auth_db)
return 0;
std::string name(luaL_checkstring(L, 1));
lua_pushboolean(L, auth_db->deleteAuth(name));
return 1;
}

// auth_list_names()
int ModApiAuth::l_auth_list_names(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
AuthDatabase *auth_db = getAuthDb(L);
if (!auth_db)
return 0;
std::vector<std::string> names;
auth_db->listNames(names);
lua_createtable(L, names.size(), 0);
int table = lua_gettop(L);
int i = 1;
for (const std::string &name : names) {
lua_pushstring(L, name.c_str());
lua_rawseti(L, table, i++);
}
return 1;
}

// auth_reload()
int ModApiAuth::l_auth_reload(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
AuthDatabase *auth_db = getAuthDb(L);
if (auth_db)
auth_db->reload();
return 0;
}

void ModApiAuth::Initialize(lua_State *L, int top)
{

lua_newtable(L);
int auth_top = lua_gettop(L);

registerFunction(L, "read", l_auth_read, auth_top);
registerFunction(L, "save", l_auth_save, auth_top);
registerFunction(L, "create", l_auth_create, auth_top);
registerFunction(L, "delete", l_auth_delete, auth_top);
registerFunction(L, "list_names", l_auth_list_names, auth_top);
registerFunction(L, "reload", l_auth_reload, auth_top);

lua_setfield(L, top, "auth");
}
54 changes: 54 additions & 0 deletions src/script/lua_api/l_auth.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
Minetest
Copyright (C) 2018 bendeutsch, Ben Deutsch <ben@bendeutsch.de>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
You should have received a copy of the GNU Lesser 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.
*/

#pragma once

#include "lua_api/l_base.h"

class AuthDatabase;
struct AuthEntry;

class ModApiAuth : public ModApiBase
{
private:
// auth_read(name)
static int l_auth_read(lua_State *L);

// auth_save(table)
static int l_auth_save(lua_State *L);

// auth_create(table)
static int l_auth_create(lua_State *L);

// auth_delete(name)
static int l_auth_delete(lua_State *L);

// auth_list_names()
static int l_auth_list_names(lua_State *L);

// auth_reload()
static int l_auth_reload(lua_State *L);

// helper for auth* methods
static AuthDatabase *getAuthDb(lua_State *L);
static void pushAuthEntry(lua_State *L, const AuthEntry &authEntry);

public:
static void Initialize(lua_State *L, int top);
};
2 changes: 2 additions & 0 deletions src/script/scripting_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "settings.h"
#include "cpp_api/s_internal.h"
#include "lua_api/l_areastore.h"
#include "lua_api/l_auth.h"
#include "lua_api/l_base.h"
#include "lua_api/l_craft.h"
#include "lua_api/l_env.h"
Expand Down Expand Up @@ -106,6 +107,7 @@ void ServerScripting::InitializeModApi(lua_State *L, int top)
ModChannelRef::Register(L);

// Initialize mod api modules
ModApiAuth::Initialize(L, top);
ModApiCraft::Initialize(L, top);
ModApiEnvMod::Initialize(L, top);
ModApiInventory::Initialize(L, top);
Expand Down
101 changes: 101 additions & 0 deletions src/serverenvironment.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,18 @@ ServerEnvironment::ServerEnvironment(ServerMap *map,
std::string name;
conf.getNoEx("player_backend", name);
m_player_database = openPlayerDatabase(name, path_world, conf);

std::string auth_name = "files";
if (conf.exists("auth_backend")) {
conf.getNoEx("auth_backend", auth_name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this even work without a &?

Copy link
Contributor Author

@bendeutsch bendeutsch May 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getNoEx method is defined as

bool getNoEx(const std::string &name, std::string &val) const;

The second parameter is pass-by-reference, and not const, so it can be assigned to.

It also works for the player backend a few lines above, which already existed in code.

} else {
conf.set("auth_backend", "files");
if (!conf.updateConfigFile(conf_path.c_str())) {
errorstream << "ServerEnvironment::ServerEnvironment(): "
<< "Failed to update world.mt!" << std::endl;
}
}
m_auth_database = openAuthDatabase(auth_name, path_world, conf);
}

ServerEnvironment::~ServerEnvironment()
Expand All @@ -439,6 +451,7 @@ ServerEnvironment::~ServerEnvironment()
}

delete m_player_database;
delete m_auth_database;
}

Map & ServerEnvironment::getMap()
Expand Down Expand Up @@ -2274,3 +2287,91 @@ bool ServerEnvironment::migratePlayersDatabase(const GameParams &game_params,
}
return true;
}

AuthDatabase *ServerEnvironment::openAuthDatabase(
const std::string &name, const std::string &savedir, const Settings &conf)
{
if (name == "sqlite3")
return new AuthDatabaseSQLite3(savedir);

if (name == "files")
return new AuthDatabaseFiles(savedir);

throw BaseException(std::string("Database backend ") + name + " not supported.");
}

bool ServerEnvironment::migrateAuthDatabase(
const GameParams &game_params, const Settings &cmd_args)
{
std::string migrate_to = cmd_args.get("migrate-auth");
Settings world_mt;
std::string world_mt_path = game_params.world_path + DIR_DELIM + "world.mt";
if (!world_mt.readConfigFile(world_mt_path.c_str())) {
errorstream << "Cannot read world.mt!" << std::endl;
return false;
}

std::string backend = "files";
if (world_mt.exists("auth_backend"))
backend = world_mt.get("auth_backend");
else
warningstream << "No auth_backend found in world.mt, "
"assuming \"files\"." << std::endl;

if (backend == migrate_to) {
errorstream << "Cannot migrate: new backend is same"
<< " as the old one" << std::endl;
return false;
}

try {
const std::unique_ptr<AuthDatabase> srcdb(ServerEnvironment::openAuthDatabase(
backend, game_params.world_path, world_mt));
const std::unique_ptr<AuthDatabase> dstdb(ServerEnvironment::openAuthDatabase(
migrate_to, game_params.world_path, world_mt));

std::vector<std::string> names_list;
srcdb->listNames(names_list);
for (const std::string &name : names_list) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const auto

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, as above, I would keep it to help future readers better understand the code.

actionstream << "Migrating auth entry for " << name << std::endl;
bool success;
AuthEntry authEntry;
success = srcdb->getAuth(name, authEntry);
success = success && dstdb->createAuth(authEntry);
if (!success)
errorstream << "Failed to migrate " << name << std::endl;
}

actionstream << "Successfully migrated " << names_list.size()
<< " auth entries" << std::endl;
world_mt.set("auth_backend", migrate_to);
if (!world_mt.updateConfigFile(world_mt_path.c_str()))
errorstream << "Failed to update world.mt!" << std::endl;
else
actionstream << "world.mt updated" << std::endl;

if (backend == "files") {
// special-case files migration:
// move auth.txt to auth.txt.bak if possible
std::string auth_txt_path =
game_params.world_path + DIR_DELIM + "auth.txt";
std::string auth_bak_path = auth_txt_path + ".bak";
if (!fs::PathExists(auth_bak_path))
if (fs::Rename(auth_txt_path, auth_bak_path))
actionstream << "Renamed auth.txt to auth.txt.bak"
<< std::endl;
else
errorstream << "Could not rename auth.txt to "
"auth.txt.bak" << std::endl;
else
warningstream << "auth.txt.bak already exists, auth.txt "
"not renamed" << std::endl;
}

} catch (BaseException &e) {
errorstream << "An error occured during migration: " << e.what()
<< std::endl;
return false;
}
return true;
}
8 changes: 8 additions & 0 deletions src/serverenvironment.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ struct GameParams;
class MapBlock;
class RemotePlayer;
class PlayerDatabase;
class AuthDatabase;
class PlayerSAO;
class ServerEnvironment;
class ActiveBlockModifier;
Expand Down Expand Up @@ -366,6 +367,10 @@ class ServerEnvironment : public Environment

static bool migratePlayersDatabase(const GameParams &game_params,
const Settings &cmd_args);

AuthDatabase *getAuthDatabase() { return m_auth_database; }
static bool migrateAuthDatabase(const GameParams &game_params,
const Settings &cmd_args);
private:

/**
Expand All @@ -375,6 +380,8 @@ class ServerEnvironment : public Environment

static PlayerDatabase *openPlayerDatabase(const std::string &name,
const std::string &savedir, const Settings &conf);
static AuthDatabase *openAuthDatabase(const std::string &name,
const std::string &savedir, const Settings &conf);
/*
Internal ActiveObject interface
-------------------------------------------
Expand Down Expand Up @@ -467,6 +474,7 @@ class ServerEnvironment : public Environment
std::vector<RemotePlayer*> m_players;

PlayerDatabase *m_player_database = nullptr;
AuthDatabase *m_auth_database = nullptr;

// Particles
IntervalLimiter m_particle_management_interval;
Expand Down
1 change: 1 addition & 0 deletions src/unittest/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
set (UNITTEST_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_authdatabase.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_activeobject.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_areastore.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_ban.cpp
Expand Down
299 changes: 299 additions & 0 deletions src/unittest/test_authdatabase.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
/*
Minetest
Copyright (C) 2018 bendeutsch, Ben Deutsch <ben@bendeutsch.de>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 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 Lesser General Public License for more details.
You should have received a copy of the GNU Lesser 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.
*/

#include "test.h"

#include <algorithm>
#include "database/database-files.h"
#include "database/database-sqlite3.h"
#include "util/string.h"
#include "filesys.h"

namespace
{
// Anonymous namespace to create classes that are only
// visible to this file
//
// These are helpers that return a *AuthDatabase and
// allow us to run the same tests on different databases and
// database acquisition strategies.

class AuthDatabaseProvider
{
public:
virtual ~AuthDatabaseProvider() = default;
virtual AuthDatabase *getAuthDatabase() = 0;
};

class FixedProvider : public AuthDatabaseProvider
{
public:
FixedProvider(AuthDatabase *auth_db) : auth_db(auth_db){};
virtual ~FixedProvider(){};
virtual AuthDatabase *getAuthDatabase() { return auth_db; };

private:
AuthDatabase *auth_db;
};

class FilesProvider : public AuthDatabaseProvider
{
public:
FilesProvider(const std::string &dir) : dir(dir){};
virtual ~FilesProvider() { delete auth_db; };
virtual AuthDatabase *getAuthDatabase()
{
delete auth_db;
auth_db = new AuthDatabaseFiles(dir);
return auth_db;
};

private:
std::string dir;
AuthDatabase *auth_db = nullptr;
};

class SQLite3Provider : public AuthDatabaseProvider
{
public:
SQLite3Provider(const std::string &dir) : dir(dir){};
virtual ~SQLite3Provider() { delete auth_db; };
virtual AuthDatabase *getAuthDatabase()
{
delete auth_db;
auth_db = new AuthDatabaseSQLite3(dir);
return auth_db;
};

private:
std::string dir;
AuthDatabase *auth_db = nullptr;
};
}

class TestAuthDatabase : public TestBase
{
public:
TestAuthDatabase()
{
TestManager::registerTestModule(this);
// fixed directory, for persistence
test_dir = getTestTempDirectory();
}
const char *getName() { return "TestAuthDatabase"; }

void runTests(IGameDef *gamedef);
void runTestsForCurrentDB();

void testRecallFail();
void testCreate();
void testRecall();
void testChange();
void testRecallChanged();
void testChangePrivileges();
void testRecallChangedPrivileges();
void testListNames();
void testDelete();

private:
std::string test_dir;
AuthDatabaseProvider *auth_provider;
};

static TestAuthDatabase g_test_instance;

void TestAuthDatabase::runTests(IGameDef *gamedef)
{
// Each set of tests is run twice for each database type:
// one where we reuse the same AuthDatabase object (to test local caching),
// and one where we create a new AuthDatabase object for each call
// (to test actual persistence).

rawstream << "-------- Files database (same object)" << std::endl;

AuthDatabase *auth_db = new AuthDatabaseFiles(test_dir);
auth_provider = new FixedProvider(auth_db);

runTestsForCurrentDB();

delete auth_db;
delete auth_provider;

// reset database
fs::DeleteSingleFileOrEmptyDirectory(test_dir + DIR_DELIM + "auth.txt");

rawstream << "-------- Files database (new objects)" << std::endl;

auth_provider = new FilesProvider(test_dir);

runTestsForCurrentDB();

delete auth_provider;

rawstream << "-------- SQLite3 database (same object)" << std::endl;

auth_db = new AuthDatabaseSQLite3(test_dir);
auth_provider = new FixedProvider(auth_db);

runTestsForCurrentDB();

delete auth_db;
delete auth_provider;

// reset database
fs::DeleteSingleFileOrEmptyDirectory(test_dir + DIR_DELIM + "auth.sqlite");

rawstream << "-------- SQLite3 database (new objects)" << std::endl;

auth_provider = new SQLite3Provider(test_dir);

runTestsForCurrentDB();

delete auth_provider;
}

////////////////////////////////////////////////////////////////////////////////

void TestAuthDatabase::runTestsForCurrentDB()
{
TEST(testRecallFail);
TEST(testCreate);
TEST(testRecall);
TEST(testChange);
TEST(testRecallChanged);
TEST(testChangePrivileges);
TEST(testRecallChangedPrivileges);
TEST(testListNames);
TEST(testDelete);
TEST(testRecallFail);
}

void TestAuthDatabase::testRecallFail()
{
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
AuthEntry authEntry;

// no such user yet
UASSERT(!auth_db->getAuth("TestName", authEntry));
}

void TestAuthDatabase::testCreate()
{
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
AuthEntry authEntry;

authEntry.name = "TestName";
authEntry.password = "TestPassword";
authEntry.privileges.emplace_back("shout");
authEntry.privileges.emplace_back("interact");
authEntry.last_login = 1000;
UASSERT(auth_db->createAuth(authEntry));
}

void TestAuthDatabase::testRecall()
{
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
AuthEntry authEntry;

UASSERT(auth_db->getAuth("TestName", authEntry));
UASSERTEQ(std::string, authEntry.name, "TestName");
UASSERTEQ(std::string, authEntry.password, "TestPassword");
// the order of privileges is unimportant
std::sort(authEntry.privileges.begin(), authEntry.privileges.end());
UASSERTEQ(std::string, str_join(authEntry.privileges, ","), "interact,shout");
}

void TestAuthDatabase::testChange()
{
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
AuthEntry authEntry;

UASSERT(auth_db->getAuth("TestName", authEntry));
authEntry.password = "NewPassword";
authEntry.last_login = 1002;
UASSERT(auth_db->saveAuth(authEntry));
}

void TestAuthDatabase::testRecallChanged()
{
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
AuthEntry authEntry;

UASSERT(auth_db->getAuth("TestName", authEntry));
UASSERTEQ(std::string, authEntry.password, "NewPassword");
// the order of privileges is unimportant
std::sort(authEntry.privileges.begin(), authEntry.privileges.end());
UASSERTEQ(std::string, str_join(authEntry.privileges, ","), "interact,shout");
UASSERTEQ(u64, authEntry.last_login, 1002);
}

void TestAuthDatabase::testChangePrivileges()
{
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
AuthEntry authEntry;

UASSERT(auth_db->getAuth("TestName", authEntry));
authEntry.privileges.clear();
authEntry.privileges.emplace_back("interact");
authEntry.privileges.emplace_back("fly");
authEntry.privileges.emplace_back("dig");
UASSERT(auth_db->saveAuth(authEntry));
}

void TestAuthDatabase::testRecallChangedPrivileges()
{
AuthDatabase *auth_db = auth_provider->getAuthDatabase();
AuthEntry authEntry;

UASSERT(auth_db->getAuth("TestName", authEntry));
// the order of privileges is unimportant
std::sort(authEntry.privileges.begin(), authEntry.privileges.end());
UASSERTEQ(std::string, str_join(authEntry.privileges, ","), "dig,fly,interact");
}

void TestAuthDatabase::testListNames()
{

AuthDatabase *auth_db = auth_provider->getAuthDatabase();
std::vector<std::string> list;

AuthEntry authEntry;

authEntry.name = "SecondName";
authEntry.password = "SecondPassword";
authEntry.privileges.emplace_back("shout");
authEntry.privileges.emplace_back("interact");
authEntry.last_login = 1003;
auth_db->createAuth(authEntry);

auth_db->listNames(list);
// not necessarily sorted, so sort before comparing
std::sort(list.begin(), list.end());
UASSERTEQ(std::string, str_join(list, ","), "SecondName,TestName");
}

void TestAuthDatabase::testDelete()
{
AuthDatabase *auth_db = auth_provider->getAuthDatabase();

UASSERT(!auth_db->deleteAuth("NoSuchName"));
UASSERT(auth_db->deleteAuth("TestName"));
// second try, expect failure
UASSERT(!auth_db->deleteAuth("TestName"));
}
23 changes: 23 additions & 0 deletions src/unittest/test_utilities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class TestUtilities : public TestBase {
void testIsNumber();
void testIsPowerOfTwo();
void testMyround();
void testStringJoin();
};

static TestUtilities g_test_instance;
Expand Down Expand Up @@ -78,6 +79,7 @@ void TestUtilities::runTests(IGameDef *gamedef)
TEST(testIsNumber);
TEST(testIsPowerOfTwo);
TEST(testMyround);
TEST(testStringJoin);
}

////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -328,3 +330,24 @@ void TestUtilities::testMyround()
UASSERT(myround(-6.5f) == -7);
}

void TestUtilities::testStringJoin()
{
std::vector<std::string> input;
UASSERT(str_join(input, ",") == "");

input.emplace_back("one");
UASSERT(str_join(input, ",") == "one");

input.emplace_back("two");
UASSERT(str_join(input, ",") == "one,two");

input.emplace_back("three");
UASSERT(str_join(input, ",") == "one,two,three");

input[1] = "";
UASSERT(str_join(input, ",") == "one,,three");

input[1] = "two";
UASSERT(str_join(input, " and ") == "one and two and three");
}

19 changes: 19 additions & 0 deletions src/util/string.h
Original file line number Diff line number Diff line change
Expand Up @@ -704,3 +704,22 @@ inline const std::string duration_to_string(int sec)

return ss.str();
}

/**
* Joins a vector of strings by the string \p delimiter.
*
* @return A std::string
*/
inline std::string str_join(const std::vector<std::string> &list,
const std::string &delimiter)
{
std::ostringstream oss;
bool first = true;
for (const auto &part : list) {
if (!first)
oss << delimiter;
oss << part;
first = false;
}
return oss.str();
}