mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Fixes all 109 golangci-lint findings (106 contextcheck, 1 gocritic,
2 gosec) that accumulated after D2 wired dbgen (whose queries take ctx)
under ctx-less db.DB wrappers while CI lint was quota-dead. No nolint
comments added; every finding fixed by genuinely threading context.
- db: all 138 hand-written db.DB methods take ctx first; the dbCtx()
Background shim is deleted; raw Query/QueryRow/Exec/Begin use their
Context variants; the four redundant ctx-less passthroughs removed.
db.Auditor/WriteAudit gain ctx.
- Seams: permissions.Checker (DB iface, HasChannelPerm,
RequireChannelAccess) and the service.Store interface mirror the new
signatures (ws.EventStore and plugin.PluginStore already did).
- Callers: api/admin handlers use r.Context(); ws per-message paths use
the connection ctx via DispatchV2; hub loops and startup wiring use
context.Background(); service methods thread ctx where they have one
and Background where no ctx exists. Public service surface reached by
ctx-holding chains (PermissionService.HasChannelPerm/GetRoleForUser/
RequireChannelAccess, message/dm/block/invite/profile methods) is now
ctx-first.
- Detached (context.WithoutCancel) where cancellation would break an
invariant, found by a 3-lens adversarial review of the diff:
* voice-leave background retries (a dead webhook/connection ctx killed
retry 2 before it ran, leaving ghost capacity-holding voice rows)
* rollbackVoiceJoin's compensating delete (its trigger IS the cancel)
* post-2FA-change DeleteOtherSessions and logout DeleteSession (the
security tail of a committed change must not die with the request)
* all api/ws audit writes (a banned user could suppress their own
login_blocked_banned row by aborting the request mid-bcrypt)
* admin backup VACUUM INTO (an interrupt left a truncated .db that
the backup list presented as restorable)
* post-commit message/edit refetches (a committed message must still
fan out when the sender disconnects)
* hub settings-cache refresh (one dead connection could pin stale
values for the 30s TTL)
- gocritic rangeValCopy fixed (index iteration); gosec G306 excluded in
config with justification (generated source must stay world-readable)
instead of flipping genprotocol output to 0o600.
Verified: gofmt/vet, all four build-tag variants, full suite, deadlock
pass, full -race pass, golangci-lint 0 issues uncapped.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
412 lines
10 KiB
Go
412 lines
10 KiB
Go
package permissions
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
// ─── Mock DB ────────────────────────────────────────────────────────────────
|
|
|
|
type mockDB struct {
|
|
channelPerms map[chanRoleKey]chanPerm
|
|
dmParticipants map[dmKey]bool
|
|
chanErr error
|
|
dmErr error
|
|
}
|
|
|
|
type (
|
|
chanRoleKey struct{ channelID, roleID int64 }
|
|
chanPerm struct{ allow, deny int64 }
|
|
dmKey struct{ userID, channelID int64 }
|
|
)
|
|
|
|
func newMockDB() *mockDB {
|
|
return &mockDB{
|
|
channelPerms: make(map[chanRoleKey]chanPerm),
|
|
dmParticipants: make(map[dmKey]bool),
|
|
}
|
|
}
|
|
|
|
func (m *mockDB) GetChannelPermissions(_ context.Context, channelID, roleID int64) (int64, int64, error) {
|
|
if m.chanErr != nil {
|
|
return 0, 0, m.chanErr
|
|
}
|
|
key := chanRoleKey{channelID, roleID}
|
|
p, ok := m.channelPerms[key]
|
|
if !ok {
|
|
return 0, 0, nil
|
|
}
|
|
return p.allow, p.deny, nil
|
|
}
|
|
|
|
func (m *mockDB) IsDMParticipant(_ context.Context, userID, channelID int64) (bool, error) {
|
|
if m.dmErr != nil {
|
|
return false, m.dmErr
|
|
}
|
|
return m.dmParticipants[dmKey{userID, channelID}], nil
|
|
}
|
|
|
|
// ─── HasChannelPerm tests ───────────────────────────────────────────────────
|
|
|
|
func TestHasChannelPerm(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rolePerms int64
|
|
roleID int64
|
|
channelID int64
|
|
perm int64
|
|
overrides map[chanRoleKey]chanPerm
|
|
chanErr error
|
|
want bool
|
|
}{
|
|
{
|
|
name: "admin bypass returns true",
|
|
rolePerms: Administrator | SendMessages,
|
|
roleID: 1,
|
|
channelID: 10,
|
|
perm: ManageChannels,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "non-admin with allow override returns true",
|
|
rolePerms: ReadMessages,
|
|
roleID: 4,
|
|
channelID: 10,
|
|
perm: SendMessages,
|
|
overrides: map[chanRoleKey]chanPerm{
|
|
{10, 4}: {allow: SendMessages, deny: 0},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "non-admin with deny override returns false",
|
|
rolePerms: ReadMessages | SendMessages,
|
|
roleID: 4,
|
|
channelID: 10,
|
|
perm: SendMessages,
|
|
overrides: map[chanRoleKey]chanPerm{
|
|
{10, 4}: {allow: 0, deny: SendMessages},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "non-admin without override uses base perms",
|
|
rolePerms: ReadMessages | SendMessages,
|
|
roleID: 4,
|
|
channelID: 10,
|
|
perm: SendMessages,
|
|
want: true,
|
|
},
|
|
{
|
|
name: "non-admin lacking base perm returns false",
|
|
rolePerms: ReadMessages,
|
|
roleID: 4,
|
|
channelID: 10,
|
|
perm: SendMessages,
|
|
want: false,
|
|
},
|
|
{
|
|
name: "db error returns false",
|
|
rolePerms: ReadMessages | SendMessages,
|
|
roleID: 4,
|
|
channelID: 10,
|
|
perm: SendMessages,
|
|
chanErr: errors.New("db error"),
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
db := newMockDB()
|
|
db.chanErr = tt.chanErr
|
|
for k, v := range tt.overrides {
|
|
db.channelPerms[k] = v
|
|
}
|
|
ck := NewChecker(db)
|
|
|
|
got := ck.HasChannelPerm(context.Background(), tt.rolePerms, tt.roleID, tt.channelID, tt.perm)
|
|
if got != tt.want {
|
|
t.Errorf("HasChannelPerm() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ─── HasChannelPermBatch tests ──────────────────────────────────────────────
|
|
|
|
func TestHasChannelPermBatch(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
rolePerms int64
|
|
overrides map[int64]ChannelOverride
|
|
channelID int64
|
|
perm int64
|
|
want bool
|
|
}{
|
|
{
|
|
name: "admin bypass returns true",
|
|
rolePerms: Administrator,
|
|
channelID: 10,
|
|
perm: ManageChannels,
|
|
overrides: map[int64]ChannelOverride{},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "uses pre-fetched allow override",
|
|
rolePerms: ReadMessages,
|
|
channelID: 10,
|
|
perm: SendMessages,
|
|
overrides: map[int64]ChannelOverride{
|
|
10: {Allow: SendMessages, Deny: 0},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "uses pre-fetched deny override",
|
|
rolePerms: ReadMessages | SendMessages,
|
|
channelID: 10,
|
|
perm: SendMessages,
|
|
overrides: map[int64]ChannelOverride{
|
|
10: {Allow: 0, Deny: SendMessages},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
name: "missing override uses base perms (zero-value)",
|
|
rolePerms: ReadMessages | SendMessages,
|
|
channelID: 99,
|
|
perm: SendMessages,
|
|
overrides: map[int64]ChannelOverride{},
|
|
want: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ck := NewChecker(newMockDB())
|
|
got := ck.HasChannelPermBatch(tt.rolePerms, tt.overrides, tt.channelID, tt.perm)
|
|
if got != tt.want {
|
|
t.Errorf("HasChannelPermBatch() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ─── VisibleChannelIDs tests ────────────────────────────────────────────────
|
|
|
|
func TestVisibleChannelIDs(t *testing.T) {
|
|
channels := []ChannelRef{
|
|
{ID: 1, Type: "text"},
|
|
{ID: 2, Type: "announcement"},
|
|
{ID: 3, Type: "voice"},
|
|
{ID: 4, Type: "dm"}, // always skipped
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
rolePerms int64
|
|
overrides map[int64]ChannelOverride
|
|
want map[int64]bool
|
|
}{
|
|
{
|
|
name: "admin sees every non-dm channel",
|
|
rolePerms: Administrator,
|
|
overrides: nil,
|
|
want: map[int64]bool{1: true, 2: true, 3: true},
|
|
},
|
|
{
|
|
name: "base ReadMessages inherits to all non-dm channels",
|
|
rolePerms: ReadMessages,
|
|
overrides: nil,
|
|
want: map[int64]bool{1: true, 2: true, 3: true},
|
|
},
|
|
{
|
|
name: "deny override hides a single channel",
|
|
rolePerms: ReadMessages,
|
|
overrides: map[int64]ChannelOverride{2: {Deny: ReadMessages}},
|
|
want: map[int64]bool{1: true, 3: true},
|
|
},
|
|
{
|
|
name: "allow override grants read to a role that otherwise lacks it",
|
|
rolePerms: 0,
|
|
overrides: map[int64]ChannelOverride{3: {Allow: ReadMessages}},
|
|
want: map[int64]bool{3: true},
|
|
},
|
|
{
|
|
name: "nil/zero role with no overrides sees nothing",
|
|
rolePerms: 0,
|
|
overrides: nil,
|
|
want: map[int64]bool{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ck := NewChecker(newMockDB())
|
|
got := ck.VisibleChannelIDs(tt.rolePerms, channels, tt.overrides)
|
|
if got[4] {
|
|
t.Errorf("dm channel 4 must never be visible, got %v", got)
|
|
}
|
|
if len(got) != len(tt.want) {
|
|
t.Fatalf("VisibleChannelIDs() = %v, want %v", got, tt.want)
|
|
}
|
|
for id := range tt.want {
|
|
if !got[id] {
|
|
t.Errorf("VisibleChannelIDs() missing channel %d; got %v", id, got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ─── RequireChannelAccess tests ─────────────────────────────────────────────
|
|
|
|
func TestRequireChannelAccess(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
userID int64
|
|
rolePerms int64
|
|
roleID int64
|
|
channelType string
|
|
channelID int64
|
|
perm int64
|
|
dmOK bool
|
|
dmErr error
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "DM channel - participant allowed",
|
|
userID: 1,
|
|
channelType: "dm",
|
|
channelID: 100,
|
|
dmOK: true,
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "DM channel - non-participant denied",
|
|
userID: 1,
|
|
channelType: "dm",
|
|
channelID: 100,
|
|
dmOK: false,
|
|
wantErr: ErrNotDMParticipant,
|
|
},
|
|
{
|
|
name: "DM channel - db error",
|
|
userID: 1,
|
|
channelType: "dm",
|
|
channelID: 100,
|
|
dmErr: errors.New("connection lost"),
|
|
},
|
|
{
|
|
name: "regular channel - has perm",
|
|
userID: 1,
|
|
rolePerms: ReadMessages | SendMessages,
|
|
roleID: 4,
|
|
channelType: "text",
|
|
channelID: 10,
|
|
perm: SendMessages,
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "regular channel - lacks perm",
|
|
userID: 1,
|
|
rolePerms: ReadMessages,
|
|
roleID: 4,
|
|
channelType: "text",
|
|
channelID: 10,
|
|
perm: SendMessages,
|
|
wantErr: ErrPermissionDenied,
|
|
},
|
|
{
|
|
name: "DM checks participant not role",
|
|
userID: 1,
|
|
rolePerms: 0, // no permissions at all
|
|
roleID: 0, // no role
|
|
channelType: "dm",
|
|
channelID: 100,
|
|
dmOK: true,
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "admin bypasses regular channel check",
|
|
userID: 1,
|
|
rolePerms: Administrator,
|
|
roleID: 1,
|
|
channelType: "text",
|
|
channelID: 10,
|
|
perm: ManageChannels | ManageRoles, // multi-bit
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "admin does NOT bypass DM participant check",
|
|
userID: 1,
|
|
rolePerms: Administrator,
|
|
roleID: 1,
|
|
channelType: "dm",
|
|
channelID: 100,
|
|
dmOK: false,
|
|
wantErr: ErrNotDMParticipant,
|
|
},
|
|
{
|
|
name: "voice channel uses role perms",
|
|
userID: 1,
|
|
rolePerms: ReadMessages | ConnectVoice | SpeakVoice,
|
|
roleID: 4,
|
|
channelType: "voice",
|
|
channelID: 20,
|
|
perm: ConnectVoice,
|
|
wantErr: nil,
|
|
},
|
|
{
|
|
name: "voice channel denied without perm",
|
|
userID: 1,
|
|
rolePerms: ReadMessages | SendMessages,
|
|
roleID: 4,
|
|
channelType: "voice",
|
|
channelID: 20,
|
|
perm: ConnectVoice,
|
|
wantErr: ErrPermissionDenied,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
db := newMockDB()
|
|
db.dmErr = tt.dmErr
|
|
if tt.dmOK {
|
|
db.dmParticipants[dmKey{tt.userID, tt.channelID}] = true
|
|
}
|
|
ck := NewChecker(db)
|
|
|
|
err := ck.RequireChannelAccess(context.Background(), tt.userID, tt.rolePerms, tt.roleID, tt.channelType, tt.channelID, tt.perm)
|
|
|
|
if tt.dmErr != nil {
|
|
// Expect wrapped error.
|
|
if err == nil {
|
|
t.Fatal("RequireChannelAccess() = nil, want error")
|
|
}
|
|
if !errors.Is(err, tt.dmErr) {
|
|
t.Errorf("RequireChannelAccess() error does not wrap dmErr: got %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if tt.wantErr == nil {
|
|
if err != nil {
|
|
t.Errorf("RequireChannelAccess() unexpected error: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil {
|
|
t.Errorf("RequireChannelAccess() = nil, want %v", tt.wantErr)
|
|
return
|
|
}
|
|
if !errors.Is(err, tt.wantErr) {
|
|
t.Errorf("RequireChannelAccess() error = %v, want %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|