mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
fix(ws): route plugin broadcasts through the service-layer send check (W2-7)
requireChannelBroadcastAccess went through RequireChannelAccess, whose DM branch checks only participant membership — a blocked user's plugin broadcast could reach the person who blocked them — and it issued a raw GetRoleByID per broadcast, bypassing the permission cache. The gate now delegates to MessageService.CanPost (extracted over checkSendPermission), so DM blocks, channel permissions, and future posting policy apply from exactly one place; fails closed when no service is wired. First brick of the permission-path unification. MemStore.GetDMRecipient gets an honest implementation so the block path is testable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -677,6 +677,19 @@ func (s *MessageService) GetAccessibleChannelIDs(userID int64) ([]int64, error)
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// CanPost reports whether userID may post into channelID, applying the same
|
||||
// checks as a real message send: channel permissions via the cached checker
|
||||
// for regular channels; participant membership AND block status for DMs.
|
||||
// Exists so gates outside the send flow (the plugin broadcast path) share
|
||||
// exactly this policy instead of hand-rolling a weaker copy.
|
||||
func (s *MessageService) CanPost(userID, channelID int64) error {
|
||||
ch, err := s.st.GetChannel(channelID)
|
||||
if err != nil || ch == nil {
|
||||
return fmt.Errorf("%w: channel not found", ErrNotFound)
|
||||
}
|
||||
return s.checkSendPermission(userID, channelID, ch.Type == "dm")
|
||||
}
|
||||
|
||||
// checkSendPermission validates send permission for DM and non-DM channels.
|
||||
func (s *MessageService) checkSendPermission(userID, channelID int64, isDM bool) error {
|
||||
if isDM {
|
||||
|
||||
@@ -673,7 +673,14 @@ func (m *MemStore) GetDMParticipantIDs(channelID int64) ([]int64, error) {
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (m *MemStore) GetDMRecipient(_ int64, _ int64) (*db.User, error) {
|
||||
func (m *MemStore) GetDMRecipient(channelID, userID int64) (*db.User, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for uid := range m.dmParticipants[channelID] {
|
||||
if uid != userID {
|
||||
return m.users[uid], nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,12 @@ package ws
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
"github.com/owncord/server/permissions"
|
||||
"github.com/owncord/server/service"
|
||||
)
|
||||
|
||||
const MsgTypeChatCommand = "chat_command"
|
||||
@@ -100,32 +101,31 @@ func handlePluginCommand(ctx context.Context, h *Hub, c *Client, reqID string, p
|
||||
}
|
||||
|
||||
// requireChannelBroadcastAccess reports whether the client may post to
|
||||
// channelID, mirroring the normal message-send permission path. DM channels are
|
||||
// validated by participant membership; all other channels require
|
||||
// READ_MESSAGES|SEND_MESSAGES. On failure it sends an error to the client and
|
||||
// returns false. Routes through the shared permissions.Checker.RequireChannelAccess
|
||||
// so DM handling matches the rest of the codebase.
|
||||
// channelID, by delegating to the SAME service-layer check a real message
|
||||
// send runs (MessageService.CanPost: cached channel permissions; DM
|
||||
// membership AND DM blocks). The previous RequireChannelAccess route skipped
|
||||
// the block check in its DM branch — a blocked user's plugin broadcast could
|
||||
// reach the person who blocked them — and issued a raw GetRoleByID per
|
||||
// broadcast, bypassing the permission cache. On failure it sends an error to
|
||||
// the client and returns false.
|
||||
func (h *Hub) requireChannelBroadcastAccess(c *Client, channelID int64) bool {
|
||||
if c.user == nil {
|
||||
c.sendMsg(buildErrorMsg(ErrCodeForbidden, "not authenticated"))
|
||||
return false
|
||||
}
|
||||
ch, err := h.db.GetChannel(channelID)
|
||||
if err != nil || ch == nil {
|
||||
c.sendMsg(buildErrorMsg(ErrCodeNotFound, "channel not found"))
|
||||
if h.messageSvc == nil {
|
||||
// No service wired (bare test hub) — fail closed rather than allow
|
||||
// an ungated broadcast.
|
||||
c.sendMsg(buildErrorMsg(ErrCodeForbidden, "broadcast gate unavailable"))
|
||||
return false
|
||||
}
|
||||
role, err := h.db.GetRoleByID(c.user.RoleID)
|
||||
if err != nil || role == nil {
|
||||
c.sendMsg(buildErrorMsg(ErrCodeForbidden, "role not found"))
|
||||
return false
|
||||
}
|
||||
if accessErr := h.permChecker.RequireChannelAccess(
|
||||
c.userID, role.Permissions, role.ID, ch.Type, channelID,
|
||||
permissions.ReadMessages|permissions.SendMessages,
|
||||
); accessErr != nil {
|
||||
if err := h.messageSvc.CanPost(c.userID, channelID); err != nil {
|
||||
if errors.Is(err, service.ErrNotFound) {
|
||||
c.sendMsg(buildErrorMsg(ErrCodeNotFound, "channel not found"))
|
||||
return false
|
||||
}
|
||||
slog.Warn("ws plugin broadcast permission denied",
|
||||
"user_id", c.userID, "channel_id", channelID, "err", accessErr)
|
||||
"user_id", c.userID, "channel_id", channelID, "err", err)
|
||||
c.sendMsg(buildErrorMsg(ErrCodeForbidden, "missing permission to post in this channel"))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -43,6 +43,10 @@ type Hub struct {
|
||||
lkProcess *LiveKitProcess
|
||||
registry *HandlerRegistry
|
||||
permChecker *permissions.Checker
|
||||
// messageSvc gates plugin broadcasts through the same posting policy as a
|
||||
// real message send (permissions, DM membership, DM blocks). Nil only in
|
||||
// bare test hubs; the broadcast gate fails closed then.
|
||||
messageSvc *service.MessageService
|
||||
|
||||
pubsub *PubSub // topic-based pub/sub for O(subscribers) broadcast
|
||||
topicLimiter *TopicRateLimiter // per-topic throughput caps
|
||||
@@ -117,6 +121,7 @@ func NewHub(database *db.DB, limiter *auth.RateLimiter, svc *service.Services) *
|
||||
chatDeps.MessageSvc = svc.Messages
|
||||
presenceDeps.ChannelSvc = svc.Channels
|
||||
reactionDeps.MessageSvc = svc.Messages
|
||||
h.messageSvc = svc.Messages
|
||||
}
|
||||
|
||||
registerChatHandlers(reg, chatDeps)
|
||||
|
||||
Reference in New Issue
Block a user