/* bzflag
 * Copyright (c) 1993-2025 Tim Riker
 *
 * This package is free software;  you can redistribute it and/or
 * modify it under the terms of the license found in the file
 * named COPYING that should have accompanied this file.
 *
 * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

/* interface header */
#include "Permissions.h"

/* system implementation headers */
#include <string>
#include <fstream>
#include <algorithm>
#include <sstream>
#include <stdlib.h>

/* common implementation headers */
#include "md5.h"
#include "bzfio.h"
#include "Protocol.h"
#include "TextUtils.h"

#include "bzfs.h"

PlayerAccessMap groupAccess;
PlayerAccessMap userDatabase;

uint8_t GetPlayerProperties(bool registered, bool verified, bool admin)
{
    uint8_t result = 0;
    if (registered)
        result |= IsRegistered;
    if (verified)
        result |= IsVerified;
    if (admin)
        result |= IsAdmin;
    return result;
}

PlayerAccessInfo::PlayerAccessInfo()
    : verified(false), loginTime(TimeKeeper::getCurrent()), loginAttempts (0),
      serverop(false), passwordAttempts(0)
{
    groups.push_back("EVERYONE");
    hasALLPerm = false;
}

void PlayerAccessInfo::setName(const char* callSign)
{
    regName = callSign;
    makeupper(regName);
}

void PlayerAccessInfo::setPermissionRights()
{
    verified = true;
    // get their real info
    PlayerAccessInfo &_info = getUserInfo(regName);
    explicitAllows = _info.explicitAllows;
    explicitDenys = _info.explicitDenys;
    groups = _info.groups;
    logDebugMessage(1,"Identify %s\n", regName.c_str());
}

void PlayerAccessInfo::reloadInfo()
{
    if (verified && userExists(regName))
    {
        PlayerAccessInfo accessInfo = getUserInfo(regName);
        explicitAllows = accessInfo.explicitAllows;
        explicitDenys  = accessInfo.explicitDenys;
        groups     = accessInfo.groups;
        loginTime      = accessInfo.loginTime;
        loginAttempts  = accessInfo.loginAttempts;
    }
}

void PlayerAccessInfo::setOperator()
{
    passwordAttempts = 0;
    serverop = true;
}

bool PlayerAccessInfo::isOperator() const
{
    return serverop;
}

bool PlayerAccessInfo::isAdmin() const
{
    return isOperator() || hasPerm(ban) || hasPerm(shortBan);
}

bool PlayerAccessInfo::showAsAdmin() const
{
    if (hasPerm(hideAdmin))
        return false;
    else if (hasPerm(showAdmin))
        return true;
    else
        return isAdmin();
}

bool PlayerAccessInfo::passwordAttemptsMax()
{
    bool maxAttempts = passwordAttempts >= 5;
    // see how many times they have tried, you only get 5
    if (!maxAttempts)
        passwordAttempts++;
    return maxAttempts;
}

std::string PlayerAccessInfo::getName()
{
    return regName;
}

bool PlayerAccessInfo::isRegistered() const
{
    return userExists(regName);
}

bool PlayerAccessInfo::isAllowedToEnter()
{
    return verified || !isRegistered();
}

bool PlayerAccessInfo::isVerified() const
{
    return verified;
}

void PlayerAccessInfo::storeInfo()
{
    PlayerAccessInfo _info;
    _info.addGroup("VERIFIED");
    userDatabase[regName] = _info;
    updateDatabases();
}

uint8_t PlayerAccessInfo::getPlayerProperties()
{
    return GetPlayerProperties(isRegistered(), verified, showAsAdmin());
}

bool PlayerAccessInfo::exists()
{
    return userExists(regName);
}

bool PlayerAccessInfo::hasGroup(const std::string &group)
{
    std::string str = group;
    if (!str.size())
        return false;

    makeupper(str);

    return std::find(groups.begin(), groups.end(), str) != groups.end();
}

bool PlayerAccessInfo::addGroup(const std::string &group)
{
    if (hasGroup(group))
        return false;

    std::string str = group;
    makeupper(str);

    groups.push_back(str);
    return true;
}

bool PlayerAccessInfo::removeGroup(const std::string &group)
{
    if (!hasGroup(group))
        return false;

    std::string str = group;
    makeupper(str);

    groups.erase(std::find(groups.begin(), groups.end(), str));
    return true;
}

