asobi_chat_acl (asobi v0.75.1)
View SourceAuthorisation policy for chat channels.
Channel ID schemes: global:<Name> - game-wide channel; any signed-in player may
join, but only for a `<Name>` declared in the
`chat => #{global => [...]}` of some configured
game mode (see #299)dm:<A>:<B> - A and B are the only allowed readers, and B
(the participant that isn't the caller) must
resolve to a real player id (see #305 below) world:<WorldId> - must currently be joined to the world
zone:<WorldId>:<X>,<Y> - must currently be joined to the world
prox:<WorldId>:<X>,<Y> - must currently be joined to the world
room:<GroupId> - the room: prefix is stripped; GroupId must
be a canonical lowercase-hyphenated uuid (the
shape `asobi_id:generate/0` always emits) and
the caller must be a member of that group<anything else> - treated as a group_id; must be a group member
#299: every other scheme stops at one world or one pair of players, so a
game whose locations are separate worlds had no id its whole player base
could share. global:<Name> is that tier. It is operator-declared rather
than client-minted: the name must appear in a game mode's
chat => #{global => [...]}, so chat.join still cannot spawn unbounded
channel processes, and the per-connection channel cap still applies.
A room: channel is closed by default: a group id that isn't a canonical
uuid is denied outright (rather than falling through to a membership lookup
that merely happens to miss), and a group id that doesn't exist or that the
caller isn't a member of yields false. Requiring the canonical uuid shape
also prevents one group from acquiring unbounded alias channel ids (dashes
stripped, different case, padded/garbage-prefixed hex) that a lossy uuid
decode downstream would otherwise all resolve to the same group - each alias
would otherwise be a distinct, unmoderated channel process.
#306: for a group with open=true, membership (checked here) is easy to
get - anyone can join without an invite - but membership is still what
gates reading. Once joined, a member sees the group's full retained
history, including messages sent before they joined; this is intentional
(see guides/websocket-protocol.md), not a gap to close here.
#305: dm:<A>:<B> had the same unbounded-minting shape as the pre-fix
room: scheme — classify/1 authorised dm:<self>:<anything> for any
value of <anything>, and chat.send/send_message/3 start a channel
process on demand with no prior chat.join, so MAX_JOINED_CHANNELS_PER_CONN
never bounded it. The other participant must now resolve to a real,
currently-known player id (checked against the players table via
asobi_repo:get/2, the same lookup asobi_social_controller already uses
to verify a friend id is real). This bounds minting to O(known players)
instead of unbounded arbitrary strings. Checking "is a real player" rather
than "is currently online" is deliberate: the other participant is very
often offline (that's when you send someone a DM), so an online-only check
would break ordinary DMing, not just close the abuse case.
Shared by asobi_chat_controller (HTTP history) and asobi_ws_handler
(WebSocket chat.join / chat.send). Keeping a single source of truth
prevents the WS path from drifting and silently allowing DM eavesdropping.
Summary
Functions
Validate a channel id shape before it is used at all: bounded length and
one of the known channel-id prefixes. Shared by the WS chat.join/chat.send
path and the HTTP chat-history path so neither can be tricked into treating
an unprefixed bare id (which classify/1's catch-all clause would otherwise
happily route to a group lookup) as a valid channel id.
Functions
Validate a channel id shape before it is used at all: bounded length and
one of the known channel-id prefixes. Shared by the WS chat.join/chat.send
path and the HTTP chat-history path so neither can be tricked into treating
an unprefixed bare id (which classify/1's catch-all clause would otherwise
happily route to a group lookup) as a valid channel id.