Replace auth.txt with SQLite auth database (#7279)

* Replace auth.txt with SQLite auth database
This commit is contained in:
Ben Deutsch 2018-08-05 13:13:38 +02:00 committed by Loïc Blot
parent 1836882495
commit 153fb211ac
19 changed files with 1153 additions and 82 deletions

@ -251,6 +251,7 @@ LOCAL_SRC_FILES := \
jni/src/util/srp.cpp \ jni/src/util/srp.cpp \
jni/src/util/timetaker.cpp \ jni/src/util/timetaker.cpp \
jni/src/unittest/test.cpp \ jni/src/unittest/test.cpp \
jni/src/unittest/test_authdatabase.cpp \
jni/src/unittest/test_collision.cpp \ jni/src/unittest/test_collision.cpp \
jni/src/unittest/test_compression.cpp \ jni/src/unittest/test_compression.cpp \
jni/src/unittest/test_connection.cpp \ jni/src/unittest/test_connection.cpp \
@ -331,6 +332,7 @@ LOCAL_SRC_FILES += \
jni/src/script/cpp_api/s_security.cpp \ jni/src/script/cpp_api/s_security.cpp \
jni/src/script/cpp_api/s_server.cpp \ jni/src/script/cpp_api/s_server.cpp \
jni/src/script/lua_api/l_areastore.cpp \ jni/src/script/lua_api/l_areastore.cpp \
jni/src/script/lua_api/l_auth.cpp \
jni/src/script/lua_api/l_base.cpp \ jni/src/script/lua_api/l_base.cpp \
jni/src/script/lua_api/l_camera.cpp \ jni/src/script/lua_api/l_camera.cpp \
jni/src/script/lua_api/l_client.cpp \ jni/src/script/lua_api/l_client.cpp \

@ -4,72 +4,22 @@
-- Builtin authentication handler -- Builtin authentication handler
-- --
local auth_file_path = core.get_worldpath().."/auth.txt" -- Make the auth object private, deny access to mods
local auth_table = {} local core_auth = core.auth
core.auth = nil
local function read_auth_file()
local newtable = {}
local file, errmsg = io.open(auth_file_path, 'rb')
if not file then
core.log("info", auth_file_path.." could not be opened for reading ("..errmsg.."); assuming new world")
return
end
for line in file:lines() do
if line ~= "" then
local fields = line:split(":", true)
local name, password, privilege_string, last_login = unpack(fields)
last_login = tonumber(last_login)
if not (name and password and privilege_string) then
error("Invalid line in auth.txt: "..dump(line))
end
local privileges = core.string_to_privs(privilege_string)
newtable[name] = {password=password, privileges=privileges, last_login=last_login}
end
end
io.close(file)
auth_table = newtable
core.notify_authentication_modified()
end
local function save_auth_file()
local newtable = {}
-- Check table for validness before attempting to save
for name, stuff in pairs(auth_table) do
assert(type(name) == "string")
assert(name ~= "")
assert(type(stuff) == "table")
assert(type(stuff.password) == "string")
assert(type(stuff.privileges) == "table")
assert(stuff.last_login == nil or type(stuff.last_login) == "number")
end
local content = {}
for name, stuff in pairs(auth_table) do
local priv_string = core.privs_to_string(stuff.privileges)
local parts = {name, stuff.password, priv_string, stuff.last_login or ""}
content[#content + 1] = table.concat(parts, ":")
end
if not core.safe_file_write(auth_file_path, table.concat(content, "\n")) then
error(auth_file_path.." could not be written to")
end
end
read_auth_file()
core.builtin_auth_handler = { core.builtin_auth_handler = {
get_auth = function(name) get_auth = function(name)
assert(type(name) == "string") assert(type(name) == "string")
-- Figure out what password to use for a new player (singleplayer local auth_entry = core_auth.read(name)
-- always has an empty password, otherwise use default, which is -- If no such auth found, return nil
-- usually empty too) if not auth_entry then
local new_password_hash = ""
-- If not in authentication table, return nil
if not auth_table[name] then
return nil return nil
end end
-- Figure out what privileges the player should have. -- Figure out what privileges the player should have.
-- Take a copy of the privilege table -- Take a copy of the privilege table
local privileges = {} local privileges = {}
for priv, _ in pairs(auth_table[name].privileges) do for priv, _ in pairs(auth_entry.privileges) do
privileges[priv] = true privileges[priv] = true
end end
-- If singleplayer, give all privileges except those marked as give_to_singleplayer = false -- If singleplayer, give all privileges except those marked as give_to_singleplayer = false
@ -89,85 +39,89 @@ core.builtin_auth_handler = {
end end
-- All done -- All done
return { return {
password = auth_table[name].password, password = auth_entry.password,
privileges = privileges, privileges = privileges,
-- Is set to nil if unknown -- Is set to nil if unknown
last_login = auth_table[name].last_login, last_login = auth_entry.last_login,
} }
end, end,
create_auth = function(name, password) create_auth = function(name, password)
assert(type(name) == "string") assert(type(name) == "string")
assert(type(password) == "string") assert(type(password) == "string")
core.log('info', "Built-in authentication handler adding player '"..name.."'") core.log('info', "Built-in authentication handler adding player '"..name.."'")
auth_table[name] = { return core_auth.create({
name = name,
password = password, password = password,
privileges = core.string_to_privs(core.settings:get("default_privs")), privileges = core.string_to_privs(core.settings:get("default_privs")),
last_login = os.time(), last_login = os.time(),
} })
save_auth_file()
end, end,
delete_auth = function(name) delete_auth = function(name)
assert(type(name) == "string") assert(type(name) == "string")
if not auth_table[name] then local auth_entry = core_auth.read(name)
if not auth_entry then
return false return false
end end
core.log('info', "Built-in authentication handler deleting player '"..name.."'") core.log('info', "Built-in authentication handler deleting player '"..name.."'")
auth_table[name] = nil return core_auth.delete(name)
save_auth_file()
return true
end, end,
set_password = function(name, password) set_password = function(name, password)
assert(type(name) == "string") assert(type(name) == "string")
assert(type(password) == "string") assert(type(password) == "string")
if not auth_table[name] then local auth_entry = core_auth.read(name)
if not auth_entry then
core.builtin_auth_handler.create_auth(name, password) core.builtin_auth_handler.create_auth(name, password)
else else
core.log('info', "Built-in authentication handler setting password of player '"..name.."'") core.log('info', "Built-in authentication handler setting password of player '"..name.."'")
auth_table[name].password = password auth_entry.password = password
save_auth_file() core_auth.save(auth_entry)
end end
return true return true
end, end,
set_privileges = function(name, privileges) set_privileges = function(name, privileges)
assert(type(name) == "string") assert(type(name) == "string")
assert(type(privileges) == "table") assert(type(privileges) == "table")
if not auth_table[name] then local auth_entry = core_auth.read(name)
core.builtin_auth_handler.create_auth(name, if not auth_entry then
auth_entry = core.builtin_auth_handler.create_auth(name,
core.get_password_hash(name, core.get_password_hash(name,
core.settings:get("default_password"))) core.settings:get("default_password")))
end end
-- Run grant callbacks -- Run grant callbacks
for priv, _ in pairs(privileges) do for priv, _ in pairs(privileges) do
if not auth_table[name].privileges[priv] then if not auth_entry.privileges[priv] then
core.run_priv_callbacks(name, priv, nil, "grant") core.run_priv_callbacks(name, priv, nil, "grant")
end end
end end
-- Run revoke callbacks -- Run revoke callbacks
for priv, _ in pairs(auth_table[name].privileges) do for priv, _ in pairs(auth_entry.privileges) do
if not privileges[priv] then if not privileges[priv] then
core.run_priv_callbacks(name, priv, nil, "revoke") core.run_priv_callbacks(name, priv, nil, "revoke")
end end
end end
auth_table[name].privileges = privileges auth_entry.privileges = privileges
core_auth.save(auth_entry)
core.notify_authentication_modified(name) core.notify_authentication_modified(name)
save_auth_file()
end, end,
reload = function() reload = function()
read_auth_file() core_auth.reload()
return true return true
end, end,
record_login = function(name) record_login = function(name)
assert(type(name) == "string") assert(type(name) == "string")
assert(auth_table[name]).last_login = os.time() local auth_entry = core_auth.read(name)
save_auth_file() assert(auth_entry)
auth_entry.last_login = os.time()
core_auth.save(auth_entry)
end, end,
iterate = function() iterate = function()
local names = {} local names = {}
for k in pairs(auth_table) do local nameslist = core_auth.list_names()
names[k] = true for k,v in pairs(nameslist) do
names[v] = true
end end
return pairs(names) return pairs(names)
end, end,
@ -177,12 +131,13 @@ core.register_on_prejoinplayer(function(name, ip)
if core.registered_auth_handler ~= nil then if core.registered_auth_handler ~= nil then
return -- Don't do anything if custom auth handler registered return -- Don't do anything if custom auth handler registered
end end
if auth_table[name] ~= nil then local auth_entry = core_auth.read(name)
if auth_entry ~= nil then
return return
end end
local name_lower = name:lower() local name_lower = name:lower()
for k in pairs(auth_table) do for k in core.builtin_auth_handler.iterate() do
if k:lower() == name_lower then if k:lower() == name_lower then
return string.format("\nCannot create new player called '%s'. ".. return string.format("\nCannot create new player called '%s'. "..
"Another account called '%s' is already registered. ".. "Another account called '%s' is already registered. "..

@ -29,6 +29,7 @@ It can be copied over from an old world to a newly created world.
World World
|-- auth.txt ----- Authentication data |-- auth.txt ----- Authentication data
|-- auth.sqlite -- Authentication data (SQLite alternative)
|-- env_meta.txt - Environment metadata |-- env_meta.txt - Environment metadata
|-- ipban.txt ---- Banned ips/users |-- ipban.txt ---- Banned ips/users
|-- map_meta.txt - Map metadata |-- map_meta.txt - Map metadata
@ -62,6 +63,34 @@ Example lines:
- Player "bar", no password, no privileges: - Player "bar", no password, no privileges:
bar:: bar::
auth.sqlite
------------
Contains authentification data as an SQLite database. This replaces auth.txt
above when auth_backend is set to "sqlite3" in world.mt .
This database contains two tables "auth" and "user_privileges":
CREATE TABLE `auth` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`name` VARCHAR(32) UNIQUE,
`password` VARCHAR(512),
`last_login` INTEGER
);
CREATE TABLE `user_privileges` (
`id` INTEGER,
`privilege` VARCHAR(32),
PRIMARY KEY (id, privilege)
CONSTRAINT fk_id FOREIGN KEY (id) REFERENCES auth (id) ON DELETE CASCADE
);
The "name" and "password" fields of the auth table are the same as the auth.txt
fields (with modern password hash). The "last_login" field is the last login
time as a unix time stamp.
The "user_privileges" table contains one entry per privilege and player.
A player with "interact" and "shout" privileges will have two entries, one
with privilege="interact" and the second with privilege="shout".
env_meta.txt env_meta.txt
------------- -------------
Simple global environment variables. Simple global environment variables.
@ -107,6 +136,7 @@ Example content (added indentation and - explanations):
readonly_backend = sqlite3 - optionally readonly seed DB (DB file _must_ be located in "readonly" subfolder) readonly_backend = sqlite3 - optionally readonly seed DB (DB file _must_ be located in "readonly" subfolder)
server_announce = false - whether the server is publicly announced or not server_announce = false - whether the server is publicly announced or not
load_mod_<mod> = false - whether <mod> is to be loaded in this world load_mod_<mod> = false - whether <mod> is to be loaded in this world
auth_backend = files - which DB backend to use for authentication data
Player File Format Player File Format
=================== ===================

@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "settings.h" #include "settings.h"
#include "porting.h" #include "porting.h"
#include "filesys.h" #include "filesys.h"
#include "util/string.h"
// !!! WARNING !!! // !!! WARNING !!!
// This backend is intended to be used on Minetest 0.4.16 only for the transition backend // This backend is intended to be used on Minetest 0.4.16 only for the transition backend
@ -177,3 +178,105 @@ void PlayerDatabaseFiles::listPlayers(std::vector<std::string> &res)
res.emplace_back(player.getName()); res.emplace_back(player.getName());
} }
} }
AuthDatabaseFiles::AuthDatabaseFiles(const std::string &savedir) : m_savedir(savedir)
{
readAuthFile();
}
bool AuthDatabaseFiles::getAuth(const std::string &name, AuthEntry &res)
{
const auto res_i = m_auth_list.find(name);
if (res_i == m_auth_list.end()) {
return false;
}
res = res_i->second;
return true;
}
bool AuthDatabaseFiles::saveAuth(const AuthEntry &authEntry)
{
m_auth_list[authEntry.name] = authEntry;
// save entire file
return writeAuthFile();
}
bool AuthDatabaseFiles::createAuth(AuthEntry &authEntry)
{
m_auth_list[authEntry.name] = authEntry;
// save entire file
return writeAuthFile();
}
bool AuthDatabaseFiles::deleteAuth(const std::string &name)
{
if (!m_auth_list.erase(name)) {
// did not delete anything -> hadn't existed
return false;
}
return writeAuthFile();
}
void AuthDatabaseFiles::listNames(std::vector<std::string> &res)
{
res.clear();
res.reserve(m_auth_list.size());
for (const auto &res_pair : m_auth_list) {
res.push_back(res_pair.first);
}
}
void AuthDatabaseFiles::reload()
{
readAuthFile();
}
bool AuthDatabaseFiles::readAuthFile()
{
std::string path = m_savedir + DIR_DELIM + "auth.txt";
std::ifstream file(path, std::ios::binary);
if (!file.good()) {
return false;
}
m_auth_list.clear();
while (file.good()) {
std::string line;
std::getline(file, line);
std::vector<std::string> parts = str_split(line, ':');
if (parts.size() < 3) // also: empty line at end
continue;
const std::string &name = parts[0];
const std::string &password = parts[1];
std::vector<std::string> privileges = str_split(parts[2], ',');
s64 last_login = parts.size() > 3 ? atol(parts[3].c_str()) : 0;
m_auth_list[name] = {
1,
name,
password,
privileges,
last_login,
};
}
return true;
}
bool AuthDatabaseFiles::writeAuthFile()
{
std::string path = m_savedir + DIR_DELIM + "auth.txt";
std::ostringstream output(std::ios_base::binary);
for (const auto &auth_i : m_auth_list) {
const AuthEntry &authEntry = auth_i.second;
output << authEntry.name << ":" << authEntry.password << ":";
output << str_join(authEntry.privileges, ",");
output << ":" << authEntry.last_login;
output << std::endl;
}
if (!fs::safeWriteToFile(path, output.str())) {
infostream << "Failed to write " << path << std::endl;
return false;
}
return true;
}

@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
// for player files // for player files
#include "database.h" #include "database.h"
#include <unordered_map>
class PlayerDatabaseFiles : public PlayerDatabase class PlayerDatabaseFiles : public PlayerDatabase
{ {
@ -41,3 +42,23 @@ private:
std::string m_savedir; 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);
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();
private:
std::unordered_map<std::string, AuthEntry> m_auth_list;
std::string m_savedir;
bool readAuthFile();
bool writeAuthFile();
};

@ -606,3 +606,170 @@ void PlayerDatabaseSQLite3::listPlayers(std::vector<std::string> &res)
sqlite3_reset(m_stmt_player_list); 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 (?, ?)");
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
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);
}
}

