Replace auth.txt with SQLite auth database (#7279)
authorBen Deutsch <ben@bendeutsch.de>
Sun, 5 Aug 2018 11:13:38 +0000 (13:13 +0200)
committerLoïc Blot <nerzhul@users.noreply.github.com>
Sun, 5 Aug 2018 11:13:38 +0000 (13:13 +0200)
* Replace auth.txt with SQLite auth database

19 files changed:
build/android/jni/Android.mk
builtin/game/auth.lua
doc/world_format.txt
src/database/database-files.cpp
src/database/database-files.h
src/database/database-sqlite3.cpp
src/database/database-sqlite3.h
src/database/database.h
src/main.cpp
src/script/lua_api/CMakeLists.txt
src/script/lua_api/l_auth.cpp [new file with mode: 0644]
src/script/lua_api/l_auth.h [new file with mode: 0644]
src/script/scripting_server.cpp
src/serverenvironment.cpp
src/serverenvironment.h
src/unittest/CMakeLists.txt
src/unittest/test_authdatabase.cpp [new file with mode: 0644]
src/unittest/test_utilities.cpp
src/util/string.h

index c8057a4dd0fc17d0042ef8ebd3b2df45f27c84bf..548e56ffe5082e771aaf7b7a90493a98ea79f31e 100644 (file)
@@ -251,6 +251,7 @@ LOCAL_SRC_FILES := \
                jni/src/util/srp.cpp                      \
                jni/src/util/timetaker.cpp                \
                jni/src/unittest/test.cpp                 \
+               jni/src/unittest/test_authdatabase.cpp    \
                jni/src/unittest/test_collision.cpp       \
                jni/src/unittest/test_compression.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_server.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_camera.cpp       \
                jni/src/script/lua_api/l_client.cpp       \
index ad2f35a138e659519733656038a8e89f80cd9b4b..7aedfc82ed490345b455d7818e22c2d9269b4720 100644 (file)
@@ -4,72 +4,22 @@
 -- Builtin authentication handler
 --
 
-local auth_file_path = core.get_worldpath().."/auth.txt"
-local auth_table = {}
-
-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()
+-- Make the auth object private, deny access to mods
+local core_auth = core.auth
+core.auth = nil
 
 core.builtin_auth_handler = {
        get_auth = function(name)
                assert(type(name) == "string")
-               -- Figure out what password to use for a new player (singleplayer
-               -- always has an empty password, otherwise use default, which is
-               -- usually empty too)
-               local new_password_hash = ""
-               -- If not in authentication table, return nil
-               if not auth_table[name] then
+               local auth_entry = core_auth.read(name)
+               -- If no such auth found, return nil
+               if not auth_entry then
                        return nil
                end
                -- Figure out what privileges the player should have.
                -- Take a copy of the privilege table
                local privileges = {}
-               for priv, _ in pairs(auth_table[name].privileges) do
+               for priv, _ in pairs(auth_entry.privileges) do
                        privileges[priv] = true
                end
                -- If singleplayer, give all privileges except those marked as give_to_singleplayer = false
@@ -89,85 +39,89 @@ core.builtin_auth_handler = {
                end
                -- All done
                return {
-                       password = auth_table[name].password,
+                       password = auth_entry.password,
                        privileges = privileges,
                        -- Is set to nil if unknown
-                       last_login = auth_table[name].last_login,
+                       last_login = auth_entry.last_login,
                }
        end,
        create_auth = function(name, password)
                assert(type(name) == "string")
                assert(type(password) == "string")
                core.log('info', "Built-in authentication handler adding player '"..name.."'")
-               auth_table[name] = {
+               return core_auth.create({
+                       name = name,
                        password = password,
                        privileges = core.string_to_privs(core.settings:get("default_privs")),
                        last_login = os.time(),
-               }
-               save_auth_file()
+               })
        end,
        delete_auth = function(name)
                assert(type(name) == "string")
-               if not auth_table[name] then
+               local auth_entry = core_auth.read(name)
+               if not auth_entry then
                        return false
                end
                core.log('info', "Built-in authentication handler deleting player '"..name.."'")
-               auth_table[name] = nil
-               save_auth_file()
-               return true
+               return core_auth.delete(name)
        end,
        set_password = function(name, password)
                assert(type(name) == "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)
                else
                        core.log('info', "Built-in authentication handler setting password of player '"..name.."'")
-                       auth_table[name].password = password
-                       save_auth_file()
+                       auth_entry.password = password
+                       core_auth.save(auth_entry)
                end
                return true
        end,
        set_privileges = function(name, privileges)
                assert(type(name) == "string")
                assert(type(privileges) == "table")
-               if not auth_table[name] then
-                       core.builtin_auth_handler.create_auth(name,
+               local auth_entry = core_auth.read(name)
+               if not auth_entry then
+                       auth_entry = core.builtin_auth_handler.create_auth(name,
                                core.get_password_hash(name,
                                        core.settings:get("default_password")))
                end
 
                -- Run grant callbacks
                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")
                        end
                end
 
                -- 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
                                core.run_priv_callbacks(name, priv, nil, "revoke")
                        end
                end
 
-               auth_table[name].privileges = privileges
+               auth_entry.privileges = privileges
+               core_auth.save(auth_entry)
                core.notify_authentication_modified(name)
-               save_auth_file()
        end,
        reload = function()
-               read_auth_file()
+               core_auth.reload()
                return true
        end,
        record_login = function(name)
                assert(type(name) == "string")
-               assert(auth_table[name]).last_login = os.time()
-               save_auth_file()
+               local auth_entry = core_auth.read(name)
+               assert(auth_entry)
+               auth_entry.last_login = os.time()
+               core_auth.save(auth_entry)
        end,
        iterate = function()
                local names = {}
-               for k in pairs(auth_table) do
-                       names[k] = true
+               local nameslist = core_auth.list_names()
+               for k,v in pairs(nameslist) do
+                       names[v] = true
                end
                return pairs(names)
        end,
@@ -177,12 +131,13 @@ core.register_on_prejoinplayer(function(name, ip)
        if core.registered_auth_handler ~= nil then
                return -- Don't do anything if custom auth handler registered
        end
-       if auth_table[name] ~= nil then
+       local auth_entry = core_auth.read(name)
+       if auth_entry ~= nil then
                return
        end
 
        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
                        return string.format("\nCannot create new player called '%s'. "..
                                        "Another account called '%s' is already registered. "..
index 9b0a1ef07a21abbac5f8b24ed462350809e33626..c5d1d1be1a8d541d63669a241713d73fdf8fe890 100644 (file)
@@ -29,6 +29,7 @@ It can be copied over from an old world to a newly created world.
 
 World
 |-- auth.txt ----- Authentication data
+|-- auth.sqlite -- Authentication data (SQLite alternative)
 |-- env_meta.txt - Environment metadata
 |-- ipban.txt ---- Banned ips/users
 |-- map_meta.txt - Map metadata
@@ -62,6 +63,34 @@ Example lines:
 - Player "bar", no password, no privileges:
     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
 -------------
 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)
   server_announce = false       - whether the server is publicly announced or not
   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
 ===================
index 70de8c8d2cc3c4b0145614b2210771b633068cc3..64eca394e0269ef03fe18465823a8a896e96c549 100644 (file)
@@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "settings.h"
 #include "porting.h"
 #include "filesys.h"
+#include "util/string.h"
 
 // !!! WARNING !!!
 // 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());
        }
 }
+
+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;
+}
index f0824a3047aa1e28dcd2ee59473e115605e194f4..218815cf7841f81ddd34a8f56ec04048ffca5ee2 100644 (file)
@@ -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
 {
@@ -41,3 +42,23 @@ private:
 
        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();
+};
index 76935ada4648934672d98ba80f38ac02aab3c502..97b0fd36a5df57c2ddf9f23553c422ad81268d5b 100644 (file)
@@ -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 (?, ?)");
+       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);
+       }
+}
index 8d9f91f21b5538e055863ed6d695bcbfb3a983df..d7202a91864063b06c9424e1e6fe9b88cb32615b 100644 (file)
@@ -85,6 +85,16 @@ protected:
                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);