bool PlayerAccessInfo::canSet(const std::string& group)
{
    if (hasPerm(PlayerAccessInfo::setAll))
        return true;
    return hasGroup(group) && hasPerm(PlayerAccessInfo::setPerms);
}

bool PlayerAccessInfo::hasPerm(PlayerAccessInfo::AccessPerm right) const
{
    if (serverop && (right != hideAdmin))
        return true;
    if (explicitDenys.test(right))
        return false;
    if (explicitAllows.test(right))
        return true;

    bool isAllowed = false;
    for (std::vector<std::string>::const_iterator itr=groups.begin(); itr!=groups.end(); ++itr)
    {
        PlayerAccessMap::iterator group = groupAccess.find(*itr);
        if (group != groupAccess.end())
        {
            if (group->second.explicitDenys.test(right))
                return false;
            else if (group->second.explicitAllows.test(right))
                isAllowed = true;
        }
    }
    if (publiclyDisconnected)
    {
        PlayerAccessMap::iterator group = groupAccess.find(std::string("DISCONNECTED"));
        if (group != groupAccess.end())
        {
            if (group->second.explicitDenys.test(right))
                return false;
            else if (group->second.explicitAllows.test(right))
                isAllowed = true;
        }
    }
    return isAllowed || hasALLPerm;
}

// grant and revoke perms
void PlayerAccessInfo::grantPerm(PlayerAccessInfo::AccessPerm right)
{
    explicitAllows.set(right);
    explicitDenys.reset(right);
}

void PlayerAccessInfo::revokePerm(PlayerAccessInfo::AccessPerm right)
{
    explicitAllows.reset(right);
    explicitDenys.set(right);
}


bool PlayerAccessInfo::hasCustomPerm(const char* right) const
{
    if (serverop || hasALLPerm)
        return true;

    std::string perm = TextUtils::toupper(std::string(right));

    // check for custom permissions in groups
    for (std::vector<std::string>::const_iterator itr=groups.begin(); itr!=groups.end(); ++itr)
    {
        PlayerAccessMap::iterator group = groupAccess.find(*itr);
        if (group != groupAccess.end())
        {
            for (unsigned int i = 0; i < group->second.customPerms.size(); i++)
            {
                if ( perm == TextUtils::toupper(group->second.customPerms[i]) )
                    return true;
            }
        }
    }

    if (publiclyDisconnected)
    {
        PlayerAccessMap::iterator group = groupAccess.find(std::string("DISCONNECTED"));
        if (group != groupAccess.end())
        {
            for (unsigned int i = 0; i < group->second.customPerms.size(); i++)
            {
                if ( perm == TextUtils::toupper(group->second.customPerms[i]) )
                    return true;
            }
        }
    }

    // check for custom permissions on a per player basis (stored in the customPerms vector)
    for (unsigned int i = 0; i < customPerms.size(); i++)
    {
        if (perm == TextUtils::toupper(customPerms[i]))
            return true;
    }

    return false;
}

void PlayerAccessInfo::grantCustomPerm(const char *right)
{
    customPerms.push_back(right);
}

void PlayerAccessInfo::revokeCustomPerm(const char *right)
{
    std::vector<std::string>::iterator position = std::find(customPerms.begin(), customPerms.end(), right);

    if (position != customPerms.end())
        customPerms.erase(position);
}

bool userExists(const std::string &nick)
{
    std::string str = nick;
    makeupper(str);
    PlayerAccessMap::iterator itr = userDatabase.find(str);
    return itr != userDatabase.end();
}

//FIXME - check for non-existing user (throw?)
PlayerAccessInfo &PlayerAccessInfo::getUserInfo(const std::string &nick)
{
    //  if (!userExists(nick))
    //    return false;
    std::string str = nick;
    makeupper(str);
    PlayerAccessMap::iterator itr = userDatabase.find(str);
    //  if (itr == userDatabase.end())
    //    return false;
    return itr->second;
}

