Files
OwnCord/docs/plans/channel-visibility-unification.md
T

83 lines
4.3 KiB
Markdown
Raw Normal View History

# Channel-Visibility Unification (backlog item 3) — Design
**Status:** implemented 2026-07-20 (D9) — re-verified 2026-08-04
(`permissions/checker.go:110` `VisibleChannelIDs`; four delegating call sites;
REST/WS agreement test `Server/ws/channel_visibility_agreement_test.go`)
**Decision:** D9 in [audit-2026-07-19-decisions.md](audit-2026-07-19-decisions.md)
**Closes:** audit finding A-2026-07-07 (backlog §6 item 3)
## Problem
Per the audit, the "does this role see this channel" rule is implemented ~4× with
comments telling each copy it "must mirror" the others:
- `service.ChannelService.ListVisibleChannels` (REST `GET /channels`),
- `ws.buildReady` (the ready payload's channel list),
- `ws.computeAllowedChannels` (replay-buffer filtering on reconnect),
- `ws.Hub.RefreshChannelVisibility` (targeted channel_create/delete after an
override change).
All four inline the same predicate — `HasAdmin(perms) || EffectivePerms(perms,
allow, deny)&ReadMessages == ReadMessages` — plus the same "skip `dm` channels,
fail closed on nil role" scaffolding. The recent private-channel fixes show this
churns; a drift between copies is an information-disclosure bug.
## Approach — funnel all four through the checker that already exists
The unified predicate is **already written**: `permissions.Checker.HasChannelPermBatch`
(pre-fetched overrides map) and `.HasChannelPerm` (single-channel). Three of the
four sites bypass it by re-inlining the bit math; `RefreshChannelVisibility` does
its own `GetChannelPermissions` + `EffectivePerms`. The fix is to route every
site through the checker, not to invent a new abstraction:
1. Add one thin helper to `permissions.Checker`:
`VisibleChannelIDs(rolePerms int64, channels []ChannelRef, overrides map[int64]ChannelOverride) map[int64]bool`
— iterates, skips `dm`, and calls the existing `HasChannelPermBatch` per
channel. `ChannelRef{ID int64; Type string}` is declared in `permissions`
(the package deliberately avoids importing `db`, so callers map their
`[]db.Channel` down to `[]ChannelRef`).
2. `ListVisibleChannels`, `buildReady`, and `computeAllowedChannels` call it.
`buildReady` still fetches the overrides map once (it reuses it for `can_send`
and unread), then hands the same map to `VisibleChannelIDs`.
`computeAllowedChannels` unions the result with the user's open DM channels
(unchanged). Nil role → empty set, which the helper yields naturally.
3. `RefreshChannelVisibility`'s per-role check calls the existing
`HasChannelPerm(rolePerms, roleID, ch.ID, ReadMessages)` instead of its inline
copy. Its targeted-send + `visibilityChangeSeq` watermark mechanics are
untouched.
## Files touched
- `Server/permissions/checker.go` — add `ChannelRef` + `VisibleChannelIDs`.
- `Server/permissions/checker_test.go` — cover the helper (admin bypass, deny
override hides, no-override inherits base, dm skipped).
- `Server/service/channel.go``ListVisibleChannels` delegates.
- `Server/ws/serve.go``buildReady`, `computeAllowedChannels` delegate; drop
the "mirrors …" comments.
- `Server/ws/hub.go``RefreshChannelVisibility` uses `HasChannelPerm`.
- Docs: `docs/audit-2026-07-19.md` (A-2026-07-07 → RESOLVED),
`docs/architecture/server.md`/`websocket.md` if a visibility callout names the
duplication.
## Test plan
- Unit tests for `VisibleChannelIDs` in `permissions`.
- **REST/WS agreement test** (the audit's explicit ask): seed a real in-memory
`db` with roles, per-channel overrides, and text/announcement/voice/dm
channels; assert `ListVisibleChannels`, the channel set inside `buildReady`,
and the non-DM subset of `computeAllowedChannels` are byte-for-byte the same
set for the same user across admin / member-with-deny / nil-role cases.
- Existing `serve_test.go`, `hub_test.go`, `channel_handler` tests stay green
unchanged (behavior is identical; only the code path is shared).
## Non-goals
- No change to permission bits, override storage, or the `EffectivePerms`
algebra — only where the existing predicate is called from.
- DM visibility stays membership-based (added on top of the server-channel set,
not routed through the checker).
- `can_send`, unread counts, and voice-state filtering in `buildReady` are
unchanged.
- Not touching `RefreshChannelVisibility`'s targeted-send / watermark design
(backlog 11/12 territory) — only its read predicate.