@@ -191,3 +201,34 @@ private:
        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;
+};
index 9926c7b93a264fce9b165cae56b223bf2e091021..b7d5519350441a8e8e37cec08e8cd65319723881 100644 (file)
@@ -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"
@@ -61,3 +62,25 @@ public:
        virtual bool removePlayer(const std::string &name) = 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;
+};
index 005e1acc74ed831431618d5572f48cf70d7540b8..e033ef63a578e456a4023d7c959a6f7f97fcd2b1 100644 (file)
@@ -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
@@ -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;
index 97c3786ec84cbc63ffd8784c492de2bb0d52ba83..32f6a2793d0437045fe477e6dfb2d1b149a16b52 100644 (file)
@@ -1,5 +1,6 @@
 set(common_SCRIPT_LUA_API_SRCS
        ${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_craft.cpp
        ${CMAKE_CURRENT_SOURCE_DIR}/l_env.cpp
diff --git a/src/script/lua_api/l_auth.cpp b/src/script/lua_api/l_auth.cpp
new file mode 100644 (file)
index 0000000..0fc57ba
--- /dev/null
@@ -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");
+}
diff --git a/src/script/lua_api/l_auth.h b/src/script/lua_api/l_auth.h
new file mode 100644 (file)
index 0000000..fb9a987
--- /dev/null
@@ -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);
+};
index 93b28b61b324f3b80d470ac2568399ad04615ceb..2204c6884358216576a80866f6cb570132e2d0db 100644 (file)
@@ -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"
@@ -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);
index 7d7eb4c088ee4cd387e044cab85dbb2f547a1ca5..3b29838256e370ef097e0de708982e9555f809a5 100644 (file)
@@ -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);
+       } 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()
@@ -439,6 +451,7 @@ ServerEnvironment::~ServerEnvironment()
        }
 
        delete m_player_database;
