fix(gateway): READY отдаёт права участника вместо пустого списка
Снапшот READY инициализировал my_permissions пустым списком и нигде его не заполнял, поэтому у всех — включая владельца сервера — клиент выключал композер («В эту комнату нельзя писать»), прятал шестерёнку, приглашения и кнопку саундборда: состав прав в снапшоте не совпадал с REST (GET /users/@me/guilds отдавал полный список). Права считает тот же движок и тот же кэш, что REST (permissions.Calculator, переданный в NewSnapshot), а инстанс-админ получает максимальный набор даже на чужом сервере, где он не участник (AGENT.md 7.19, 11.6). Тесты: TestReadyCarriesMyPermissions (непустые права роли по умолчанию, VIEW_CHANNEL/SEND_MESSAGES и согласие с can_send комнаты) и TestReadyGivesInstanceAdminAllPermissions.
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -214,6 +215,71 @@ func TestHelloAndReady(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReadyCarriesMyPermissions проверяет, что снапшот несёт права участника:
|
||||||
|
// пустой `my_permissions` выключал композер и управление сервером у всех,
|
||||||
|
// включая владельца (AGENT.md 11.6).
|
||||||
|
func TestReadyCarriesMyPermissions(t *testing.T) {
|
||||||
|
f := newFixture(t)
|
||||||
|
token := registerUser(t, f, "perm_user", "perm@example.com")
|
||||||
|
joinMainGuildForTest(t, f, token)
|
||||||
|
|
||||||
|
conn, _ := f.dial(t)
|
||||||
|
send(t, conn, gateway.OpIdentify, map[string]any{"token": token})
|
||||||
|
|
||||||
|
var snapshot gateway.Ready
|
||||||
|
if err := json.Unmarshal(readEnvelope(t, conn).D, &snapshot); err != nil {
|
||||||
|
t.Fatalf("decode READY: %v", err)
|
||||||
|
}
|
||||||
|
perms := snapshot.Guilds[0].MyPerms
|
||||||
|
if len(perms) == 0 {
|
||||||
|
t.Fatal("READY must carry my_permissions of the default role")
|
||||||
|
}
|
||||||
|
// Роль @user даёт эти права каждому участнику (AGENT.md 6.3).
|
||||||
|
for _, want := range []string{"VIEW_CHANNEL", "SEND_MESSAGES"} {
|
||||||
|
if !slices.Contains(perms, want) {
|
||||||
|
t.Fatalf("my_permissions = %v, want %s", perms, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !snapshot.Guilds[0].Channels[0].CanSend {
|
||||||
|
t.Fatal("my_permissions must agree with channel can_send")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReadyGivesInstanceAdminAllPermissions: у инстанс-админа права
|
||||||
|
// максимальные даже на чужом сервере, где он не участник (AGENT.md 7.19).
|
||||||
|
func TestReadyGivesInstanceAdminAllPermissions(t *testing.T) {
|
||||||
|
f := newFixture(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
token := registerUser(t, f, "admin_user", "admin@example.com")
|
||||||
|
// Инстанс-админ — участник главного сервера: список серверов берётся из
|
||||||
|
// членства, а права на них максимальные.
|
||||||
|
joinMainGuildForTest(t, f, token)
|
||||||
|
user, _, err := f.auth.ResolveSession(ctx, token)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveSession: %v", err)
|
||||||
|
}
|
||||||
|
if err := f.store.SetInstanceAdmin(ctx, user.ID, true); err != nil {
|
||||||
|
t.Fatalf("SetInstanceAdmin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, _ := f.dial(t)
|
||||||
|
send(t, conn, gateway.OpIdentify, map[string]any{"token": token})
|
||||||
|
|
||||||
|
var snapshot gateway.Ready
|
||||||
|
if err := json.Unmarshal(readEnvelope(t, conn).D, &snapshot); err != nil {
|
||||||
|
t.Fatalf("decode READY: %v", err)
|
||||||
|
}
|
||||||
|
if len(snapshot.Guilds) != 1 {
|
||||||
|
t.Fatalf("instance admin must see the guild, got %d", len(snapshot.Guilds))
|
||||||
|
}
|
||||||
|
perms := snapshot.Guilds[0].MyPerms
|
||||||
|
for _, want := range []string{"ADMINISTRATOR", "SEND_MESSAGES", "MANAGE_ROLES", "BAN_MEMBERS"} {
|
||||||
|
if !slices.Contains(perms, want) {
|
||||||
|
t.Fatalf("my_permissions инстанс-админа = %v, want %s", perms, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestReadyHidesPrivateChannels(t *testing.T) {
|
func TestReadyHidesPrivateChannels(t *testing.T) {
|
||||||
f := newFixture(t)
|
f := newFixture(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func (s *Snapshot) buildGuild(ctx context.Context, guild store.Guild, user *stor
|
|||||||
Roles: []ReadyRole{},
|
Roles: []ReadyRole{},
|
||||||
MemberIDs: []string{},
|
MemberIDs: []string{},
|
||||||
MyRoles: []string{},
|
MyRoles: []string{},
|
||||||
MyPerms: []string{},
|
MyPerms: readyPermissions(resolved),
|
||||||
Emojis: []ReadyEmoji{},
|
Emojis: []ReadyEmoji{},
|
||||||
VoiceStates: []ReadyVoiceState{},
|
VoiceStates: []ReadyVoiceState{},
|
||||||
Sounds: []ReadySound{},
|
Sounds: []ReadySound{},
|
||||||
@@ -257,6 +257,18 @@ func (s *Snapshot) buildGuild(ctx context.Context, guild store.Guild, user *stor
|
|||||||
return readyGuild, nil
|
return readyGuild, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readyPermissions отдаёт права пользователя на сервере списком имён — тем же
|
||||||
|
// движком и в том же виде, что REST (`permissions.Names`). Без них клиент
|
||||||
|
// выключал композер и прятал управление сервером (AGENT.md 11.6).
|
||||||
|
func readyPermissions(resolved permissions.Resolved) []string {
|
||||||
|
// У инстанс-админа права максимальные на любом сервере, включая чужой, где
|
||||||
|
// он не участник (AGENT.md 7.19): в маске прав роли их нет.
|
||||||
|
if resolved.IsInstanceAdmin {
|
||||||
|
return permissions.Names(permissions.AllPermissions)
|
||||||
|
}
|
||||||
|
return permissions.Names(resolved.Guild)
|
||||||
|
}
|
||||||
|
|
||||||
func channelPayload(channel store.Channel, canSend, canConnect bool) ReadyChannel {
|
func channelPayload(channel store.Channel, canSend, canConnect bool) ReadyChannel {
|
||||||
payload := ReadyChannel{
|
payload := ReadyChannel{
|
||||||
ID: formatID(channel.ID),
|
ID: formatID(channel.ID),
|
||||||
|
|||||||
Reference in New Issue
Block a user