Phase 2: the event core
CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s

Implements PLAN.md Phase 2: the library every later phase is built on,
as pure code under server/internal/matrix with no HTTP and no storage.

canonicaljson: the canonical JSON encoding, hand-written because
encoding/json escapes <, > and & and accepts floats. Integers only, in
[-(2^53)+1, 2^53-1]; lone surrogate escapes rejected rather than turned
into U+FFFD.

signing: the Signing JSON algorithm alongside the existing key handling,
and one lenient base64 decoder shared by the packages above it.

id: identifier grammars as they appear inside events, deliberately laxer
than userapi's account rules because rooms carry historical user IDs.

roomversion: the registry. Versions 3 to 12 are supported, each written
out in full as a Rules value with the flags the other packages read.
Versions 1 and 2 are known but refused: their event format and state
resolution v1 wait for federation. createRoom will default to 11, as the
spec asks for a while after 1.16, with 12 available.

event: PDU parsing and validation, content and reference hashes, event
IDs, the redaction algorithm for every supported version, signing and
verification, and a Builder.

auth: the authorisation rules in spec order, power levels with their
defaults, v12 creators with infinite power, the auth events selection
algorithm, rule 3's auth_events checks, and the separate test for
whether a redaction applies. Comments use the room version 12 rule
numbering, with the version 11 numbers given where they differ.

stateres: state resolution v2, and version 12's amendment (an empty
starting state and the conflicted state subgraph).

Tested against the spec's own vectors byte for byte: the canonical JSON
examples, both JSON signing vectors and both event signing vectors.
State resolution ports scenarios from Synapse's suite. canonicaljson
and event.Parse have fuzz targets.

The capabilities endpoint now advertises the registry instead of a
hard-coded "11". docs/event-core.md describes the packages and the
known gaps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
tajniak81andClaude Opus 5.5 committed 2026-10-15 22:23:35 +02:00
1 parent 6004bb01de
commit 2fbd5f53ce
30 files changed
+8928 -9

No files matched your search