@ -85,6 +85,16 @@ protected:
return (u32) sqlite3_column_int(s, iCol); 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) inline float sqlite_to_float(sqlite3_stmt *s, int iCol)
{ {
return (float) sqlite3_column_double(s, iCol); return (float) sqlite3_column_double(s, iCol);
@ -191,3 +201,34 @@ private:
sqlite3_stmt *m_stmt_player_metadata_remove = nullptr; sqlite3_stmt *m_stmt_player_metadata_remove = nullptr;
sqlite3_stmt *m_stmt_player_metadata_add = 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;
};

@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#pragma once #pragma once
#include <set>
#include <string> #include <string>
#include <vector> #include <vector>
#include "irr_v3d.h" #include "irr_v3d.h"
@ -61,3 +62,25 @@ public:
virtual bool removePlayer(const std::string &name) = 0; virtual bool removePlayer(const std::string &name) = 0;
virtual void listPlayers(std::vector<std::string> &res) = 0; virtual void listPlayers(std::vector<std::string> &res) = 0;
}; };
struct AuthEntry
{
u64 id;
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;
};

@ -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)")))); _("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, 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)")))); _("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, allowed_options->insert(std::make_pair("terminal", ValueSpec(VALUETYPE_FLAG,
_("Feature an interactive terminal (Only works when using minetestserver or with --server)")))); _("Feature an interactive terminal (Only works when using minetestserver or with --server)"))));
#ifndef SERVER #ifndef SERVER
@ -840,6 +842,9 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
if (cmd_args.exists("migrate-players")) if (cmd_args.exists("migrate-players"))
return ServerEnvironment::migratePlayersDatabase(game_params, cmd_args); 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 (cmd_args.exists("terminal")) {
#if USE_CURSES #if USE_CURSES
bool name_ok = true; bool name_ok = true;

@ -1,5 +1,6 @@
set(common_SCRIPT_LUA_API_SRCS set(common_SCRIPT_LUA_API_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/l_areastore.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_areastore.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_auth.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_base.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_base.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_craft.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_craft.cpp
${CMAKE_CURRENT_SOURCE_DIR}/l_env.cpp ${CMAKE_CURRENT_SOURCE_DIR}/l_env.cpp

@ -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) {
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)
{
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");
}

@ -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);
};

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "settings.h" #include "settings.h"
#include "cpp_api/s_internal.h" #include "cpp_api/s_internal.h"
#include "lua_api/l_areastore.h" #include "lua_api/l_areastore.h"
#include "lua_api/l_auth.h"
#include "lua_api/l_base.h" #include "lua_api/l_base.h"
#include "lua_api/l_craft.h" #include "lua_api/l_craft.h"
#include "lua_api/l_env.h" #include "lua_api/l_env.h"
@ -106,6 +107,7 @@ void ServerScripting::InitializeModApi(lua_State *L, int top)
ModChannelRef::Register(L); ModChannelRef::Register(L);
// Initialize mod api modules // Initialize mod api modules
ModApiAuth::Initialize(L, top);
ModApiCraft::Initialize(L, top); ModApiCraft::Initialize(L, top);
ModApiEnvMod::Initialize(L, top); ModApiEnvMod::Initialize(L, top);
ModApiInventory::Initialize(L, top); ModApiInventory::Initialize(L, top);

