Skip to content

Commit

Permalink
Replace auth.txt with SQLite auth database (#7279)
Browse files Browse the repository at this point in the history
* Replace auth.txt with SQLite auth database
  • Loading branch information
bendeutsch authored and nerzhul committed Aug 5, 2018
1 parent 1836882 commit 153fb21
Show file tree
Hide file tree
Showing 19 changed files with 1,153 additions and 82 deletions.
2 changes: 2 additions & 0 deletions build/android/jni/Android.mk
Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down
119 changes: 37 additions & 82 deletions builtin/game/auth.lua
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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. "..
Expand Down
30 changes: 30 additions & 0 deletions doc/world_format.txt
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
===================
Expand Down
103 changes: 103 additions & 0 deletions src/database/database-files.cpp
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

0 comments on commit 153fb21

Please sign in to comment.