mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
[Server] Gate room channels on privilege levels (#7477)
* [Server] Gate room channels on privilege levels * [Server] Filter channel catalog per user, split read/write gating, add banners - Server_Room::getInfo() now takes the requesting user and only lists channels that user may read. Previously every room member received the full catalog (e.g. the moderators channel) while message delivery was already gated, so restricted channels were visible but silent. - Split channel access into mayReadChannel/mayWriteChannel: history replay, catalog listing, and event fan-out use the read level (access_level); Command_RoomChannelSay uses the write level (write_access_level, falling back to the read level when unset). - Plumb write_access_level and banner_message through both room sources (SQL rooms_channels table and config-file channels via writeaccess and banner keys). - Schema 38 -> 39: add write_access_level (NULL means inherit the read level) and banner_message columns; also widen access_level to the full six-value privilege enum to match the 37 -> 38 migration. * Fixup: evaluate channel access per user flag instead of ladder User flags are independent bits: IsDeveloper does not imply IsModerator/IsAdmin, so developer-only users must not reach moderator/admin channels. Replace the userPrivilegeLevel ladder with hasChannelPrivilege: moderator channels need IsModerator (admins hold it alongside IsAdmin), admin channels need IsAdmin, developer channels need IsDeveloper or IsAdmin, judge channels admit IsJudge/IsModerator. Covers catalog filtering, history replay, write gating and fan-out, and corrects the ini example and proto docs. * [Server] Make channel privilege helpers static per review * [Server] Fail closed on unknown channel access levels per review - roomChannelAccessFromKey returns nullopt for unknown keywords instead of silently falling back to Public - skip channels with unknown read/write levels with qWarning, empty read defaults to Public, empty write means no override - mayWriteChannel now also requires read access so write-below-read configs cannot post into invisible channels * [Server] Remove stray conflict marker in channel replay comment * [Server] Fix servatrice.cpp formatting per clang-format-18 --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de> Co-authored-by: BruebachL <bruebachl@cockatrice.example>
This commit is contained in:
parent
17d2e8d86e
commit
05dcde69db
9 changed files with 211 additions and 29 deletions
|
|
@ -769,7 +769,7 @@ Response::ResponseCode Server_ProtocolHandler::cmdGetGamesOfUser(const Command_G
|
|||
while (roomIterator.hasNext()) {
|
||||
Server_Room *room = roomIterator.next().value();
|
||||
room->gamesLock.lockForRead();
|
||||
room->getInfo(*re->add_room_list(), false, true);
|
||||
room->getInfo(*re->add_room_list(), false, true, true, userInfo);
|
||||
QListIterator<ServerInfo_Game> gameIterator(room->getGamesOfUser(nameFromStdString(cmd.user_name())));
|
||||
while (gameIterator.hasNext()) {
|
||||
re->add_game_list()->CopyFrom(gameIterator.next());
|
||||
|
|
@ -827,7 +827,7 @@ Response::ResponseCode Server_ProtocolHandler::cmdListRooms(const Command_ListRo
|
|||
Event_ListRooms event;
|
||||
QMapIterator<int, Server_Room *> roomIterator(server->getRooms());
|
||||
while (roomIterator.hasNext()) {
|
||||
roomIterator.next().value()->getInfo(*event.add_room_list(), false);
|
||||
roomIterator.next().value()->getInfo(*event.add_room_list(), false, false, true, userInfo);
|
||||
}
|
||||
rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event));
|
||||
|
||||
|
|
@ -871,9 +871,13 @@ Response::ResponseCode Server_ProtocolHandler::cmdJoinRoom(const Command_JoinRoo
|
|||
rc.enqueuePostResponseItem(ServerMessage::ROOM_EVENT, room->prepareRoomEvent(roomChatHistory));
|
||||
}
|
||||
|
||||
// Replay the history of every catalog channel. Privilege filtering for
|
||||
// restricted channels is applied on top of this in the moderator stack.
|
||||
// Replay the history of every catalog channel the joining user may see.
|
||||
// Public channels are replayed to everyone; restricted channels only to
|
||||
// members whose privilege level meets the channel's access level.
|
||||
for (const ServerInfo_RoomChannel &channel : room->getChannels()) {
|
||||
if (!room->mayReadChannel(*userInfo, channel)) {
|
||||
continue;
|
||||
}
|
||||
const QList<ServerInfo_ChatMessage> channelHistory = room->getChatHistory(QString::fromStdString(channel.id()));
|
||||
for (const ServerInfo_ChatMessage &chatMessage : channelHistory) {
|
||||
Event_RoomChannelSay channelChatHistory;
|
||||
|
|
@ -891,7 +895,7 @@ Response::ResponseCode Server_ProtocolHandler::cmdJoinRoom(const Command_JoinRoo
|
|||
rc.enqueuePostResponseItem(ServerMessage::ROOM_EVENT, room->prepareRoomEvent(joinMessageEvent));
|
||||
|
||||
auto *re = new Response_JoinRoom;
|
||||
room->getInfo(*re->mutable_room_info(), true);
|
||||
room->getInfo(*re->mutable_room_info(), true, false, true, userInfo);
|
||||
|
||||
rc.setResponseExtension(re);
|
||||
return Response::RespOk;
|
||||
|
|
@ -986,6 +990,9 @@ Response::ResponseCode Server_ProtocolHandler::cmdRoomChannelSay(const Command_R
|
|||
// The client only ever offers catalog ids; a crafted or stale id is rejected.
|
||||
return Response::RespInvalidCommand;
|
||||
}
|
||||
if (!room->mayWriteChannel(*userInfo, *channel)) {
|
||||
return Response::RespFunctionNotAllowed;
|
||||
}
|
||||
|
||||
if (!addSaidMessageSize(static_cast<int>(cmd.message().size()))) {
|
||||
return Response::RespChatFlood;
|
||||
|
|
|
|||
|
|
@ -107,8 +107,11 @@ Server *Server_Room::getServer() const
|
|||
return static_cast<Server *>(parent());
|
||||
}
|
||||
|
||||
const ServerInfo_Room &
|
||||
Server_Room::getInfo(ServerInfo_Room &result, bool complete, bool showGameTypes, bool includeExternalData) const
|
||||
const ServerInfo_Room &Server_Room::getInfo(ServerInfo_Room &result,
|
||||
bool complete,
|
||||
bool showGameTypes,
|
||||
bool includeExternalData,
|
||||
const ServerInfo_User *forUser) const
|
||||
{
|
||||
result.set_room_id(id);
|
||||
result.set_name(name.toStdString());
|
||||
|
|
@ -158,6 +161,9 @@ Server_Room::getInfo(ServerInfo_Room &result, bool complete, bool showGameTypes,
|
|||
}
|
||||
|
||||
for (const ServerInfo_RoomChannel &channel : channels) {
|
||||
if (forUser != nullptr && !mayReadChannel(*forUser, channel)) {
|
||||
continue;
|
||||
}
|
||||
result.add_channel_list()->CopyFrom(channel);
|
||||
}
|
||||
|
||||
|
|
@ -179,6 +185,43 @@ const ServerInfo_RoomChannel *Server_Room::findChannel(const QString &channelId)
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
bool Server_Room::hasChannelPrivilege(const ServerInfo_User &userInfo,
|
||||
ServerInfo_RoomChannel::AccessLevel requiredLevel)
|
||||
{
|
||||
// User flags are independent bits, not a ladder: holding IsDeveloper does
|
||||
// not imply IsModerator or IsAdmin. The database grants admins the admin
|
||||
// and the moderator bit together, so admins still reach moderator (and
|
||||
// judge) channels; moderators likewise reach judge channels.
|
||||
const uint32_t userLevel = userInfo.user_level();
|
||||
switch (requiredLevel) {
|
||||
case ServerInfo_RoomChannel::Public:
|
||||
return true;
|
||||
case ServerInfo_RoomChannel::Registered:
|
||||
return userLevel & ServerInfo_User::IsRegistered;
|
||||
case ServerInfo_RoomChannel::Judge:
|
||||
return userLevel & (ServerInfo_User::IsJudge | ServerInfo_User::IsModerator);
|
||||
case ServerInfo_RoomChannel::Moderator:
|
||||
return userLevel & ServerInfo_User::IsModerator;
|
||||
case ServerInfo_RoomChannel::Admin:
|
||||
return userLevel & ServerInfo_User::IsAdmin;
|
||||
case ServerInfo_RoomChannel::Developer:
|
||||
return userLevel & (ServerInfo_User::IsDeveloper | ServerInfo_User::IsAdmin);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Server_Room::mayReadChannel(const ServerInfo_User &userInfo, const ServerInfo_RoomChannel &channel)
|
||||
{
|
||||
return hasChannelPrivilege(userInfo, channel.access_level());
|
||||
}
|
||||
|
||||
bool Server_Room::mayWriteChannel(const ServerInfo_User &userInfo, const ServerInfo_RoomChannel &channel)
|
||||
{
|
||||
const auto writeLevel = channel.has_write_access_level() ? channel.write_access_level() : channel.access_level();
|
||||
return mayReadChannel(userInfo, channel) && hasChannelPrivilege(userInfo, writeLevel);
|
||||
}
|
||||
|
||||
QList<ServerInfo_ChatMessage> Server_Room::getChatHistory(const QString &channelId) const
|
||||
{
|
||||
return channelChatHistory.value(isMainChannel(channelId) ? MAIN_CHANNEL_KEY : channelId);
|
||||
|
|
@ -342,18 +385,31 @@ Response::ResponseCode Server_Room::processJoinGameCommand(const Command_JoinGam
|
|||
void Server_Room::say(const QString &userName, const QString &userMessage, const QString &channelId, bool sendToIsl)
|
||||
{
|
||||
const bool isMain = isMainChannel(channelId);
|
||||
ServerInfo_RoomChannel::AccessLevel accessLevel = ServerInfo_RoomChannel::Public;
|
||||
if (!isMain) {
|
||||
const ServerInfo_RoomChannel *channel = findChannel(channelId);
|
||||
if (channel == nullptr) {
|
||||
// The channel was not whitelisted against the catalog; drop the message.
|
||||
return;
|
||||
}
|
||||
accessLevel = channel->access_level();
|
||||
// V1 does not relay restricted channels across ISL (plan N5).
|
||||
if (accessLevel != ServerInfo_RoomChannel::Public) {
|
||||
sendToIsl = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isMain) {
|
||||
Event_RoomSay event;
|
||||
event.set_name(userName.toStdString());
|
||||
event.set_message(userMessage.toStdString());
|
||||
sendRoomEvent(prepareRoomEvent(event), sendToIsl);
|
||||
sendRoomEvent(prepareRoomEvent(event), sendToIsl, accessLevel);
|
||||
} else {
|
||||
Event_RoomChannelSay event;
|
||||
event.set_channel_id(channelId.toStdString());
|
||||
event.set_name(userName.toStdString());
|
||||
event.set_message(userMessage.toStdString());
|
||||
sendRoomEvent(prepareRoomEvent(event), sendToIsl);
|
||||
sendRoomEvent(prepareRoomEvent(event), sendToIsl, accessLevel);
|
||||
}
|
||||
|
||||
if (chatHistorySize != 0) {
|
||||
|
|
@ -405,13 +461,17 @@ void Server_Room::removeSaidMessages(const QString &userName, int amount, bool s
|
|||
}
|
||||
}
|
||||
|
||||
void Server_Room::sendRoomEvent(RoomEvent *event, bool sendToIsl)
|
||||
void Server_Room::sendRoomEvent(RoomEvent *event, bool sendToIsl, ServerInfo_RoomChannel::AccessLevel accessLevel)
|
||||
{
|
||||
usersLock.lockForRead();
|
||||
{
|
||||
QMapIterator<QString, Server_ProtocolHandler *> userIterator(users);
|
||||
while (userIterator.hasNext()) {
|
||||
userIterator.next().value()->sendProtocolItem(*event);
|
||||
Server_ProtocolHandler *user = userIterator.next().value();
|
||||
if (!hasChannelPrivilege(*user->getUserInfo(), accessLevel)) {
|
||||
continue;
|
||||
}
|
||||
user->sendProtocolItem(*event);
|
||||
}
|
||||
}
|
||||
usersLock.unlock();
|
||||
|
|
|
|||
|
|
@ -119,6 +119,24 @@ public:
|
|||
const ServerInfo_RoomChannel *findChannel(const QString &channelId) const;
|
||||
/** @brief True when @p channelId refers to the implicit Main channel. */
|
||||
static bool isMainChannel(const QString &channelId);
|
||||
/**
|
||||
* @brief True when the flags in @p userInfo satisfy @p requiredLevel.
|
||||
*
|
||||
* User flags are independent bits, not a ladder: IsDeveloper does not
|
||||
* imply IsModerator or IsAdmin, so developer-only users cannot read or
|
||||
* post in moderator/admin channels. Admins hold the moderator bit
|
||||
* alongside the admin bit and moderators are admitted to judge channels.
|
||||
*/
|
||||
static bool hasChannelPrivilege(const ServerInfo_User &userInfo, ServerInfo_RoomChannel::AccessLevel requiredLevel);
|
||||
/** @brief True when @p userInfo may read @p channel. */
|
||||
static bool mayReadChannel(const ServerInfo_User &userInfo, const ServerInfo_RoomChannel &channel);
|
||||
/**
|
||||
* @brief True when @p userInfo may write to @p channel.
|
||||
*
|
||||
* Writing requires the channel's write level, or — when no explicit write
|
||||
* level is configured — the same level as reading.
|
||||
*/
|
||||
static bool mayWriteChannel(const ServerInfo_User &userInfo, const ServerInfo_RoomChannel &channel);
|
||||
const QMap<int, Server_Game *> &getGames() const
|
||||
{
|
||||
return games;
|
||||
|
|
@ -128,8 +146,11 @@ public:
|
|||
return externalGames;
|
||||
}
|
||||
Server *getServer() const;
|
||||
const ServerInfo_Room &
|
||||
getInfo(ServerInfo_Room &result, bool complete, bool showGameTypes = false, bool includeExternalData = true) const;
|
||||
const ServerInfo_Room &getInfo(ServerInfo_Room &result,
|
||||
bool complete,
|
||||
bool showGameTypes = false,
|
||||
bool includeExternalData = true,
|
||||
const ServerInfo_User *forUser = nullptr) const;
|
||||
int getGamesCreatedByUser(const QString &name) const;
|
||||
QList<ServerInfo_Game> getGamesOfUser(const QString &name) const;
|
||||
QList<ServerInfo_ChatMessage> getChatHistory(const QString &channelId) const;
|
||||
|
|
@ -155,7 +176,9 @@ public:
|
|||
void addGame(Server_Game *game);
|
||||
void removeGame(Server_Game *game);
|
||||
|
||||
void sendRoomEvent(RoomEvent *event, bool sendToIsl = true);
|
||||
void sendRoomEvent(RoomEvent *event,
|
||||
bool sendToIsl = true,
|
||||
ServerInfo_RoomChannel::AccessLevel accessLevel = ServerInfo_RoomChannel::Public);
|
||||
RoomEvent *prepareRoomEvent(const ::google::protobuf::Message &roomEvent);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue