mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
channel_create and channel_update were handed to BroadcastToAll and enqueued with channelID 0, so the full channel payload -- name, topic and category of a channel that channel_overrides hides from the recipient's role -- went to every connected client and was replayed unconditionally from the ring buffer. Both now resolve an audience through the same READ_MESSAGES helper the voice path uses and enqueue under the real channel id, which filters live delivery and both replay tiers by one mechanism. channel_delete stays unfiltered by design: the row is already gone, so a check there would strand the channel in the sidebar of users who saw it via a positive override. Verified by a panel of agents; a base-revert control fails on both the live leak and the replay leak, while the pre-existing broadcast tests pass unmodified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
113 lines
4.3 KiB
Go
113 lines
4.3 KiB
Go
package ws_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/owncord/server/db"
|
|
"github.com/owncord/server/permissions"
|
|
"github.com/owncord/server/ws"
|
|
)
|
|
|
|
// countChannelMetaFor counts channel_create / channel_update events for a
|
|
// specific channel id in a batch of raw WS frames.
|
|
func countChannelMetaFor(msgs [][]byte, channelID int64) int {
|
|
n := 0
|
|
for _, m := range msgs {
|
|
var env struct {
|
|
Type string `json:"type"`
|
|
Payload struct {
|
|
ID int64 `json:"id"`
|
|
} `json:"payload"`
|
|
}
|
|
if json.Unmarshal(m, &env) != nil {
|
|
continue
|
|
}
|
|
if env.Payload.ID != channelID {
|
|
continue
|
|
}
|
|
if env.Type == "channel_create" || env.Type == "channel_update" {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// TestChannelMetadata_NotDeliveredToRolesDeniedRead locks the visibility
|
|
// invariant for channel metadata: channel_create / channel_update used to go
|
|
// out via BroadcastToAll, so every authenticated client learned the name,
|
|
// category and topic of a channel that channel_overrides hides from their role —
|
|
// live, and again on reconnect, since an event stored under channelID 0 is
|
|
// replayed unconditionally. A role that may READ the channel must still receive
|
|
// both, live and on replay.
|
|
func TestChannelMetadata_NotDeliveredToRolesDeniedRead(t *testing.T) {
|
|
hub, database := newHandlerHub(t)
|
|
|
|
pubID := seedTestChannel(t, database, "chmeta-general")
|
|
privID := seedTestChannel(t, database, "chmeta-leadership")
|
|
|
|
insider := seedMemberUser(t, database, "chmeta-insider") // role 4: base READ_MESSAGES
|
|
outsider := seedModUser(t, database, "chmeta-outsider") // role 3: denied READ below
|
|
if err := database.UpsertChannelOverride(
|
|
context.Background(), privID, outsider.RoleID, 0, permissions.ReadMessages,
|
|
); err != nil {
|
|
t.Fatalf("UpsertChannelOverride: %v", err)
|
|
}
|
|
|
|
insiderSend := make(chan []byte, 64)
|
|
outsiderSend := make(chan []byte, 64)
|
|
hub.Register(ws.NewTestClientWithUser(hub, insider, 0, insiderSend))
|
|
hub.Register(ws.NewTestClientWithUser(hub, outsider, 0, outsiderSend))
|
|
time.Sleep(30 * time.Millisecond)
|
|
|
|
pub := &db.Channel{ID: pubID, Name: "chmeta-general", Type: "text", Category: "Text"}
|
|
priv := &db.Channel{
|
|
ID: privID, Name: "chmeta-leadership", Type: "text",
|
|
Category: "Staff", Topic: "acquisition talks",
|
|
}
|
|
hub.BroadcastChannelCreate(pub)
|
|
hub.BroadcastChannelUpdate(pub)
|
|
hub.BroadcastChannelCreate(priv)
|
|
hub.BroadcastChannelUpdate(priv)
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
// ── live delivery ─────────────────────────────────────────────────────────
|
|
insiderLive := drainChanTimeout(insiderSend, 200*time.Millisecond)
|
|
outsiderLive := drainChanTimeout(outsiderSend, 200*time.Millisecond)
|
|
|
|
if got := countChannelMetaFor(insiderLive, privID); got != 2 {
|
|
t.Errorf("insider received %d channel_create/channel_update for the private channel, want 2", got)
|
|
}
|
|
// Positive control: the outsider is connected and receiving, so a zero count
|
|
// on the private channel is filtering and not a broken delivery path.
|
|
if got := countChannelMetaFor(outsiderLive, pubID); got != 2 {
|
|
t.Errorf("outsider received %d channel_create/channel_update for the readable channel, want 2", got)
|
|
}
|
|
if got := countChannelMetaFor(outsiderLive, privID); got != 0 {
|
|
t.Errorf("a role denied READ received %d channel metadata events for the private channel, want 0", got)
|
|
}
|
|
|
|
// ── reconnect replay ──────────────────────────────────────────────────────
|
|
oldest := hub.ReplayBuffer().OldestSeq()
|
|
if oldest == 0 {
|
|
t.Fatal("replay buffer recorded no channel events (oldest seq is 0)")
|
|
}
|
|
replayFor := func(u *db.User) [][]byte {
|
|
t.Helper()
|
|
allowed, err := hub.ComputeAllowedChannelsForTest(database, u)
|
|
if err != nil {
|
|
t.Fatalf("ComputeAllowedChannelsForTest: %v", err)
|
|
}
|
|
return hub.ReplayBuffer().EventsSinceFiltered(oldest+1, allowed)
|
|
}
|
|
|
|
if got := countChannelMetaFor(replayFor(insider), privID); got != 2 {
|
|
t.Errorf("insider replay contained %d channel metadata events for the private channel, want 2", got)
|
|
}
|
|
if got := countChannelMetaFor(replayFor(outsider), privID); got != 0 {
|
|
t.Errorf("replay leaked %d channel metadata events for the private channel to a role denied READ, want 0", got)
|
|
}
|
|
}
|