std::string nameFromPerm(PlayerAccessInfo::AccessPerm perm)
{
    switch (perm)
    {
    case PlayerAccessInfo::actionMessage:
        return "actionMessage";
    case PlayerAccessInfo::adminMessageReceive:
        return "adminMessageReceive";
    case PlayerAccessInfo::adminMessageSend:
        return "adminMessageSend";
    case PlayerAccessInfo::antiban :
        return "antiban";
    case PlayerAccessInfo::antikick :
        return "antikick";
    case PlayerAccessInfo::antikill :
        return "antikill";
    case PlayerAccessInfo::antipoll :
        return "antipoll";
    case PlayerAccessInfo::antipollban :
        return "antipollban";
    case PlayerAccessInfo::antipollkick :
        return "antipollkick";
    case PlayerAccessInfo::antipollkill :
        return "antipollkill";
    case PlayerAccessInfo::ban:
        return "ban";
    case PlayerAccessInfo::banlist:
        return "banlist";
    case PlayerAccessInfo::clientQuery:
        return "clientQuery";
    case PlayerAccessInfo::countdown:
        return "countdown";
    case PlayerAccessInfo::date:
        return "date";
    case PlayerAccessInfo::endGame:
        return "endGame";
    case PlayerAccessInfo::flagHistory:
        return "flagHistory";
    case PlayerAccessInfo::flagMaster:
        return "flagMaster";
    case PlayerAccessInfo::flagMod:
        return "flagMod";
    case PlayerAccessInfo::hideAdmin:
        return "hideAdmin";
    case PlayerAccessInfo::idleStats:
        return "idleStats";
    case PlayerAccessInfo::info:
        return "info";
    case PlayerAccessInfo::kick:
        return "kick";
    case PlayerAccessInfo::kill_:
        return "kill";
    case PlayerAccessInfo::lagStats:
        return "lagStats";
    case PlayerAccessInfo::lagwarn:
        return "lagwarn";
    case PlayerAccessInfo::jitterwarn:
        return "jitterwarn";
    case PlayerAccessInfo::listPerms:
        return "listPerms";
    case PlayerAccessInfo::listPlugins:
        return "listPlugins";
    case PlayerAccessInfo::masterBan:
        return "masterban";
    case PlayerAccessInfo::modCount:
        return "modCount";
    case PlayerAccessInfo::mute:
        return "mute";
    case PlayerAccessInfo::packetlosswarn:
        return "packetlosswarn";
    case PlayerAccessInfo::playerList:
        return "playerList";
    case PlayerAccessInfo::plugins:
        return "plugins";
    case PlayerAccessInfo::poll:
        return "poll";
    case PlayerAccessInfo::pollBan:
        return "pollBan";
    case PlayerAccessInfo::pollKick:
        return "pollKick";
    case PlayerAccessInfo::pollKill:
        return "pollKill";
    case PlayerAccessInfo::pollSet:
        return "pollSet";
    case PlayerAccessInfo::pollFlagReset:
        return "pollFlagReset";
    case PlayerAccessInfo::privateMessage:
        return "privateMessage";
    case PlayerAccessInfo::record:
        return "record";
    case PlayerAccessInfo::rejoin:
        return "rejoin";
    case PlayerAccessInfo::removePerms:
        return "removePerms";
    case PlayerAccessInfo::replay:
        return "replay";
    case PlayerAccessInfo::report:
        return "report";
    case PlayerAccessInfo::say:
        return "say";
    case PlayerAccessInfo::sendHelp :
        return "sendHelp";
    case PlayerAccessInfo::setAll:
        return "setAll";
    case PlayerAccessInfo::setPerms:
        return "setPerms";
    case PlayerAccessInfo::setVar:
        return "setVar";
    case PlayerAccessInfo::showAdmin:
        return "showAdmin";
    case PlayerAccessInfo::showMotto:
        return "showMotto";
    case PlayerAccessInfo::showOthers:
        return "showOthers";
    case PlayerAccessInfo::shortBan:
        return "shortBan";
    case PlayerAccessInfo::shutdownServer:
        return "shutdownServer";
    case PlayerAccessInfo::spawn:
        return "spawn";
    case PlayerAccessInfo::superKill:
        return "superKill";
    case PlayerAccessInfo::talk:
        return "talk";
    case PlayerAccessInfo::unban:
        return "unban";
    case PlayerAccessInfo::unmute:
        return "unmute";
    case PlayerAccessInfo::veto:
        return "veto";
    case PlayerAccessInfo::viewReports:
        return "viewReports";
    case PlayerAccessInfo::vote:
        return "vote";
    default:
        return TextUtils::format("UNKNOWN_PERMISSION: %d", (int)perm).c_str();
    };
}