+       delete m_auth_database;
 }
 
 Map & ServerEnvironment::getMap()
@@ -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) {
+                       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;
+}
index 225f788d9d5ea4e13ad78d1a4212e0546f6a189a..b7a121adff241706fc2e3161676cfc28df4a4cd9 100644 (file)
@@ -32,6 +32,7 @@ struct GameParams;
 class MapBlock;
 class RemotePlayer;
 class PlayerDatabase;
+class AuthDatabase;
 class PlayerSAO;
 class ServerEnvironment;
 class ActiveBlockModifier;
@@ -366,6 +367,10 @@ public:
 
        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:
 
        /**
@@ -375,6 +380,8 @@ private:
 
        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
                -------------------------------------------
@@ -467,6 +474,7 @@ private:
        std::vector<RemotePlayer*> m_players;
 
        PlayerDatabase *m_player_database = nullptr;
+       AuthDatabase *m_auth_database = nullptr;
 
        // Particles
        IntervalLimiter m_particle_management_interval;
index 311204e329b06745d1f4d9d75553abc3a5cd0282..3ffe1978ecd6a5f3bc724534b55a7fd858a87bd2 100644 (file)
@@ -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
diff --git a/src/unittest/test_authdatabase.cpp b/src/unittest/test_authdatabase.cpp
new file mode 100644 (file)
index 0000000..1e8fd23
--- /dev/null
@@ -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"));
+}
index 315cbf0fc3854cd7542e6857e3a41f448165e7a0..6f4a01432e1b97f971e89d8f08e1c2d5d6329726 100644 (file)
@@ -51,6 +51,7 @@ public:
        void testIsNumber();
        void testIsPowerOfTwo();
        void testMyround();
+       void testStringJoin();
 };
 
 static TestUtilities g_test_instance;
@@ -78,6 +79,7 @@ void TestUtilities::runTests(IGameDef *gamedef)
        TEST(testIsNumber);
        TEST(testIsPowerOfTwo);
        TEST(testMyround);
+       TEST(testStringJoin);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -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");
+}
+
index 35b7cfa8a718d7870fef5173805b0784c3f48d81..ab9a4a6c8563fdde3a6d08404bdf39eb1b73b179 100644 (file)
@@ -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();
+}