M5: moderation, permissions, channel CRUD, in-app account management
- Server-side moderation & permissions (kick/ban/move/server-mute, channel CRUD). - Database schema v2: channels, bans; BLAKE2b channel passwords, Argon2id accounts. - C ABI additions and client-side handling (vc_kick_user, vc_ban_user, vc_set_permission, vc_set_server_mute, vc_move_user, vc_create/edit/delete_channel, vc_create/reset/delete/list_account). - vccli flags for all M5 operations plus --username/--password auth. - Four new tests covering permissions, kick/ban/move/mute, admin accounts, channel CRUD. - Docs: protocol.md envelope updates, security.md channel-password hashing, PROGRESS.md.
This commit is contained in:
@@ -22,6 +22,22 @@ static voicecat::v1::Envelope make_env(uint64_t req_id = 0) {
|
||||
return e;
|
||||
}
|
||||
|
||||
static voicecat::v1::Permissions all_permissions() {
|
||||
voicecat::v1::Permissions p;
|
||||
p.set_can_create_temp_channel(true);
|
||||
p.set_can_kick(true);
|
||||
p.set_can_ban(true);
|
||||
p.set_can_move_users(true);
|
||||
p.set_can_admin_accounts(true);
|
||||
p.set_is_admin(true);
|
||||
return p;
|
||||
}
|
||||
|
||||
static voicecat::v1::Permissions no_permissions() {
|
||||
voicecat::v1::Permissions p;
|
||||
return p;
|
||||
}
|
||||
|
||||
ConnSession::ConnSession(std::shared_ptr<Database> db,
|
||||
std::shared_ptr<SessionRegistry> registry,
|
||||
std::shared_ptr<voicecat::WorkerPool> workers,
|
||||
@@ -73,7 +89,7 @@ void ConnSession::on_frame(std::vector<uint8_t> frame) {
|
||||
break;
|
||||
case voicecat::v1::Envelope::kLeaveChannel:
|
||||
if (st == State::Authenticated)
|
||||
registry_->set_user_channel(user_id_.load(), 1);
|
||||
handle_leave_channel();
|
||||
break;
|
||||
case voicecat::v1::Envelope::kUdpBinding:
|
||||
if (st == State::Authenticated)
|
||||
@@ -87,6 +103,54 @@ void ConnSession::on_frame(std::vector<uint8_t> frame) {
|
||||
if (st == State::Authenticated)
|
||||
handle_stream_stop(env.stream_stop());
|
||||
break;
|
||||
|
||||
// ── M5 moderation / admin ─────────────────────────────────────────────
|
||||
case voicecat::v1::Envelope::kKick:
|
||||
if (st == State::Authenticated) handle_kick_request(env.request_id(), env.kick());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kBan:
|
||||
if (st == State::Authenticated) handle_ban_request(env.request_id(), env.ban());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kSetPermission:
|
||||
if (st == State::Authenticated)
|
||||
handle_set_permission(env.request_id(), env.set_permission());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kServerMute:
|
||||
if (st == State::Authenticated)
|
||||
handle_server_mute_request(env.request_id(), env.server_mute());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kMoveUser:
|
||||
if (st == State::Authenticated) handle_move_user(env.request_id(), env.move_user());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kCreateChannel:
|
||||
if (st == State::Authenticated)
|
||||
handle_create_channel(env.request_id(), env.create_channel());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kEditChannel:
|
||||
if (st == State::Authenticated)
|
||||
handle_edit_channel(env.request_id(), env.edit_channel());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kDeleteChannel:
|
||||
if (st == State::Authenticated)
|
||||
handle_delete_channel(env.request_id(), env.delete_channel());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kCreateAccount:
|
||||
if (st == State::Authenticated)
|
||||
handle_create_account(env.request_id(), env.create_account());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kResetPassword:
|
||||
if (st == State::Authenticated)
|
||||
handle_reset_password(env.request_id(), env.reset_password());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kDeleteAccount:
|
||||
if (st == State::Authenticated)
|
||||
handle_delete_account(env.request_id(), env.delete_account());
|
||||
break;
|
||||
case voicecat::v1::Envelope::kListAccounts:
|
||||
if (st == State::Authenticated)
|
||||
handle_list_accounts(env.request_id(), env.list_accounts());
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -149,6 +213,27 @@ asio::ip::udp::endpoint ConnSession::udp_endpoint() const {
|
||||
return udp_ep_;
|
||||
}
|
||||
|
||||
// ── Permission helpers ───────────────────────────────────────────────────────
|
||||
|
||||
bool ConnSession::has_permission(bool (voicecat::v1::Permissions::* getter)() const) const {
|
||||
return (permissions_.*getter)();
|
||||
}
|
||||
|
||||
void ConnSession::set_permissions(const voicecat::v1::Permissions& perms) {
|
||||
permissions_ = perms;
|
||||
registry_->set_session_permissions(session_id_, perms);
|
||||
}
|
||||
|
||||
void ConnSession::send_generic_result(uint64_t req_id, bool ok, uint32_t code,
|
||||
const std::string& message) {
|
||||
auto env = make_env(req_id);
|
||||
auto* gr = env.mutable_generic_result();
|
||||
gr->set_ok(ok);
|
||||
gr->set_code(code);
|
||||
gr->set_message(message);
|
||||
send_envelope(env);
|
||||
}
|
||||
|
||||
// ── Handlers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
void ConnSession::handle_client_hello(uint64_t req_id, const voicecat::v1::ClientHello& msg) {
|
||||
@@ -190,6 +275,7 @@ void ConnSession::finish_guest_auth(const voicecat::v1::GuestAuth& guest, uint64
|
||||
send_envelope(env);
|
||||
return;
|
||||
}
|
||||
|
||||
voicecat::v1::User user;
|
||||
user.set_nickname(guest.nickname().empty() ? "Guest" : guest.nickname());
|
||||
user.set_is_guest(true);
|
||||
@@ -200,6 +286,8 @@ void ConnSession::finish_guest_auth(const voicecat::v1::GuestAuth& guest, uint64
|
||||
user_id_.store(uid, std::memory_order_relaxed);
|
||||
state_.store(State::Authenticated, std::memory_order_release);
|
||||
|
||||
permissions_ = no_permissions();
|
||||
registry_->set_session_permissions(session_id_, permissions_);
|
||||
registry_->register_udp_token(udp_token_, session_id_);
|
||||
|
||||
{
|
||||
@@ -208,6 +296,7 @@ void ConnSession::finish_guest_auth(const voicecat::v1::GuestAuth& guest, uint64
|
||||
res->set_ok(true);
|
||||
res->set_session_id(session_id_);
|
||||
*res->mutable_self() = user;
|
||||
*res->mutable_permissions() = permissions_;
|
||||
res->set_udp_token(udp_token_.data(), udp_token_.size());
|
||||
send_envelope(env);
|
||||
}
|
||||
@@ -220,6 +309,15 @@ void ConnSession::finish_password_auth(const std::string& username,
|
||||
// Argon2id runs on the worker pool (deliberately slow).
|
||||
auto self = shared_from_this();
|
||||
workers_->post([self, username, password, req_id] {
|
||||
// M5: check username bans before verifying password.
|
||||
if (self->db_->ban_check("username", username)) {
|
||||
auto env = make_env(req_id);
|
||||
env.mutable_auth_result()->set_ok(false);
|
||||
env.mutable_auth_result()->set_error("account banned");
|
||||
self->send_envelope(env);
|
||||
return;
|
||||
}
|
||||
|
||||
auto acc = self->db_->authenticate(username, password);
|
||||
if (!acc) {
|
||||
auto env = make_env(req_id);
|
||||
@@ -238,6 +336,9 @@ void ConnSession::finish_password_auth(const std::string& username,
|
||||
self->user_id_.store(uid, std::memory_order_relaxed);
|
||||
self->state_.store(State::Authenticated, std::memory_order_release);
|
||||
|
||||
voicecat::v1::Permissions perms = acc->is_admin ? all_permissions() : no_permissions();
|
||||
self->permissions_ = perms;
|
||||
self->registry_->set_session_permissions(self->session_id_, perms);
|
||||
self->registry_->register_udp_token(self->udp_token_, self->session_id_);
|
||||
|
||||
{
|
||||
@@ -246,8 +347,7 @@ void ConnSession::finish_password_auth(const std::string& username,
|
||||
res->set_ok(true);
|
||||
res->set_session_id(self->session_id_);
|
||||
*res->mutable_self() = user;
|
||||
auto* perms = res->mutable_permissions();
|
||||
perms->set_is_admin(acc->is_admin);
|
||||
*res->mutable_permissions() = perms;
|
||||
res->set_udp_token(self->udp_token_.data(), self->udp_token_.size());
|
||||
self->send_envelope(env);
|
||||
}
|
||||
@@ -274,15 +374,75 @@ void ConnSession::broadcast_user_joined(const voicecat::v1::User& user) {
|
||||
|
||||
void ConnSession::handle_join_channel(uint64_t req_id,
|
||||
const voicecat::v1::JoinChannelRequest& msg) {
|
||||
bool ok = registry_->set_user_channel(user_id_.load(), msg.channel_id());
|
||||
uint32_t uid = user_id_.load();
|
||||
auto ch = registry_->get_channel(msg.channel_id());
|
||||
if (!ch) {
|
||||
auto env = make_env(req_id);
|
||||
auto* res = env.mutable_join_channel_result();
|
||||
res->set_ok(false);
|
||||
res->set_error("channel not found");
|
||||
send_envelope(env);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ch->password_protected()) {
|
||||
if (!registry_->check_channel_password(msg.channel_id(), msg.password())) {
|
||||
auto env = make_env(req_id);
|
||||
auto* res = env.mutable_join_channel_result();
|
||||
res->set_ok(false);
|
||||
res->set_error("invalid channel password");
|
||||
send_envelope(env);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (ch->max_users() > 0) {
|
||||
auto members = registry_->find_channel_sessions(msg.channel_id(), session_id_);
|
||||
if (members.size() >= ch->max_users()) {
|
||||
auto env = make_env(req_id);
|
||||
auto* res = env.mutable_join_channel_result();
|
||||
res->set_ok(false);
|
||||
res->set_error("channel is full");
|
||||
send_envelope(env);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool ok = registry_->set_user_channel(uid, msg.channel_id());
|
||||
auto env = make_env(req_id);
|
||||
auto* res = env.mutable_join_channel_result();
|
||||
res->set_ok(ok);
|
||||
if (!ok) res->set_error("channel not found");
|
||||
else res->set_channel_id(msg.channel_id());
|
||||
if (!ok) {
|
||||
res->set_error("channel not found");
|
||||
} else {
|
||||
res->set_channel_id(msg.channel_id());
|
||||
*res->mutable_audio() = ch->audio();
|
||||
|
||||
// Broadcast that this user changed channel.
|
||||
if (auto updated_user = registry_->user_snapshot_user(uid)) {
|
||||
auto bcast = make_env();
|
||||
auto* ue = bcast.mutable_user_event();
|
||||
ue->set_kind(voicecat::v1::UserEvent::UPDATED);
|
||||
*ue->mutable_user() = *updated_user;
|
||||
registry_->broadcast(bcast, session_id_);
|
||||
}
|
||||
}
|
||||
send_envelope(env);
|
||||
}
|
||||
|
||||
void ConnSession::handle_leave_channel() {
|
||||
uint32_t uid = user_id_.load();
|
||||
if (!uid) return;
|
||||
if (!registry_->set_user_channel(uid, 1)) return;
|
||||
if (auto updated_user = registry_->user_snapshot_user(uid)) {
|
||||
auto bcast = make_env();
|
||||
auto* ue = bcast.mutable_user_event();
|
||||
ue->set_kind(voicecat::v1::UserEvent::UPDATED);
|
||||
*ue->mutable_user() = *updated_user;
|
||||
registry_->broadcast(bcast, session_id_);
|
||||
}
|
||||
}
|
||||
|
||||
void ConnSession::handle_text_message(const voicecat::v1::TextMessage& msg) {
|
||||
using namespace std::chrono;
|
||||
int64_t now_ms = duration_cast<milliseconds>(
|
||||
@@ -404,6 +564,170 @@ void ConnSession::handle_stream_stop(const voicecat::v1::StreamStop& msg) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── M5 handlers ──────────────────────────────────────────────────────────────
|
||||
|
||||
void ConnSession::handle_kick_request(uint64_t req_id, const voicecat::v1::KickRequest& msg) {
|
||||
if (!is_admin() && !has_permission(&voicecat::v1::Permissions::can_kick)) {
|
||||
send_generic_result(req_id, false, 6, "permission denied");
|
||||
return;
|
||||
}
|
||||
bool ok = registry_->kick_user(msg.user_id(), msg.reason());
|
||||
send_generic_result(req_id, ok, ok ? 0 : 3, ok ? "" : "user not found");
|
||||
}
|
||||
|
||||
void ConnSession::handle_ban_request(uint64_t req_id, const voicecat::v1::BanRequest& msg) {
|
||||
if (!is_admin() && !has_permission(&voicecat::v1::Permissions::can_ban)) {
|
||||
send_generic_result(req_id, false, 6, "permission denied");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ban by username for persistence (nickname == username for password users).
|
||||
// Also ban by runtime user_id for immediate effect.
|
||||
if (auto target = registry_->find_session_by_user_id(msg.user_id())) {
|
||||
if (!target->user_id()) {
|
||||
send_generic_result(req_id, false, 3, "user not found");
|
||||
return;
|
||||
}
|
||||
if (auto nick = registry_->user_nickname(target->user_id())) {
|
||||
std::string err;
|
||||
db_->ban_create("username", *nick, msg.reason(),
|
||||
static_cast<int64_t>(msg.expires_unix_ms()), err);
|
||||
}
|
||||
}
|
||||
|
||||
bool ok = registry_->ban_user(msg.user_id(), msg.reason(),
|
||||
static_cast<int64_t>(msg.expires_unix_ms()));
|
||||
send_generic_result(req_id, ok, ok ? 0 : 3, ok ? "" : "user not found");
|
||||
}
|
||||
|
||||
void ConnSession::handle_set_permission(uint64_t req_id,
|
||||
const voicecat::v1::SetPermissionRequest& msg) {
|
||||
if (!is_admin() && !has_permission(&voicecat::v1::Permissions::can_admin_accounts)) {
|
||||
send_generic_result(req_id, false, 6, "permission denied");
|
||||
return;
|
||||
}
|
||||
auto target = registry_->find_session_by_user_id(msg.user_id());
|
||||
if (!target) {
|
||||
send_generic_result(req_id, false, 3, "user not online");
|
||||
return;
|
||||
}
|
||||
target->set_permissions(msg.permissions());
|
||||
send_generic_result(req_id, true, 0, "");
|
||||
}
|
||||
|
||||
void ConnSession::handle_server_mute_request(uint64_t req_id,
|
||||
const voicecat::v1::ServerMuteRequest& msg) {
|
||||
if (!is_admin() && !has_permission(&voicecat::v1::Permissions::can_kick)) {
|
||||
send_generic_result(req_id, false, 6, "permission denied");
|
||||
return;
|
||||
}
|
||||
bool ok = registry_->set_server_mute(msg.user_id(), msg.muted(), msg.deafened());
|
||||
send_generic_result(req_id, ok, ok ? 0 : 3, ok ? "" : "user not found");
|
||||
}
|
||||
|
||||
void ConnSession::handle_move_user(uint64_t req_id, const voicecat::v1::MoveUserRequest& msg) {
|
||||
if (!is_admin() && !has_permission(&voicecat::v1::Permissions::can_move_users)) {
|
||||
send_generic_result(req_id, false, 6, "permission denied");
|
||||
return;
|
||||
}
|
||||
bool ok = registry_->move_user(msg.user_id(), msg.channel_id());
|
||||
send_generic_result(req_id, ok, ok ? 0 : 3, ok ? "" : "user or channel not found");
|
||||
}
|
||||
|
||||
void ConnSession::handle_create_channel(uint64_t req_id,
|
||||
const voicecat::v1::CreateChannelRequest& msg) {
|
||||
if (!is_admin() && !has_permission(&voicecat::v1::Permissions::can_create_temp_channel)) {
|
||||
send_generic_result(req_id, false, 6, "permission denied");
|
||||
return;
|
||||
}
|
||||
std::string error;
|
||||
uint32_t id = registry_->create_channel(msg.channel(), msg.password(), error);
|
||||
send_generic_result(req_id, id != 0, id != 0 ? 0 : 3,
|
||||
id != 0 ? "" : (error.empty() ? "create failed" : error));
|
||||
}
|
||||
|
||||
void ConnSession::handle_edit_channel(uint64_t req_id,
|
||||
const voicecat::v1::EditChannelRequest& msg) {
|
||||
if (!is_admin()) {
|
||||
send_generic_result(req_id, false, 6, "permission denied");
|
||||
return;
|
||||
}
|
||||
std::string error;
|
||||
bool ok = registry_->update_channel(msg.channel(), msg.password(), error);
|
||||
send_generic_result(req_id, ok, ok ? 0 : 3,
|
||||
ok ? "" : (error.empty() ? "update failed" : error));
|
||||
}
|
||||
|
||||
void ConnSession::handle_delete_channel(uint64_t req_id,
|
||||
const voicecat::v1::DeleteChannelRequest& msg) {
|
||||
if (!is_admin()) {
|
||||
send_generic_result(req_id, false, 6, "permission denied");
|
||||
return;
|
||||
}
|
||||
std::string error;
|
||||
bool ok = registry_->delete_channel(msg.channel_id(), error);
|
||||
send_generic_result(req_id, ok, ok ? 0 : 3,
|
||||
ok ? "" : (error.empty() ? "delete failed" : error));
|
||||
}
|
||||
|
||||
void ConnSession::handle_create_account(uint64_t req_id,
|
||||
const voicecat::v1::CreateAccountRequest& msg) {
|
||||
if (!is_admin() && !has_permission(&voicecat::v1::Permissions::can_admin_accounts)) {
|
||||
send_generic_result(req_id, false, 6, "permission denied");
|
||||
return;
|
||||
}
|
||||
auto self = shared_from_this();
|
||||
workers_->post([self, req_id, msg]() mutable {
|
||||
std::string error;
|
||||
auto acc = self->db_->create_account(msg.username(), msg.password(), false, error);
|
||||
self->send_generic_result(req_id, acc.has_value(), acc.has_value() ? 0 : 3,
|
||||
acc.has_value() ? "" : error);
|
||||
});
|
||||
}
|
||||
|
||||
void ConnSession::handle_reset_password(uint64_t req_id,
|
||||
const voicecat::v1::ResetPasswordRequest& msg) {
|
||||
if (!is_admin() && !has_permission(&voicecat::v1::Permissions::can_admin_accounts)) {
|
||||
send_generic_result(req_id, false, 6, "permission denied");
|
||||
return;
|
||||
}
|
||||
auto self = shared_from_this();
|
||||
workers_->post([self, req_id, msg]() mutable {
|
||||
std::string error;
|
||||
bool ok = self->db_->reset_password(msg.username(), msg.new_password(), error);
|
||||
self->send_generic_result(req_id, ok, ok ? 0 : 3, ok ? "" : error);
|
||||
});
|
||||
}
|
||||
|
||||
void ConnSession::handle_delete_account(uint64_t req_id,
|
||||
const voicecat::v1::DeleteAccountRequest& msg) {
|
||||
if (!is_admin() && !has_permission(&voicecat::v1::Permissions::can_admin_accounts)) {
|
||||
send_generic_result(req_id, false, 6, "permission denied");
|
||||
return;
|
||||
}
|
||||
std::string error;
|
||||
bool ok = db_->delete_account(msg.username(), error);
|
||||
send_generic_result(req_id, ok, ok ? 0 : 3, ok ? "" : error);
|
||||
}
|
||||
|
||||
void ConnSession::handle_list_accounts(uint64_t req_id,
|
||||
const voicecat::v1::ListAccountsRequest& /*msg*/) {
|
||||
if (!is_admin() && !has_permission(&voicecat::v1::Permissions::can_admin_accounts)) {
|
||||
send_generic_result(req_id, false, 6, "permission denied");
|
||||
return;
|
||||
}
|
||||
auto env = make_env(req_id);
|
||||
auto* lr = env.mutable_list_accounts_result();
|
||||
for (const auto& acc : db_->list_accounts()) {
|
||||
auto* e = lr->add_accounts();
|
||||
e->set_username(acc.username);
|
||||
e->set_is_admin(acc.is_admin);
|
||||
e->set_created_at_unix_ms(static_cast<uint64_t>(acc.created_at) * 1000);
|
||||
e->set_last_login_unix_ms(static_cast<uint64_t>(acc.last_login) * 1000);
|
||||
}
|
||||
send_envelope(env);
|
||||
}
|
||||
|
||||
void ConnSession::send_disconnect_and_close(uint32_t code, const std::string& reason) {
|
||||
auto env = make_env();
|
||||
auto* d = env.mutable_disconnect();
|
||||
|
||||
Reference in New Issue
Block a user