PlayerAccessInfo::AccessPerm permFromName(const std::string &name)
{
    if (name == "ACTIONMESSAGE") return PlayerAccessInfo::actionMessage;
    if (name == "ADMINMESSAGERECEIVE") return PlayerAccessInfo::adminMessageReceive;
    if (name == "ADMINMESSAGESEND") return PlayerAccessInfo::adminMessageSend;
    if (name == "ANTIBAN") return PlayerAccessInfo::antiban;
    if (name == "ANTIKICK") return PlayerAccessInfo::antikick;
    if (name == "ANTIKILL") return PlayerAccessInfo::antikill;
    if (name == "ANTIPOLL") return PlayerAccessInfo::antipoll;
    if (name == "ANTIPOLLBAN") return PlayerAccessInfo::antipollban;
    if (name == "ANTIPOLLKICK") return PlayerAccessInfo::antipollkick;
    if (name == "ANTIPOLLKILL") return PlayerAccessInfo::antipollkill;
    if (name == "BAN") return PlayerAccessInfo::ban;
    if (name == "BANLIST") return PlayerAccessInfo::banlist;
    if (name == "CLIENTQUERY") return PlayerAccessInfo::clientQuery;
    if (name == "COUNTDOWN") return PlayerAccessInfo::countdown;
    if (name == "DATE") return PlayerAccessInfo::date;
    if (name == "ENDGAME") return PlayerAccessInfo::endGame;
    if (name == "FLAGHISTORY") return PlayerAccessInfo::flagHistory;
    if (name == "FLAGMASTER") return PlayerAccessInfo::flagMaster;
    if (name == "FLAGMOD") return PlayerAccessInfo::flagMod;
    if (name == "HIDEADMIN") return PlayerAccessInfo::hideAdmin;
    if (name == "IDLESTATS") return PlayerAccessInfo::idleStats;
    if (name == "INFO") return PlayerAccessInfo::info;
    if (name == "KICK") return PlayerAccessInfo::kick;
    if (name == "KILL") return PlayerAccessInfo::kill_;
    if (name == "LAGSTATS") return PlayerAccessInfo::lagStats;
    if (name == "LAGWARN") return PlayerAccessInfo::lagwarn;
    if (name == "JITTERWARN") return PlayerAccessInfo::jitterwarn;
    if (name == "LISTPERMS") return PlayerAccessInfo::listPerms;
    if (name == "LISTPLUGINS") return PlayerAccessInfo::listPlugins;
    if (name == "MASTERBAN") return PlayerAccessInfo::masterBan;
    if (name == "MODCOUNT") return PlayerAccessInfo::modCount;
    if (name == "MUTE") return PlayerAccessInfo::mute;
    if (name == "PACKETLOSSWARN") return PlayerAccessInfo::packetlosswarn;
    if (name == "PLAYERLIST") return PlayerAccessInfo::playerList;
    if (name == "PLUGINS") return PlayerAccessInfo::plugins;
    if (name == "POLL") return PlayerAccessInfo::poll;
    if (name == "POLLBAN") return PlayerAccessInfo::pollBan;
    if (name == "POLLKICK") return PlayerAccessInfo::pollKick;
    if (name == "POLLKILL") return PlayerAccessInfo::pollKill;
    if (name == "POLLSET") return PlayerAccessInfo::pollSet;
    if (name == "POLLFLAGRESET") return PlayerAccessInfo::pollFlagReset;
    if (name == "PRIVATEMESSAGE") return PlayerAccessInfo::privateMessage;
    if (name == "RECORD") return PlayerAccessInfo::record;
    if (name == "REJOIN") return PlayerAccessInfo::rejoin;
    if (name == "REMOVEPERMS") return PlayerAccessInfo::removePerms;
    if (name == "REPLAY") return PlayerAccessInfo::replay;
    if (name == "REPORT") return PlayerAccessInfo::report;
    if (name == "SAY") return PlayerAccessInfo::say;
    if (name == "SENDHELP") return PlayerAccessInfo::sendHelp;
    if (name == "SETALL") return PlayerAccessInfo::setAll;
    if (name == "SETPERMS") return PlayerAccessInfo::setPerms;
    if (name == "SETVAR") return PlayerAccessInfo::setVar;
    if (name == "SHORTBAN") return PlayerAccessInfo::shortBan;
    if (name == "SHOWADMIN") return PlayerAccessInfo::showAdmin;
    if (name == "SHOWMOTTO") return PlayerAccessInfo::showMotto;
    if (name == "SHOWOTHERS") return PlayerAccessInfo::showOthers;
    if (name == "SHUTDOWNSERVER") return PlayerAccessInfo::shutdownServer;
    if (name == "SPAWN") return PlayerAccessInfo::spawn;
    if (name == "SUPERKILL") return PlayerAccessInfo::superKill;
    if (name == "TALK") return PlayerAccessInfo::talk;
    if (name == "UNBAN") return PlayerAccessInfo::unban;
    if (name == "UNMUTE") return PlayerAccessInfo::unmute;
    if (name == "VETO") return PlayerAccessInfo::veto;
    if (name == "VIEWREPORTS") return PlayerAccessInfo::viewReports;
    if (name == "VOTE") return PlayerAccessInfo::vote;
    return PlayerAccessInfo::lastPerm;
}