+559
View File
@@ -0,0 +1,559 @@
// Package auth implements the Matrix authorisation rules: given an event
// and the room state it refers to, may that event be in the room?
//
// This is the part of a homeserver that must not be improvised. Every
// server in a room runs these rules over the same events and has to reach
// the same verdict, or the room's history forks. The rules are therefore
// written out here in the order and the shape the spec gives them, with
// the room version differences driven by the roomversion registry rather
// than by guesswork, and the comments say which rule is which so that the
// code can be read against the spec side by side.
//
// # Rule numbers
//
// "Rule N" in these comments means the numbering on the room version 12
// page, the most recent one. Version 12 inserted a new rule 2 (the room ID
// is the create event's ID), so on the pages for versions 6 to 11 each rule
// after the first is one lower: rule 5 here is rule 4 there, rule 10 here
// is rule 9 there. Versions 1 to 5 happen to line up with version 12 again
// from rule 5 on, because their rule 4 was the m.room.aliases rule that
// version 6 removed. Where a sub-rule exists in only some versions, or is
// numbered differently beyond that shift, the comment says so.
//
// Nothing here touches storage or the network. The one exception is the
// restricted join rule, which needs somebody else's signature verified;
// that comes in through the Verifier interface.
//
// What this package does *not* decide:
//
// - Whether a redaction should be applied. The rules allow nearly every
// m.room.redaction event; whether the redaction takes effect is a
// separate test (ShouldApplyRedaction) the roomserver runs once it has
// both events.
// - Soft failure. An event can be allowed by the rules against its own
// auth events and still be refused a place in the room's forward
// extremities because it fails against the *current* state. That is
// the roomserver's business (PLAN.md phase 3).
package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"slices"
"github.com/dariuszjaniak/warp/server/internal/matrix/event"
"github.com/dariuszjaniak/warp/server/internal/matrix/id"
"github.com/dariuszjaniak/warp/server/internal/matrix/roomversion"
"github.com/dariuszjaniak/warp/server/internal/matrix/signing"
)
// The join rules a room may have.
const (
JoinRulePublic = "public"
JoinRuleInvite = "invite"
JoinRuleKnock = "knock"
JoinRuleRestricted = "restricted"
JoinRuleKnockRestricted = "knock_restricted"
JoinRulePrivate = "private"
)
// ErrRejected means the authorisation rules refused the event. Every
// rejection from Check wraps it, and the message says which rule said no.
var ErrRejected = errors.New("rejected by the authorisation rules")
// ErrNoVerifier means a decision needed somebody else's signature checked
// and no Verifier was supplied. It is kept separate from ErrRejected
// because the event is not known to be bad — we are simply not equipped to
// judge it, and retrying with a verifier may well allow it.
var ErrNoVerifier = errors.New("cannot verify the signature this rule needs")
// Verifier checks that an event carries a valid signature from a server.
//
// Only one rule needs this: a join under the restricted join rule is
// authorised by a third party, named in
// content.join_authorised_via_users_server, and the event must be signed
// by *that user's* server rather than only by the joining user's. Verifying
// it may mean fetching another server's keys, which is why it is an
// interface here instead of a function.
type Verifier interface {
VerifyEventSignature(ctx context.Context, serverName string, e *event.Event) error
}
// reject builds a rejection error.
func reject(format string, args ...any) error {
return fmt.Errorf("%w: %s", ErrRejected, fmt.Sprintf(format, args...))
}
// Check applies the authorisation rules to an event.
//
// st is the room state the event is judged against: for an event arriving
// from a client or over federation that is the state its auth_events name,
// and during state resolution it is the state resolved so far. A nil error
// means the event is allowed.
//
// verifier may be nil. It is needed only for a join authorised by another
// user under the restricted join rule; without one, such a join fails with
// ErrNoVerifier rather than being waved through.
func Check(ctx context.Context, e *event.Event, st State, verifier Verifier) error {
rules := e.Rules()
// Rule 1: the create event answers to nothing but itself.
if e.Type() == event.TypeCreate {
return checkCreate(e)
}
create := st.Create()
if create == nil {
// Not one of the numbered rules but a precondition for all of them:
// without the create event we do not know the room's creators, and
// from room version 12 we do not even know its ID. (The numbered
// rule about a missing create event, 2.4 on the version 11 page,
// judges the declared auth_events and lives in CheckAuthEvents.)
return reject("the room state has no m.room.create event")
}
// Rule 2 (room version 12+): the room ID is the create event's ID, so
// an event claiming a different room is not in this room.
if rules.RoomIDIsCreateEventID {
roomID, err := event.RoomIDOfCreateEvent(create)
if err != nil {
return err
}
if e.RoomID() != roomID {
return reject("room_id %q is not the ID of this room's m.room.create event (%q)", e.RoomID(), roomID)
}
}
// Rule 4: a room created with m.federate false never leaves the
// creator's server.
if err := checkFederate(e, create); err != nil {
return err
}
pl, err := ResolvePowerLevels(st.PowerLevels(), create, rules)
if err != nil {
return err
}
// The m.room.aliases rule, rule 4 on the pages for room versions 1 to
// 5 and absent from version 6 on: every server managed its own alias
// list, so an aliases event needed only to match its sender's domain.
// It was dropped because it let any server in the room edit state.
if rules.AliasesEventAuth && e.Type() == event.TypeAliases {
return checkAliases(e)
}
// Rule 5: membership changes have their own rules, and are the only
// thing a user who is not in the room may send.
if e.Type() == event.TypeMember {
return checkMember(ctx, e, st, create, pl, verifier)
}
// Rule 6: everything else requires the sender to be in the room.
if m := st.Membership(e.Sender()); m != event.MembershipJoin {
return reject("the sender is not joined to the room (membership is %q)", m)
}
senderLevel := pl.UserLevel(e.Sender())
// Rule 7: an invite to a third party needs the power to invite.
if e.Type() == event.TypeThirdPartyInvite {
if senderLevel < pl.Invite {
return reject("the sender's power level %s is below the invite level %d",
levelString(senderLevel), pl.Invite)
}
return nil
}
// Rule 8: the sender must be allowed to send this type of event.
if required := pl.SendLevel(e.Type(), e.IsState()); senderLevel < required {
return reject("the sender's power level %s is below the %d needed to send %s",
levelString(senderLevel), required, e.Type())
}
// Rule 9: state keyed by a user ID belongs to that user. It is what
// stops one member rewriting another's per-user state.
if sk, ok := e.StateKey(); ok && len(sk) > 0 && sk[0] == '@' && sk != e.Sender() {
return reject("the state_key %q is a user ID that is not the sender", sk)
}
// Rule 10: a power levels event may not grant or revoke more than the
// sender holds.
if e.Type() == event.TypePowerLevels {
return checkPowerLevels(e, st.PowerLevels(), create, senderLevel)
}
// Rule 11: otherwise, allow.
return nil
}
// checkCreate applies rule 1, the rules for the event that starts a room.
func checkCreate(e *event.Event) error {
rules := e.Rules()
// Rule 1.1: a create event is the root of the DAG.
if len(e.PrevEventIDs()) > 0 {
return reject("an m.room.create event must have no prev_events")
}
// Rule 1.2.
if rules.RoomIDIsCreateEventID {
if e.RoomID() != "" {
return reject("an m.room.create event in a version %s room must not have a room_id", rules.Version)
}
} else {
roomServer, err := id.ServerNameOfRoomID(e.RoomID())
if err != nil {
return reject("room_id: %v", err)
}
senderServer, err := e.SenderServer()
if err != nil {
return reject("sender: %v", err)
}
if roomServer != senderServer {
return reject("the room_id domain %q does not match the sender's domain %q", roomServer, senderServer)
}
}
var content struct {
RoomVersion *string `json:"room_version"`
Creator *string `json:"creator"`
Additional *json.RawMessage `json:"additional_creators"`
}
if err := json.Unmarshal(e.Content(), &content); err != nil {
return reject("m.room.create content: %v", err)
}
// Rule 1.3: a room version we do not implement is one we must not
// pretend to be in.
if content.RoomVersion != nil {
if !roomversion.IsSupported(roomversion.Version(*content.RoomVersion)) {
return reject("room version %q is not one this server recognises", *content.RoomVersion)
}
}
// Rule 1.4 (room version 12+): additional creators must be user IDs.
// Creators cannot be changed later, so a malformed entry here would be
// permanent.
if rules.AdditionalCreators && content.Additional != nil {
var additional []string
if err := json.Unmarshal(*content.Additional, &additional); err != nil {
return reject("m.room.create additional_creators must be an array of strings: %v", err)
}
for _, userID := range additional {
if err := id.ValidateUserID(userID); err != nil {
return reject("m.room.create additional_creators: %v", err)
}
}
}
// Room versions up to 10 required the creator property, which only
// ever repeated the sender.
if rules.CreateEventRequiresCreator && content.Creator == nil {
return reject("an m.room.create event in a version %s room must have a creator", rules.Version)
}
return nil
}
// checkFederate applies rule 4: a room whose create event says
// m.federate false accepts events only from the creator's own server.
func checkFederate(e *event.Event, create *event.Event) error {
var content struct {
Federate *bool `json:"m.federate"`
}
if err := json.Unmarshal(create.Content(), &content); err != nil {
return reject("m.room.create content: %v", err)
}
if content.Federate == nil || *content.Federate {
return nil
}
senderServer, err := e.SenderServer()
if err != nil {
return reject("sender: %v", err)
}
createServer, err := create.SenderServer()
if err != nil {
return reject("the create event's sender: %v", err)
}
if senderServer != createServer {
return reject("the room does not federate and %q is not %q", senderServer, createServer)
}
return nil
}
// checkAliases applies the room version 1 to 5 rule for m.room.aliases.
func checkAliases(e *event.Event) error {
sk, ok := e.StateKey()
if !ok {
return reject("an m.room.aliases event must have a state_key")
}
senderServer, err := e.SenderServer()
if err != nil {
return reject("sender: %v", err)
}
if sk != senderServer {
return reject("the state_key %q is not the sender's server %q", sk, senderServer)
}
return nil
}
// checkPowerLevels applies rule 10: the content is well-formed, and no
// level is moved past the sender's own.
//
// The asymmetry in the rules is deliberate. You may lower a level you
// could have reached yourself, and you may raise one up to your own level,
// but you may not touch anybody whose level equals yours — otherwise two
// administrators could demote each other, and whoever's event won state
// resolution would end up alone in charge.
func checkPowerLevels(e, old, create *event.Event, senderLevel int64) error {
rules := e.Rules()
// Rules 10.1 to 10.3: parsing the content is the check. ResolvePowerLevels
// rejects a level that is not an integer, an events or notifications
// map whose values are not integers, and a users key that is not a
// user ID.
if _, err := ResolvePowerLevels(e, create, rules); err != nil {
return err
}
newContent, err := levelContent(e)
if err != nil {
return err
}
// Rule 10.4 (room version 12+): creators have infinite power, so
// naming one here could only ever be an attempt to take it away.
//
// Being new, this sub-rule shifts the ones after it: on the version 11
// page both halves of their numbers are one lower, so 10.5 here is 9.4
// there and 10.10 here is 9.9.
if rules.CreatorsHaveInfinitePower {
creators, err := Creators(create)
if err != nil {
return err
}
for _, creator := range creators {
if _, ok := newContent.sub("users")[creator]; ok {
return reject("m.room.power_levels must not give a level to the room creator %q", creator)
}
}
}
// Rule 10.5: the first power levels event in a room is always allowed.
// Until it exists the creator is the only person with any power, so
// there is nobody to protect anything from.
if old == nil {
return nil
}
oldContent, err := levelContent(old)
if err != nil {
return err
}
// Rule 10.6: the named levels.
for _, name := range []string{
"users_default", "events_default", "state_default",
"ban", "redact", "kick", "invite",
} {
if err := checkLevelChange(name, "", oldContent.top, newContent.top, senderLevel, false); err != nil {
return err
}
}
// Rules 10.7 and 10.8: the events and notifications maps.
for _, section := range []string{"events", "notifications"} {
oldSection, newSection := oldContent.sub(section), newContent.sub(section)
for _, key := range unionKeys(oldSection, newSection) {
if err := checkLevelChange(key, section, oldSection, newSection, senderLevel, false); err != nil {
return err
}
}
}
// Rules 10.9 and 10.10: the users map, where an entry other than the
// sender's own is protected at the sender's own level rather than
// above it.
oldUsers, newUsers := oldContent.sub("users"), newContent.sub("users")
for _, userID := range unionKeys(oldUsers, newUsers) {
if err := checkLevelChange(userID, "users", oldUsers, newUsers, senderLevel, userID != e.Sender()); err != nil {
return err
}
}
return nil
}
// checkLevelChange compares one level before and after.
//
// A level that is absent on one side is not compared on that side: the
// rules speak of "the current value" and "the new value", and where there
// is no value there is nothing to check.
func checkLevelChange(key, section string, old, new map[string]int64, senderLevel int64, protectPeers bool) error {
oldLevel, hadOld := old[key]
newLevel, hasNew := new[key]
if hadOld && hasNew && oldLevel == newLevel {
return nil
}
where := key
if section != "" {
where = section + "." + key
}
// The peer rule: you may not change or remove the entry of somebody at
// your own level.
if protectPeers && hadOld && oldLevel == senderLevel {
return reject("%s is at the sender's own power level %s and may not be changed by them",
where, levelString(senderLevel))
}
if hadOld && oldLevel > senderLevel {
return reject("%s is %d, above the sender's power level %s", where, oldLevel, levelString(senderLevel))
}
if hasNew && newLevel > senderLevel {
return reject("%s would become %d, above the sender's power level %s", where, newLevel, levelString(senderLevel))
}
return nil
}
// levels holds a power levels event's content as plain integers, keeping
// track of which keys were actually present.
type levels struct {
top map[string]int64
subs map[string]map[string]int64
}
func (l levels) sub(name string) map[string]int64 {
if m, ok := l.subs[name]; ok {
return m
}
return map[string]int64{}
}
// levelContent reads a power levels event's content for comparison.
//
// It parses leniently on purpose: by the time this runs, the new event has
// already been through ResolvePowerLevels, and the old one was accepted
// into the room's state under whatever rules applied then. A value we
// cannot read is left out rather than treated as zero, which would invent
// a level nobody set.
func levelContent(e *event.Event) (levels, error) {
out := levels{top: map[string]int64{}, subs: map[string]map[string]int64{}}
var obj map[string]json.RawMessage
if err := json.Unmarshal(e.Content(), &obj); err != nil {
return out, reject("m.room.power_levels content: %v", err)
}
rules := e.Rules()
for _, name := range []string{
"users_default", "events_default", "state_default",
"ban", "redact", "kick", "invite",
} {
if raw, ok := obj[name]; ok {
if level, err := parseLevel(raw, rules); err == nil {
out.top[name] = level
}
}
}
for _, name := range []string{"events", "notifications", "users"} {
raw, ok := obj[name]
if !ok {
continue
}
if levels, err := parseLevelMap(raw, rules); err == nil {
out.subs[name] = levels
}
}
return out, nil
}
// unionKeys returns every key in either map, in a stable order so that the
// first rejection reported for a given pair of events is always the same
// one — a rule that rejects differently on different servers is as bad as
// one that rejects differently from the spec.
func unionKeys(a, b map[string]int64) []string {
seen := make(map[string]bool, len(a)+len(b))
keys := make([]string, 0, len(a)+len(b))
for _, m := range []map[string]int64{a, b} {
for k := range m {
if !seen[k] {
seen[k] = true
keys = append(keys, k)
}
}
}
slices.Sort(keys)
return keys
}
// levelString renders a power level for an error message, spelling out the
// one value that is not a number anybody wrote down.
func levelString(level int64) string {
if level == InfinitePowerLevel {
return "infinite (room creator)"
}
return fmt.Sprintf("%d", level)
}
// joinRuleOf reads the join rule from an m.room.join_rules event.
func joinRuleOf(e *event.Event) (string, error) {
var content struct {
JoinRule string `json:"join_rule"`
}
if err := json.Unmarshal(e.Content(), &content); err != nil {
return "", reject("m.room.join_rules content: %v", err)
}
return content.JoinRule, nil
}
// ShouldApplyRedaction reports whether a redaction event takes effect on
// the event it targets.
//
// The authorisation rules let almost any redaction event into the room;
// this is the separate test the spec puts under "Handling redactions",
// which a server can only run once it holds both events. Either the sender
// has the redact level, or the redaction comes from the same server as the
// event it redacts — a server may always take back its own users' events.
//
// Note that the power level test passes even when the sender could not have
// sent the event they are redacting. Moderation is not the same permission
// as authorship.
func ShouldApplyRedaction(redaction, target *event.Event, pl *PowerLevels) bool {
if pl.UserLevel(redaction.Sender()) >= pl.Redact {
return true
}
return id.SameServer(redaction.Sender(), target.Sender())
}
// VerifyThirdPartyInviteSignature reports whether any signature on a
// third-party invite's signed block was made by one of the public keys the
// m.room.third_party_invite event published.
//
// The keys travel in the event itself, so unlike the restricted join rule
// this needs nothing from outside: the identity server's key was recorded
// when the invite was issued.
func VerifyThirdPartyInviteSignature(signed json.RawMessage, inviteEvent *event.Event) bool {
keys, err := thirdPartyInviteKeys(inviteEvent)
if err != nil || len(keys) == 0 {
return false
}
sigs, err := signing.ExtractSignatures(signed)
if err != nil {
return false
}
for serverName, byKeyID := range sigs {
for keyID := range byKeyID {
if algorithm, _, ok := signing.SplitKeyID(keyID); !ok || algorithm != signing.KeyIDPrefix {
continue
}
for _, pub := range keys {
if err := signing.VerifyJSON(serverName, keyID, pub, signed); err == nil {
return true
}
}
}
}
return false
}
File diff suppressed because it is too large. Load diff
+201
View File
@@ -0,0 +1,201 @@
package auth
import (
"encoding/json"
"fmt"
"github.com/dariuszjaniak/warp/server/internal/matrix/event"
"github.com/dariuszjaniak/warp/server/internal/matrix/roomversion"
)
// AuthEventKeys returns the state an event must cite in its auth_events,
// per the auth events selection algorithm in the server specification.
//
// The list is what makes an event self-contained: a server receiving it
// years later, with no idea what the room looks like now, can fetch
// exactly these events and re-run the rules. That is also why the set is
// as small as it is — it is the state the rules read, and nothing else.
func AuthEventKeys(e *event.Event) ([]StateKey, error) {
stateKey := (*string)(nil)
if sk, ok := e.StateKey(); ok {
stateKey = &sk
}
return AuthEventKeysFor(e.Rules(), e.Type(), e.Sender(), stateKey, e.Content())
}
// AuthEventKeysFor is AuthEventKeys for an event that does not exist yet,
// so that the roomserver can look up the state it needs before building it.
func AuthEventKeysFor(rules roomversion.Rules, eventType, sender string, stateKey *string, content json.RawMessage) ([]StateKey, error) {
// The create event starts the room; there is nothing for it to cite.
if eventType == event.TypeCreate {
return nil, nil
}
var keys []StateKey
// From room version 12 the create event is left out: the room ID *is*
// its event ID, so every event already refers to it.
if !rules.CreateEventExcludedFromAuthEvents {
keys = append(keys, StateKey{Type: event.TypeCreate})
}
keys = append(keys,
StateKey{Type: event.TypePowerLevels},
StateKey{Type: event.TypeMember, StateKey: sender},
)
if eventType != event.TypeMember {
return keys, nil
}
if stateKey == nil {
return nil, reject("an m.room.member event must have a state_key")
}
keys = append(keys, StateKey{Type: event.TypeMember, StateKey: *stateKey})
var parsed memberContent
if err := json.Unmarshal(content, &parsed); err != nil {
return nil, reject("m.room.member content: %v", err)
}
switch parsed.Membership {
case event.MembershipJoin, event.MembershipInvite, event.MembershipKnock:
keys = append(keys, StateKey{Type: event.TypeJoinRules})
}
if parsed.Membership == event.MembershipInvite && parsed.ThirdPartyInvite != nil {
var signed struct {
Token string `json:"token"`
}
if err := json.Unmarshal(parsed.ThirdPartyInvite.Signed, &signed); err == nil && signed.Token != "" {
keys = append(keys, StateKey{Type: event.TypeThirdPartyInvite, StateKey: signed.Token})
}
}
if parsed.Membership == event.MembershipJoin &&
parsed.JoinAuthorisedViaUsersServer != "" &&
rules.RestrictedJoinRule {
keys = append(keys, StateKey{Type: event.TypeMember, StateKey: parsed.JoinAuthorisedViaUsersServer})
}
return dedupe(keys), nil
}
// CheckAuthEvents applies rule 3 (rule 2 on the version 11 page): the
// auth_events an event declares are well-formed and are the ones the
// selection algorithm asks for.
//
// authEvents are the events the IDs in e.AuthEventIDs() resolve to, with
// any that this server rejected already left out — rule 3.3 asks whether
// an auth event was itself rejected, which only storage can answer, so
// leaving it out of the slice is how the caller reports it. A missing
// entry is then caught here as a citation that does not resolve.
func CheckAuthEvents(e *event.Event, authEvents []*event.Event) error {
if e.Type() == event.TypeCreate {
if len(e.AuthEventIDs()) > 0 {
return reject("an m.room.create event must have no auth_events")
}
return nil
}
allowed, err := AuthEventKeys(e)
if err != nil {
return err
}
allowedSet := make(map[StateKey]bool, len(allowed))
for _, k := range allowed {
allowedSet[k] = true
}
seen := make(map[StateKey]bool, len(authEvents))
hasCreate := false
resolved := 0
for _, auth := range authEvents {
if auth == nil {
// A caller builds this slice from storage lookups, so a nil is
// a citation that resolved to nothing. Counting it as missing
// is both the right answer and cheaper than trusting every
// caller to filter their own misses.
continue
}
resolved++
sk, ok := auth.StateKey()
if !ok {
return reject("auth_events contains %s, which is not a state event", auth.EventID())
}
key := StateKey{Type: auth.Type(), StateKey: sk}
// Rule 3.1: two events for the same piece of state leave it
// ambiguous which one authorised this event.
if seen[key] {
return reject("auth_events has two entries for (%s, %q)", key.Type, key.StateKey)
}
seen[key] = true
// Rule 3.2.
if !allowedSet[key] {
return reject("auth_events contains (%s, %q), which the auth events selection algorithm does not ask for",
key.Type, key.StateKey)
}
// Rule 3.4 (2.5 on the version 11 page): an auth event from
// another room proves nothing about this one.
if auth.RoomID() != e.RoomID() {
return reject("the auth event %s belongs to room %q, not %q", auth.EventID(), auth.RoomID(), e.RoomID())
}
if key.Type == event.TypeCreate {
hasCreate = true
}
}
// Rule 2.4 on the version 11 page; version 12 has no such sub-rule,
// because the create event is not cited at all and its new rule 2 —
// the room ID is the create event's ID — carries the same guarantee.
if !e.Rules().CreateEventExcludedFromAuthEvents && !hasCreate {
return reject("auth_events has no m.room.create event")
}
// Rule 3.3 is the caller's to answer, and this is where their answer
// lands: an ID that resolved to nothing was either never seen or was
// itself rejected, and either way it authorises nothing. The check is
// last so that a specific fault above is reported as itself.
if resolved < len(e.AuthEventIDs()) {
return reject("%d of the %d auth_events are missing or were themselves rejected",
len(e.AuthEventIDs())-resolved, len(e.AuthEventIDs()))
}
return nil
}
// SelectAuthEvents picks the events from a room's state that belong in an
// event's auth_events, ready to be turned into a list of event IDs.
func SelectAuthEvents(rules roomversion.Rules, eventType, sender string, stateKey *string, content json.RawMessage, st State) ([]*event.Event, error) {
keys, err := AuthEventKeysFor(rules, eventType, sender, stateKey, content)
if err != nil {
return nil, err
}
out := make([]*event.Event, 0, len(keys))
for _, k := range keys {
if e := st[k]; e != nil {
out = append(out, e)
}
}
if len(out) > event.MaxAuthEvents {
// The selection algorithm cannot produce this many, so reaching it
// means the algorithm and the limit have drifted apart.
return nil, fmt.Errorf("auth events selection produced %d events, more than the %d an event may carry",
len(out), event.MaxAuthEvents)
}
return out, nil
}
// dedupe removes repeated keys, which happens when a user invites or
// authorises themselves: the sender's member event and the target's are
// then the same piece of state.
func dedupe(keys []StateKey) []StateKey {
seen := make(map[StateKey]bool, len(keys))
out := make([]StateKey, 0, len(keys))
for _, k := range keys {
if seen[k] {
continue
}
seen[k] = true
out = append(out, k)
}
return out
}
+358
View File
@@ -0,0 +1,358 @@
package auth
import (
"context"
"crypto/ed25519"
"encoding/json"
"fmt"
"github.com/dariuszjaniak/warp/server/internal/matrix/event"
"github.com/dariuszjaniak/warp/server/internal/matrix/id"
"github.com/dariuszjaniak/warp/server/internal/matrix/signing"
)
// memberContent is the part of an m.room.member event's content the rules
// read. The pointer and json.RawMessage fields are there so that "absent"
// can be told from "empty", which several of the rules turn on.
type memberContent struct {
Membership string `json:"membership"`
JoinAuthorisedViaUsersServer string `json:"join_authorised_via_users_server"`
ThirdPartyInvite *thirdPartyInvite `json:"third_party_invite"`
}
type thirdPartyInvite struct {
Signed json.RawMessage `json:"signed"`
}
// checkMember applies rule 5, the membership rules.
//
// These are the only rules a user who is not in the room may satisfy, so
// they carry the whole of who can get in: invitations, public rooms,
// knocking, restricted rooms, and who may throw whom out.
func checkMember(ctx context.Context, e *event.Event, st State, create *event.Event, pl *PowerLevels, verifier Verifier) error {
rules := e.Rules()
// Rule 5.1.
target, ok := e.StateKey()
if !ok {
return reject("an m.room.member event must have a state_key")
}
var content memberContent
if err := json.Unmarshal(e.Content(), &content); err != nil {
return reject("m.room.member content: %v", err)
}
if content.Membership == "" {
return reject("an m.room.member event must have a membership")
}
// Rule 5.2: a join authorised by another user must be signed by that
// user's server, not merely by the joining user's. Without it, anybody
// could claim to have been let into a restricted room.
if rules.RestrictedJoinRule && content.JoinAuthorisedViaUsersServer != "" {
if err := checkJoinAuthorisation(ctx, e, content.JoinAuthorisedViaUsersServer, verifier); err != nil {
return err
}
}
senderMembership := st.Membership(e.Sender())
targetMembership := st.Membership(target)
switch content.Membership {
case event.MembershipJoin:
return checkJoin(e, st, create, pl, target, targetMembership, content)
case event.MembershipInvite:
return checkInvite(e, st, pl, target, senderMembership, targetMembership, content)
case event.MembershipLeave:
return checkLeave(e, pl, target, senderMembership, targetMembership)
case event.MembershipBan:
return checkBan(e, pl, target, senderMembership)
case event.MembershipKnock:
if !rules.KnockJoinRule {
return reject("room version %s does not allow knocking", rules.Version)
}
return checkKnock(e, st, target, senderMembership)
default:
// Rule 5.8.
return reject("unknown membership %q", content.Membership)
}
}
// checkJoin applies rule 5.3.
func checkJoin(e *event.Event, st State, create *event.Event, pl *PowerLevels, target, targetMembership string, content memberContent) error {
rules := e.Rules()
// Rule 5.3.1: the creator's own join, the second event in the room.
// There is no state yet to authorise it against, so it is recognised
// by its shape: it follows the create event and nothing else.
prev := e.PrevEventIDs()
if len(prev) == 1 && prev[0] == create.EventID() && target == create.Sender() {
return nil
}
// Rule 5.3.2: you join yourself. Being dragged into a room is an
// invite, which the invited user then accepts with a join of their own.
if e.Sender() != target {
return reject("the sender may not join %q on their behalf", target)
}
// Rule 5.3.3.
if targetMembership == event.MembershipBan {
return reject("the user is banned from the room")
}
joinRule := st.JoinRule()
switch joinRule {
case JoinRuleInvite, JoinRuleKnock:
// Rule 5.3.4. A knock does not admit anybody by itself: it asks,
// and somebody with the power to invite still has to answer.
if targetMembership == event.MembershipInvite || targetMembership == event.MembershipJoin {
return nil
}
return reject("the room's join rule is %q and the user has not been invited (membership is %q)",
joinRule, targetMembership)
case JoinRuleRestricted, JoinRuleKnockRestricted:
if joinRule == JoinRuleRestricted && !rules.RestrictedJoinRule {
return reject("room version %s does not have the restricted join rule", rules.Version)
}
if joinRule == JoinRuleKnockRestricted && !rules.KnockRestrictedJoinRule {
return reject("room version %s does not have the knock_restricted join rule", rules.Version)
}
// Rule 5.3.5.1: an existing invite or join still works, so a
// restricted room is never *harder* to enter than an invite room.
if targetMembership == event.MembershipInvite || targetMembership == event.MembershipJoin {
return nil
}
// Rule 5.3.5.2. The authorising user has to be in the room and
// able to invite, because what they are really doing is vouching
// that the joining user is a member of one of the allowed rooms —
// something this server may be unable to see for itself.
authoriser := content.JoinAuthorisedViaUsersServer
if authoriser == "" {
return reject("the room's join rule is %q and the join is not authorised by any user", joinRule)
}
if m := st.Membership(authoriser); m != event.MembershipJoin {
return reject("the authorising user %q is not joined to the room (membership is %q)", authoriser, m)
}
if level := pl.UserLevel(authoriser); level < pl.Invite {
return reject("the authorising user %q has power level %s, below the invite level %d",
authoriser, levelString(level), pl.Invite)
}
// Rule 5.3.5.3.
return nil
case JoinRulePublic:
// Rule 5.3.6.
return nil
default:
// Rule 5.3.7. "private" lands here, and so does any join rule
// from a future room version: a rule we cannot evaluate must keep
// people out rather than let them in.
return reject("the room's join rule %q does not allow joining", joinRule)
}
}
// checkInvite applies rule 5.4.
func checkInvite(e *event.Event, st State, pl *PowerLevels, target, senderMembership, targetMembership string, content memberContent) error {
// Rule 5.4.1: a third-party invite is an invitation issued to an email
// address or phone number before the server knew which user it was.
// The identity server's signature stands in for the inviter's power.
if content.ThirdPartyInvite != nil {
return checkThirdPartyInvite(e, st, target, targetMembership, content)
}
// Rule 5.4.2.
if senderMembership != event.MembershipJoin {
return reject("the sender is not joined to the room (membership is %q)", senderMembership)
}
// Rule 5.4.3.
if targetMembership == event.MembershipJoin || targetMembership == event.MembershipBan {
return reject("the user is already %q in the room", targetMembership)
}
// Rule 5.4.4.
if level := pl.UserLevel(e.Sender()); level < pl.Invite {
return reject("the sender's power level %s is below the invite level %d", levelString(level), pl.Invite)
}
// Rule 5.4.5 is the negation of the above.
return nil
}
// checkThirdPartyInvite applies rule 5.4.1.
func checkThirdPartyInvite(e *event.Event, st State, target, targetMembership string, content memberContent) error {
// Rule 5.4.1.1.
if targetMembership == event.MembershipBan {
return reject("the user is banned from the room")
}
// Rule 5.4.1.2.
if len(content.ThirdPartyInvite.Signed) == 0 {
return reject("the third_party_invite has no signed property")
}
var signed struct {
MXID string `json:"mxid"`
Token string `json:"token"`
}
if err := json.Unmarshal(content.ThirdPartyInvite.Signed, &signed); err != nil {
return reject("the third_party_invite signed property: %v", err)
}
// Rule 5.4.1.3.
if signed.MXID == "" || signed.Token == "" {
return reject("the third_party_invite signed property needs both mxid and token")
}
// Rule 5.4.1.4.
if signed.MXID != target {
return reject("the third_party_invite names %q but the event invites %q", signed.MXID, target)
}
// Rule 5.4.1.5.
inviteEvent := st.ThirdPartyInvite(signed.Token)
if inviteEvent == nil {
return reject("there is no m.room.third_party_invite in the room state for token %q", signed.Token)
}
// Rule 5.4.1.6: the same user who issued the invitation must be the
// one redeeming it.
if inviteEvent.Sender() != e.Sender() {
return reject("the third-party invite was issued by %q, not by %q", inviteEvent.Sender(), e.Sender())
}
// Rules 5.4.1.7 and 5.4.1.8.
if !VerifyThirdPartyInviteSignature(content.ThirdPartyInvite.Signed, inviteEvent) {
return reject("no signature on the third-party invite matches a public key in %s", inviteEvent.EventID())
}
return nil
}
// checkLeave applies rule 5.5, which covers both leaving and being kicked.
func checkLeave(e *event.Event, pl *PowerLevels, target, senderMembership, targetMembership string) error {
rules := e.Rules()
// Rule 5.5.1: leaving on your own account, which also covers turning
// down an invitation and withdrawing a knock.
if e.Sender() == target {
switch targetMembership {
case event.MembershipInvite, event.MembershipJoin:
return nil
case event.MembershipKnock:
if rules.KnockJoinRule {
return nil
}
}
return reject("the user cannot leave from membership %q", targetMembership)
}
// Rule 5.5.2.
if senderMembership != event.MembershipJoin {
return reject("the sender is not joined to the room (membership is %q)", senderMembership)
}
senderLevel := pl.UserLevel(e.Sender())
// Rule 5.5.3: unbanning is a leave over a ban, and it takes the power
// to have banned in the first place.
if targetMembership == event.MembershipBan && senderLevel < pl.Ban {
return reject("the sender's power level %s is below the ban level %d needed to unban",
levelString(senderLevel), pl.Ban)
}
// Rule 5.5.4.
if senderLevel < pl.Kick {
return reject("the sender's power level %s is below the kick level %d", levelString(senderLevel), pl.Kick)
}
if targetLevel := pl.UserLevel(target); targetLevel >= senderLevel {
return reject("the sender's power level %s does not exceed %q's power level %s",
levelString(senderLevel), target, levelString(targetLevel))
}
// Rule 5.5.5 is the negation of the above.
return nil
}
// checkBan applies rule 5.6.
func checkBan(e *event.Event, pl *PowerLevels, target, senderMembership string) error {
// Rule 5.6.1.
if senderMembership != event.MembershipJoin {
return reject("the sender is not joined to the room (membership is %q)", senderMembership)
}
// Rule 5.6.2.
senderLevel := pl.UserLevel(e.Sender())
if senderLevel < pl.Ban {
return reject("the sender's power level %s is below the ban level %d", levelString(senderLevel), pl.Ban)
}
if targetLevel := pl.UserLevel(target); targetLevel >= senderLevel {
return reject("the sender's power level %s does not exceed %q's power level %s",
levelString(senderLevel), target, levelString(targetLevel))
}
// Rule 5.6.3 is the negation of the above.
return nil
}
// checkKnock applies rule 5.7.
func checkKnock(e *event.Event, st State, target, senderMembership string) error {
// Rule 5.7.1.
joinRule := st.JoinRule()
if joinRule != JoinRuleKnock && joinRule != JoinRuleKnockRestricted {
return reject("the room's join rule is %q, so it cannot be knocked on", joinRule)
}
// Rule 5.7.2.
if e.Sender() != target {
return reject("the sender may not knock on behalf of %q", target)
}
// Rule 5.7.3: knocking is allowed from anything but banned, invited or
// joined — there is no point knocking on a door that is already open.
switch senderMembership {
case event.MembershipBan, event.MembershipInvite, event.MembershipJoin:
// Rule 5.7.4.
return reject("the user cannot knock while their membership is %q", senderMembership)
default:
return nil
}
}
// checkJoinAuthorisation applies rule 5.2.
func checkJoinAuthorisation(ctx context.Context, e *event.Event, authoriser string, verifier Verifier) error {
serverName, err := id.ServerNameOf(authoriser)
if err != nil {
return reject("join_authorised_via_users_server: %v", err)
}
if verifier == nil {
return fmt.Errorf("%w: %s is authorised by %q, whose server's signature cannot be checked",
ErrNoVerifier, e.EventID(), authoriser)
}
if err := verifier.VerifyEventSignature(ctx, serverName, e); err != nil {
return reject("the join is authorised by %q but carries no valid signature from %q: %v",
authoriser, serverName, err)
}
return nil
}
// thirdPartyInviteKeys returns the public keys an m.room.third_party_invite
// event published, from either of the two places the spec allows.
func thirdPartyInviteKeys(e *event.Event) ([]ed25519.PublicKey, error) {
var content struct {
PublicKey string `json:"public_key"`
PublicKeys []struct {
PublicKey string `json:"public_key"`
} `json:"public_keys"`
}
if err := json.Unmarshal(e.Content(), &content); err != nil {
return nil, reject("m.room.third_party_invite content: %v", err)
}
encoded := make([]string, 0, len(content.PublicKeys)+1)
if content.PublicKey != "" {
encoded = append(encoded, content.PublicKey)
}
for _, k := range content.PublicKeys {
if k.PublicKey != "" {
encoded = append(encoded, k.PublicKey)
}
}
keys := make([]ed25519.PublicKey, 0, len(encoded))
for _, s := range encoded {
raw, err := signing.DecodeBase64(s)
if err != nil || len(raw) != ed25519.PublicKeySize {
// A key we cannot read cannot match a signature, and the rule
// asks whether *any* key matches — so a bad one is skipped
// rather than fatal.
continue
}
keys = append(keys, ed25519.PublicKey(raw))
}
return keys, nil
}
+270
View File
@@ -0,0 +1,270 @@
package auth
import (
"encoding/json"
"fmt"
"math"
"strconv"
"github.com/dariuszjaniak/warp/server/internal/matrix/event"
"github.com/dariuszjaniak/warp/server/internal/matrix/id"
"github.com/dariuszjaniak/warp/server/internal/matrix/roomversion"
)
// InfinitePowerLevel is the level a room creator has from room version 12.
//
// The spec calls it "infinitely high". Since every level that can be
// written down is capped at 2^53-1 by canonical JSON, the largest int64
// stands in for infinity without any comparison going wrong — and nothing
// here does arithmetic on a level, only compares them.
const InfinitePowerLevel int64 = math.MaxInt64
// The default levels, which apply key by key: a power levels event that
// sets only "users" leaves every other level at the value below.
//
// state_default is 50 even in a room with no m.room.power_levels event at
// all, which looks odd until you notice that the creator has 100 there, so
// the only person who can send state is the one person who should be able
// to.
const (
DefaultBan int64 = 50
DefaultKick int64 = 50
DefaultRedact int64 = 50
DefaultInvite int64 = 0
DefaultEventsDefault int64 = 0
DefaultStateDefault int64 = 50
DefaultUsersDefault int64 = 0
DefaultNotification int64 = 50
// CreatorPowerLevel is what the room's creator has while the room has
// no m.room.power_levels event, in room versions up to 11.
CreatorPowerLevel int64 = 100
)
// PowerLevels is a room's resolved power level configuration: the event's
// content with every default filled in, and the room's creators noted so
// that their level can be answered.
type PowerLevels struct {
Ban int64
Kick int64
Redact int64
Invite int64
EventsDefault int64
StateDefault int64
UsersDefault int64
Events map[string]int64
Notifications map[string]int64
Users map[string]int64
// creators have InfinitePowerLevel from room version 12. Before that
// the map is empty and the creator's 100 is written into Users
// instead, because it is a real level there that a later power levels
// event can change.
creators map[string]bool
}
// ResolvePowerLevels works out the levels in force, given the room's
// m.room.power_levels event (which may be nil) and its create event.
//
// The create event is needed even when there is a power levels event: from
// room version 12 it is what says who the creators are, and their level
// cannot be written down.
func ResolvePowerLevels(plEvent, createEvent *event.Event, rules roomversion.Rules) (*PowerLevels, error) {
pl := &PowerLevels{
Ban: DefaultBan,
Kick: DefaultKick,
Redact: DefaultRedact,
Invite: DefaultInvite,
EventsDefault: DefaultEventsDefault,
StateDefault: DefaultStateDefault,
UsersDefault: DefaultUsersDefault,
Events: map[string]int64{},
Notifications: map[string]int64{},
Users: map[string]int64{},
creators: map[string]bool{},
}
if createEvent != nil {
creators, err := Creators(createEvent)
if err != nil {
return nil, err
}
if rules.CreatorsHaveInfinitePower {
for _, c := range creators {
pl.creators[c] = true
}
} else if plEvent == nil && len(creators) > 0 {
// With no power levels event, the creator has 100 and everyone
// else has nothing.
pl.Users[creators[0]] = CreatorPowerLevel
}
}
if plEvent == nil {
return pl, nil
}
if err := pl.apply(plEvent.Content(), rules); err != nil {
return nil, err
}
return pl, nil
}
// apply overlays the content of an m.room.power_levels event.
func (p *PowerLevels) apply(content json.RawMessage, rules roomversion.Rules) error {
var obj map[string]json.RawMessage
if err := json.Unmarshal(content, &obj); err != nil {
return reject("m.room.power_levels content is not an object: %v", err)
}
scalars := map[string]*int64{
"ban": &p.Ban,
"kick": &p.Kick,
"redact": &p.Redact,
"invite": &p.Invite,
"events_default": &p.EventsDefault,
"state_default": &p.StateDefault,
"users_default": &p.UsersDefault,
}
for name, target := range scalars {
raw, ok := obj[name]
if !ok {
continue
}
level, err := parseLevel(raw, rules)
if err != nil {
return reject("m.room.power_levels %s: %v", name, err)
}
*target = level
}
for name, target := range map[string]*map[string]int64{
"events": &p.Events,
"notifications": &p.Notifications,
"users": &p.Users,
} {
raw, ok := obj[name]
if !ok {
continue
}
levels, err := parseLevelMap(raw, rules)
if err != nil {
return reject("m.room.power_levels %s: %v", name, err)
}
if name == "users" {
for userID := range levels {
if err := id.ValidateUserID(userID); err != nil {
return reject("m.room.power_levels users: %v", err)
}
}
}
*target = levels
}
return nil
}
// UserLevel returns a user's power level.
func (p *PowerLevels) UserLevel(userID string) int64 {
if p.creators[userID] {
return InfinitePowerLevel
}
if level, ok := p.Users[userID]; ok {
return level
}
return p.UsersDefault
}
// IsCreator reports whether a user is one of the room's creators, in a room
// version where that gives them infinite power.
func (p *PowerLevels) IsCreator(userID string) bool { return p.creators[userID] }
// SendLevel returns the level needed to send an event of a type. The events
// map overrides everything; otherwise a state event needs state_default and
// a message event events_default.
func (p *PowerLevels) SendLevel(eventType string, isState bool) int64 {
if level, ok := p.Events[eventType]; ok {
return level
}
if isState {
return p.StateDefault
}
return p.EventsDefault
}
// NotificationLevel returns the level needed to trigger a notification,
// such as "room" for an @room mention.
func (p *PowerLevels) NotificationLevel(key string) int64 {
if level, ok := p.Notifications[key]; ok {
return level
}
return DefaultNotification
}
// Creators returns the user IDs that count as creators of the room: the
// sender of the create event, plus content.additional_creators in room
// versions that allow it.
func Creators(createEvent *event.Event) ([]string, error) {
if createEvent.Type() != event.TypeCreate {
return nil, fmt.Errorf("%s is not an m.room.create event", createEvent.EventID())
}
creators := []string{createEvent.Sender()}
if !createEvent.Rules().AdditionalCreators {
return creators, nil
}
var content struct {
AdditionalCreators []string `json:"additional_creators"`
}
if err := json.Unmarshal(createEvent.Content(), &content); err != nil {
return nil, reject("m.room.create additional_creators is not an array of strings: %v", err)
}
for _, c := range content.AdditionalCreators {
if err := id.ValidateUserID(c); err != nil {
return nil, reject("m.room.create additional_creators: %v", err)
}
creators = append(creators, c)
}
return creators, nil
}
// parseLevel reads a single power level.
//
// Room versions up to 9 accepted a level written as a string, so "50" and
// 50 both meant fifty — a laxness that let two servers that disagreed
// about how to parse a string disagree about who could speak. Version 10
// requires an integer.
func parseLevel(raw json.RawMessage, rules roomversion.Rules) (int64, error) {
var level int64
if err := json.Unmarshal(raw, &level); err == nil {
return level, nil
}
if rules.RequireIntegerPowerLevels {
return 0, fmt.Errorf("%s is not an integer", raw)
}
var s string
if err := json.Unmarshal(raw, &s); err != nil {
return 0, fmt.Errorf("%s is neither an integer nor a string", raw)
}
level, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, fmt.Errorf("%s is a string that is not an integer", raw)
}
return level, nil
}
// parseLevelMap reads the events, notifications or users map.
func parseLevelMap(raw json.RawMessage, rules roomversion.Rules) (map[string]int64, error) {
var obj map[string]json.RawMessage
if err := json.Unmarshal(raw, &obj); err != nil {
return nil, fmt.Errorf("not an object: %v", err)
}
out := make(map[string]int64, len(obj))
for k, v := range obj {
level, err := parseLevel(v, rules)
if err != nil {
return nil, fmt.Errorf("%q: %w", k, err)
}
out[k] = level
}
return out, nil
}
+95
View File
@@ -0,0 +1,95 @@
package auth
import (
"github.com/dariuszjaniak/warp/server/internal/matrix/event"
)
// StateKey identifies one piece of room state: an event type and a state
// key together. Room state is a map from this to an event.
type StateKey struct {
Type string
StateKey string
}
// State is the room state an event is authorised against.
//
// It is only ever the handful of events the rules actually read — the
// create event, the power levels, the join rules, the members involved and
// any third-party invite — not the whole state of the room. Two callers
// build it differently: event intake builds it from the event's own
// auth_events, and state resolution builds it from the state it has
// resolved so far.
type State map[StateKey]*event.Event
// NewState indexes a set of state events.
//
// A non-state event in the input is ignored rather than refused: the usual
// source is an event's auth_events, and a server that cited something odd
// there should fail the auth_events checks, not crash the indexer.
func NewState(events []*event.Event) State {
st := make(State, len(events))
for _, e := range events {
if sk, ok := e.StateKey(); ok {
st[StateKey{Type: e.Type(), StateKey: sk}] = e
}
}
return st
}
// Get returns one state event, or nil.
func (s State) Get(eventType, stateKey string) *event.Event {
return s[StateKey{Type: eventType, StateKey: stateKey}]
}
// Create returns the m.room.create event, or nil if the state does not
// contain one — which for any event but the create event itself means the
// event cannot be authorised at all.
func (s State) Create() *event.Event { return s.Get(event.TypeCreate, "") }
// PowerLevels returns the m.room.power_levels event, or nil. A room
// without one is not broken: until the creator sends one, the defaults
// apply and the creator alone has power.
func (s State) PowerLevels() *event.Event { return s.Get(event.TypePowerLevels, "") }
// JoinRules returns the m.room.join_rules event, or nil.
func (s State) JoinRules() *event.Event { return s.Get(event.TypeJoinRules, "") }
// Member returns a user's m.room.member event, or nil.
func (s State) Member(userID string) *event.Event { return s.Get(event.TypeMember, userID) }
// ThirdPartyInvite returns the m.room.third_party_invite event for a token,
// or nil.
func (s State) ThirdPartyInvite(token string) *event.Event {
return s.Get(event.TypeThirdPartyInvite, token)
}
// Membership returns a user's membership in the room.
//
// A user with no m.room.member event has never been in the room, which the
// rules treat exactly like having left: every rule that cares asks whether
// the membership *is* something, never whether it is absent.
func (s State) Membership(userID string) string {
e := s.Member(userID)
if e == nil {
return event.MembershipLeave
}
membership, err := e.Membership()
if err != nil || membership == "" {
return event.MembershipLeave
}
return membership
}
// JoinRule returns the room's join rule, defaulting to "invite" as the
// spec does for a room with no m.room.join_rules event.
func (s State) JoinRule() string {
e := s.JoinRules()
if e == nil {
return JoinRuleInvite
}
rule, err := joinRuleOf(e)
if err != nil || rule == "" {
return JoinRuleInvite
}
return rule
}