fix(ws): resolve live role in hasChannelPerm to honor mid-session demotions

hasChannelPerm resolved permissions from the connect-time role snapshot (c.user.RoleID), so a user reassigned to a lower role kept the old role's voice privileges (CONNECT_VOICE and the SPEAK/VIDEO grants in the LiveKit token) until reconnect. Resolve the current role via GetRoleForUser(c.userID), matching the V2 handlers. (Security scan F5)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
J3vb
2026-07-23 11:59:13 +02:00
co-authored by Claude Opus 4.8
parent 420eec227c
commit e98c1d7cbc
3 changed files with 56 additions and 4 deletions
+5
View File
@@ -228,3 +228,8 @@ func (h *Hub) HandleWebhookParticipantLeftForTest(userID int64, channelID int64,
func (h *Hub) MustFullResyncForTest(lastSeq uint64) bool {
return h.mustFullResync(lastSeq)
}
// HasChannelPermForTest exposes Hub.hasChannelPerm for external tests.
func (h *Hub) HasChannelPermForTest(c *Client, channelID, perm int64) bool {
return h.hasChannelPerm(c, channelID, perm)
}
+8 -4
View File
@@ -193,11 +193,15 @@ func (h *Hub) handleMessage(c *Client, raw []byte) {
// hasChannelPerm reports whether the client's role has all the given permission bits.
// Delegates to the unified permissions.Checker.
//
// F5: resolve the user's CURRENT role via GetRoleForUser(c.userID) rather than the
// role snapshotted onto c.user at connect time. A mid-session role reassignment
// (e.g. stripping CONNECT_VOICE) must take effect immediately for the live
// connection — including the SPEAK/VIDEO grants baked into a freshly minted
// LiveKit token — instead of persisting until the user reconnects. This mirrors
// the V2 handlers, which already resolve the live role (deps.go).
func (h *Hub) hasChannelPerm(c *Client, channelID int64, perm int64) bool {
if c.user == nil {
return false
}
role, err := h.db.GetRoleByID(c.user.RoleID)
role, err := h.db.GetRoleForUser(c.userID)
if err != nil || role == nil {
return false
}
+43
View File
@@ -0,0 +1,43 @@
package ws_test
import (
"testing"
"github.com/owncord/server/permissions"
"github.com/owncord/server/ws"
)
// TestHasChannelPerm_UsesLiveRoleNotConnectSnapshot locks F5: hasChannelPerm must
// resolve the user's CURRENT role, not the role snapshotted onto the Client at
// connect time. Otherwise a user reassigned to a lower role mid-session keeps the
// old role's voice privileges (CONNECT_VOICE / the SPEAK/VIDEO grants baked into
// the LiveKit token) until they reconnect.
func TestHasChannelPerm_UsesLiveRoleNotConnectSnapshot(t *testing.T) {
hub, database := newHandlerHub(t)
// Connect-time role: Member (id 4), which carries CONNECT_VOICE.
user := seedMemberUser(t, database, "demoted")
chID := seedTestChannel(t, database, "vc-stale")
// The client's cached user snapshot still points at the Member role — this
// is exactly the stale state the connection holds after a role reassignment.
send := make(chan []byte, 4)
c := ws.NewTestClientWithUser(hub, user, chID, send)
// Admin reassigns the user to a role WITHOUT CONNECT_VOICE. The live WS
// connection is not refreshed, so c.user.RoleID is now stale.
if _, err := database.Exec(
`INSERT INTO roles (id, name, color, permissions, position, is_default)
VALUES (100, 'novoice', NULL, ?, 5, 0)`,
permissions.ReadMessages,
); err != nil {
t.Fatalf("seed novoice role: %v", err)
}
if _, err := database.Exec(`UPDATE users SET role_id = 100 WHERE id = ?`, user.ID); err != nil {
t.Fatalf("reassign user role: %v", err)
}
if hub.HasChannelPermForTest(c, chID, permissions.ConnectVoice) {
t.Fatal("hasChannelPerm granted CONNECT_VOICE from the stale connect-time role; it must use the live role")
}
}