// Parse a list of permissions &permissionString and set the corresponding Permissions
// in &info. Return true if a group is referenced but not defined yet, else false.
// return value is only needed for groupdb parsing, not for userdb.
bool parsePermissionString(const std::string &permissionString, PlayerAccessInfo &info)
{
    if (permissionString.length() < 1)
        return false;

    std::istringstream permStream(permissionString);
    std::string word;
    bool invokeGroupdbRecursion = false;

    while (permStream >> word)
    {
        makeupper(word);

        // do we have an operator? check for a leading, non alphabetic character
        char first = '\0';
        if (!TextUtils::isAlphabetic(word[0]))
        {
            first = word[0];
            word.erase(0,1);
        }

        // Operators are not allowed for userdb
        if (!info.groupState.test(PlayerAccessInfo::isGroup) && first != '\0')
        {
            logDebugMessage(1,"userdb: illegal permission string, operators are not allowed in userdb\n");
            return false;
        }

        // if we have an operator, lets handle it
        // operators are only allowed for groups, they make no sense for userdb
        if (info.groupState.test(PlayerAccessInfo::isGroup) && first != '\0')
        {
            switch (first)
            {
            case '*':
            {
                // referenced group
                // don't copy setThis, groups have to be named explicitly for every group
                // to prevent unexpected side effects
                PlayerAccessMap::iterator refgroup = groupAccess.find(word);
                if (refgroup != groupAccess.end())
                {
                    info.explicitAllows |= refgroup->second.explicitAllows;
                    info.explicitDenys |= refgroup->second.explicitDenys;
                }
                else
                    invokeGroupdbRecursion = true;

                continue;
            }

            case '!':
            {
                // forbid a permission
                PlayerAccessInfo::AccessPerm perm = permFromName(word);
                if (perm != PlayerAccessInfo::lastPerm)
                    info.explicitDenys.set(perm);
                else
                {
                    if (word == "ALL")
                    {
                        info.explicitDenys.set();
                        info.explicitDenys[PlayerAccessInfo::lastPerm] = false;
                        info.hasALLPerm = false;
                    }
                    else
                        logDebugMessage(1,"groupdb: Cannot forbid unknown permission %s\n", word.c_str());
                }

                continue;
            }

            case '-':
            {
                // remove a permission
                PlayerAccessInfo::AccessPerm perm = permFromName(word);
                if (perm != PlayerAccessInfo::lastPerm)
                    info.explicitAllows.reset(perm);
                else
                {
                    if (word == "ALL")
                    {
                        info.explicitAllows.reset();
                        info.hasALLPerm = false;
                    }
                    else
                        logDebugMessage(1,"groupdb: Cannot remove unknown permission %s\n", word.c_str());
                }

                continue;
            }

            // + is like no operator, just let it pass through
            case '+':
                break;

            default:
                logDebugMessage(1,"groupdb: ignoring unknown operator type %c\n", first);
            }
        }

        // regular permission
        PlayerAccessInfo::AccessPerm perm = permFromName(word);
        if (perm != PlayerAccessInfo::lastPerm)
            info.explicitAllows.set(perm);
        else
        {
            if (word == "ALL")
            {
                info.explicitAllows.set();
                info.explicitAllows[PlayerAccessInfo::lastPerm] = false;
                info.hasALLPerm = true;
            }
            else
            {
                // Easy access
                std::vector<std::string>& c = info.customPerms;

                // Only store the custom permission if it doesn't exist, in order to prevent duplicates
                if (std::find(c.begin(), c.end(), word) == c.end())
                    c.push_back(word);
            }
        }
    }
    return invokeGroupdbRecursion;
}