@ -414,6 +414,18 @@ ServerEnvironment::ServerEnvironment(ServerMap *map,
std::string name; std::string name;
conf.getNoEx("player_backend", name); conf.getNoEx("player_backend", name);
m_player_database = openPlayerDatabase(name, path_world, conf); m_player_database = openPlayerDatabase(name, path_world, conf);
std::string auth_name = "files";
if (conf.exists("auth_backend")) {
conf.getNoEx("auth_backend", auth_name);
} 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() ServerEnvironment::~ServerEnvironment()
@ -439,6 +451,7 @@ ServerEnvironment::~ServerEnvironment()
} }
delete m_player_database; delete m_player_database;
delete m_auth_database;
} }
Map & ServerEnvironment::getMap() Map & ServerEnvironment::getMap()
@ -2274,3 +2287,91 @@ bool ServerEnvironment::migratePlayersDatabase(const GameParams &game_params,
} }
return true; 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) {
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;
}

@ -32,6 +32,7 @@ struct GameParams;
class MapBlock; class MapBlock;
class RemotePlayer; class RemotePlayer;
class PlayerDatabase; class PlayerDatabase;
class AuthDatabase;
class PlayerSAO; class PlayerSAO;
class ServerEnvironment; class ServerEnvironment;
class ActiveBlockModifier; class ActiveBlockModifier;
@ -366,6 +367,10 @@ public:
static bool migratePlayersDatabase(const GameParams &game_params, static bool migratePlayersDatabase(const GameParams &game_params,
const Settings &cmd_args); const Settings &cmd_args);
AuthDatabase *getAuthDatabase() { return m_auth_database; }
static bool migrateAuthDatabase(const GameParams &game_params,
const Settings &cmd_args);
private: private:
/** /**
@ -375,6 +380,8 @@ private:
static PlayerDatabase *openPlayerDatabase(const std::string &name, static PlayerDatabase *openPlayerDatabase(const std::string &name,
const std::string &savedir, const Settings &conf); const std::string &savedir, const Settings &conf);
static AuthDatabase *openAuthDatabase(const std::string &name,
const std::string &savedir, const Settings &conf);
/* /*
Internal ActiveObject interface Internal ActiveObject interface
------------------------------------------- -------------------------------------------
@ -467,6 +474,7 @@ private:
std::vector<RemotePlayer*> m_players; std::vector<RemotePlayer*> m_players;
PlayerDatabase *m_player_database = nullptr; PlayerDatabase *m_player_database = nullptr;
AuthDatabase *m_auth_database = nullptr;
// Particles // Particles
IntervalLimiter m_particle_management_interval; IntervalLimiter m_particle_management_interval;

@ -1,5 +1,6 @@
set (UNITTEST_SRCS set (UNITTEST_SRCS
${CMAKE_CURRENT_SOURCE_DIR}/test.cpp ${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_activeobject.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_areastore.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_areastore.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test_ban.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_ban.cpp

@ -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"));
}

@ -51,6 +51,7 @@ public:
void testIsNumber(); void testIsNumber();
void testIsPowerOfTwo(); void testIsPowerOfTwo();
void testMyround(); void testMyround();
void testStringJoin();
}; };
static TestUtilities g_test_instance; static TestUtilities g_test_instance;
@ -78,6 +79,7 @@ void TestUtilities::runTests(IGameDef *gamedef)
TEST(testIsNumber); TEST(testIsNumber);
TEST(testIsPowerOfTwo); TEST(testIsPowerOfTwo);
TEST(testMyround); TEST(testMyround);
TEST(testStringJoin);
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@ -328,3 +330,24 @@ void TestUtilities::testMyround()
UASSERT(myround(-6.5f) == -7); 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");
}

@ -704,3 +704,22 @@ inline const std::string duration_to_string(int sec)
return ss.str(); 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();
}