asobi_chat_acl (asobi v0.75.1)

View Source

Authorisation 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

authorized(ChannelId, PlayerId)

-spec authorized(binary(), binary()) -> boolean().

validate_channel_id(ChannelId)

-spec validate_channel_id(binary()) -> boolean().

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.