bool PlayerAccessInfo::readGroupsFile(const std::string &filename)
{
    std::ifstream in(filename);
    if (!in)
        return false;

    int recursionNeeded = 1;
    bool initialRun = true;

    // read groupdb and check for unsolved group references, read the file again
    // for each of them. One more recursion should make sure everything is fine.
    do
    {
        in.clear();
        in.seekg(0, std::ios::beg);
        int linenum = 0;

        std::string line;
        while (std::getline(in, line))
        {
            linenum++;

            // strip leading whitespace
            line.erase(0, line.find_first_not_of(" \t"));

            // check for a comment string or empty line
            if (line.empty() || line[0] == '#')
                continue;

            makeupper(line);

            std::string::size_type colonpos = line.find(':');
            if (colonpos == std::string::npos)
                logDebugMessage(1,"WARNING: bad groupdb line (%i)\n", linenum);
            else
            {
                std::string name = line.substr(0, colonpos);
                std::string perm = line.substr(colonpos + 1);

                // check if we already have this group, else make a new
                PlayerAccessInfo accessInfo;
                PlayerAccessMap::iterator oldgroup = groupAccess.find(name);
                if (oldgroup != groupAccess.end())
                    accessInfo = oldgroup->second;
                else
                    accessInfo.groupState[PlayerAccessInfo::isGroup] = true;

                // Parse the permission string. If it contains a yet undefined group
                // add a recursion
                if (parsePermissionString(perm, accessInfo) && initialRun)
                    recursionNeeded++;

                accessInfo.verified = true;
                groupAccess[name] = accessInfo;
            }
        }
        initialRun = false;
    }
    while (recursionNeeded--);

    return true;
}

bool PlayerAccessInfo::readPermsFile(const std::string &filename)
{
    std::ifstream in(filename);
    if (!in)
        return false;

    for (;;)
    {
        // 1st line - name
        std::string name;
        if (!std::getline(in, name))
            break;

        PlayerAccessInfo accessInfo;

        // 2nd line - groups
        std::string groupline;
        std::getline(in, groupline); // FIXME -it's an error when line cannot be read
        std::istringstream groupstream(groupline);
        std::string group;
        while (groupstream >> group)
            accessInfo.addGroup(group);

        // 3rd line - allows
        std::string perms;
        std::getline(in, perms);
        parsePermissionString(perms, accessInfo);

        // 4th line - denies
        // FIXME: not nice ... any ideas how to make it better?
        std::getline(in, perms);
        PlayerAccessInfo dummy;
        parsePermissionString(perms, dummy);
        accessInfo.explicitDenys = dummy.explicitAllows;

        userDatabase[name] = accessInfo;
    }

    return true;
}

bool PlayerAccessInfo::writePermsFile(const std::string &filename)
{
    int i;
    std::ofstream out(filename);
    if (!out)
        return false;
    PlayerAccessMap::iterator itr = userDatabase.begin();
    std::vector<std::string>::iterator group;
    while (itr != userDatabase.end())
    {
        out << itr->first << std::endl;
        group = itr->second.groups.begin();
        while (group != itr->second.groups.end())
        {
            out << (*group) << ' ';
            ++group;
        }
        out << std::endl;
        // allows
        for (i = 0; i < PlayerAccessInfo::lastPerm; i++)
            if (itr->second.explicitAllows.test(i))
                out << nameFromPerm((PlayerAccessInfo::AccessPerm) i);
        out << std::endl;
        // denys
        for (i = 0; i < PlayerAccessInfo::lastPerm; i++)
            if (itr->second.explicitDenys.test(i))
                out << nameFromPerm((PlayerAccessInfo::AccessPerm) i);
        out << std::endl;
        ++itr;
    }
    out.close();
    return true;
}

std::string     groupsFile;
std::string     userDatabaseFile;

void PlayerAccessInfo::updateDatabases()
{
    if (userDatabaseFile.size())
        writePermsFile(userDatabaseFile);
}


// Local Variables: ***
// mode: C++ ***
// tab-width: 4 ***
// c-basic-offset: 4 ***
// indent-tabs-mode: nil ***
// End: ***
// ex: shiftwidth=4 tabstop=4
