mautrix-go/federation/eventauth/eventauth.go
Tulir Asokan 9878c3d675
Some checks are pending
Go / Lint (latest) (push) Waiting to run
Go / Build (old, libolm) (push) Waiting to run
Go / Build (latest, libolm) (push) Waiting to run
Go / Build (old, goolm) (push) Waiting to run
Go / Build (latest, goolm) (push) Waiting to run
federation/eventauth: change error message for users-specific power level check
2025-09-26 23:36:58 +03:00

846 lines
35 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2025 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//go:build goexperiment.jsonv2
package eventauth
import (
"encoding/json"
"encoding/json/jsontext"
"errors"
"fmt"
"slices"
"strconv"
"strings"
"github.com/tidwall/gjson"
"go.mau.fi/util/exgjson"
"go.mau.fi/util/exstrings"
"go.mau.fi/util/ptr"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/federation/pdu"
"maunium.net/go/mautrix/federation/signutil"
"maunium.net/go/mautrix/id"
)
type AuthFailError struct {
Index string
Message string
Wrapped error
}
func (afe AuthFailError) Error() string {
if afe.Message != "" {
return fmt.Sprintf("fail %s: %s", afe.Index, afe.Message)
} else if afe.Wrapped != nil {
return fmt.Sprintf("fail %s: %s", afe.Index, afe.Wrapped.Error())
}
return fmt.Sprintf("fail %s", afe.Index)
}
func (afe AuthFailError) Unwrap() error {
return afe.Wrapped
}
var mFederatePath = exgjson.Path("m.federate")
var (
ErrCreateHasPrevEvents = AuthFailError{Index: "1.1", Message: "m.room.create event has prev_events"}
ErrCreateHasRoomID = AuthFailError{Index: "1.2", Message: "m.room.create event has room_id set"}
ErrRoomIDDoesntMatchSender = AuthFailError{Index: "1.2", Message: "room ID server doesn't match sender server"}
ErrUnknownRoomVersion = AuthFailError{Index: "1.3", Wrapped: id.ErrUnknownRoomVersion}
ErrInvalidAdditionalCreators = AuthFailError{Index: "1.4", Message: "m.room.create event has invalid additional_creators"}
ErrMissingCreator = AuthFailError{Index: "1.4", Message: "m.room.create event is missing creator field"}
ErrInvalidRoomIDLength = AuthFailError{Index: "2", Message: "room ID length is invalid"}
ErrFailedToGetCreateEvent = AuthFailError{Index: "2", Message: "failed to get m.room.create event"}
ErrCreateEventNotFound = AuthFailError{Index: "2", Message: "m.room.create event not found using room ID as event ID"}
ErrRejectedCreateEvent = AuthFailError{Index: "2", Message: "m.room.create event was rejected"}
ErrFailedToGetAuthEvents = AuthFailError{Index: "3", Message: "failed to get auth events"}
ErrFailedToParsePowerLevels = AuthFailError{Index: "?", Message: "failed to parse power levels"}
ErrDuplicateAuthEvent = AuthFailError{Index: "3.1", Message: "duplicate type/state key pair in auth events"}
ErrNonStateAuthEvent = AuthFailError{Index: "3.2", Message: "non-state event in auth events"}
ErrMissingAuthEvent = AuthFailError{Index: "3.2", Message: "missing auth event"}
ErrUnexpectedAuthEvent = AuthFailError{Index: "3.2", Message: "unexpected type/state key pair in auth events"}
ErrNoCreateEvent = AuthFailError{Index: "3.2", Message: "no m.room.create event found in auth events"}
ErrRejectedAuthEvent = AuthFailError{Index: "3.3", Message: "auth event was rejected"}
ErrMismatchingRoomIDInAuthEvent = AuthFailError{Index: "3.4", Message: "auth event room ID does not match event room ID"}
ErrFederationDisabled = AuthFailError{Index: "4", Message: "federation is disabled for this room"}
ErrMemberNotState = AuthFailError{Index: "5.1", Message: "m.room.member event is not a state event"}
ErrNotSignedByAuthoriser = AuthFailError{Index: "5.2", Message: "m.room.member event is not signed by server of join_authorised_via_users_server"}
ErrCantJoinOtherUser = AuthFailError{Index: "5.3.2", Message: "can't send join event with different state key"}
ErrCantJoinBanned = AuthFailError{Index: "5.3.3", Message: "user is banned from the room"}
ErrAuthoriserCantInvite = AuthFailError{Index: "5.3.5.2", Message: "authoriser doesn't have sufficient power level to invite"}
ErrCantJoinWithoutInvite = AuthFailError{Index: "5.3.7", Message: "can't join invite-only room without invite"}
ErrInvalidJoinRule = AuthFailError{Index: "5.3.7", Message: "invalid join rule in room"}
ErrThirdPartyInviteBanned = AuthFailError{Index: "5.4.1.1", Message: "third party invite target user is banned"}
ErrThirdPartyInviteMissingFields = AuthFailError{Index: "5.4.1.3", Message: "third party invite is missing mxid or token fields"}
ErrThirdPartyInviteMXIDMismatch = AuthFailError{Index: "5.4.1.4", Message: "mxid in signed third party invite doesn't match event state key"}
ErrThirdPartyInviteNotFound = AuthFailError{Index: "5.4.1.5", Message: "matching m.room.third_party_invite event not found in auth events"}
ErrThirdPartyInviteSenderMismatch = AuthFailError{Index: "5.4.1.6", Message: "sender of third party invite doesn't match sender of member event"}
ErrThirdPartyInviteNotSigned = AuthFailError{Index: "5.4.1.8", Message: "no valid signatures found for third party invite"}
ErrInviterNotInRoom = AuthFailError{Index: "5.4.2", Message: "inviter's membership is not join"}
ErrInviteTargetAlreadyInRoom = AuthFailError{Index: "5.4.3", Message: "invite target user is already in the room"}
ErrInviteTargetBanned = AuthFailError{Index: "5.4.3", Message: "invite target user is banned"}
ErrInsufficientPermissionForInvite = AuthFailError{Index: "5.4.5", Message: "inviter does not have sufficient permission to send invites"}
ErrCantLeaveWithoutBeingInRoom = AuthFailError{Index: "5.5.1", Message: "can't leave room without being in it"}
ErrCantKickWithoutBeingInRoom = AuthFailError{Index: "5.5.2", Message: "can't kick another user without being in the room"}
ErrInsufficientPermissionForUnban = AuthFailError{Index: "5.5.3", Message: "sender does not have sufficient permission to unban users"}
ErrInsufficientPermissionForKick = AuthFailError{Index: "5.5.5", Message: "sender does not have sufficient permission to kick the user"}
ErrCantBanWithoutBeingInRoom = AuthFailError{Index: "5.6.1", Message: "can't ban another user without being in the room"}
ErrInsufficientPermissionForBan = AuthFailError{Index: "5.6.3", Message: "sender does not have sufficient permission to ban the user"}
ErrNotKnockableRoom = AuthFailError{Index: "5.7.1", Message: "join rule doesn't allow knocking"}
ErrCantKnockOtherUser = AuthFailError{Index: "5.7.1", Message: "can't send knock event with different state key"}
ErrCantKnockWhileInRoom = AuthFailError{Index: "5.7.2", Message: "can't knock while joined, invited or banned"}
ErrUnknownMembership = AuthFailError{Index: "5.8", Message: "unknown membership in m.room.member event"}
ErrNotInRoom = AuthFailError{Index: "6", Message: "sender is not a member of the room"}
ErrInsufficientPowerForThirdPartyInvite = AuthFailError{Index: "7.1", Message: "sender does not have sufficient power level to send third party invite"}
ErrInsufficientPowerLevel = AuthFailError{Index: "8", Message: "sender does not have sufficient power level to send event"}
ErrMismatchingPrivateStateKey = AuthFailError{Index: "9", Message: "state keys starting with @ must match sender user ID"}
ErrTopLevelPLNotInteger = AuthFailError{Index: "10.1", Message: "invalid type for top-level power level field"}
ErrPLNotInteger = AuthFailError{Index: "10.2", Message: "invalid type for power level"}
ErrInvalidUserIDInPL = AuthFailError{Index: "10.3", Message: "invalid user ID in power levels"}
ErrUserPLNotInteger = AuthFailError{Index: "10.3", Message: "invalid type for user power level"}
ErrCreatorInPowerLevels = AuthFailError{Index: "10.4", Message: "room creators must not be specified in power levels"}
ErrInvalidPowerChange = AuthFailError{Index: "10.x", Message: "illegal power level change"}
ErrInvalidUserPowerChange = AuthFailError{Index: "10.9", Message: "illegal power level change"}
)
func isRejected(evt *pdu.PDU) bool {
return evt.InternalMeta.Rejected
}
type GetEventsFunc = func(ids []id.EventID) ([]*pdu.PDU, error)
func Authorize(roomVersion id.RoomVersion, evt *pdu.PDU, getEvents GetEventsFunc, getKey pdu.GetKeyFunc) error {
if evt.Type == event.StateCreate.Type {
// 1. If type is m.room.create:
return authorizeCreate(roomVersion, evt)
}
var createEvt *pdu.PDU
if roomVersion.RoomIDIsCreateEventID() {
// 2. If the events room_id is not an event ID for an accepted (not rejected) m.room.create event,
// with the sigil ! instead of $, reject.
if len(evt.RoomID) != 44 {
return fmt.Errorf("%w (%d)", ErrInvalidRoomIDLength, len(evt.RoomID))
} else if createEvts, err := getEvents([]id.EventID{id.EventID("$" + evt.RoomID[1:])}); err != nil {
return fmt.Errorf("%w: %w", ErrFailedToGetCreateEvent, err)
} else if len(createEvts) != 1 {
return fmt.Errorf("%w (%s)", ErrCreateEventNotFound, evt.RoomID)
} else if isRejected(createEvts[0]) {
return ErrRejectedCreateEvent
} else {
createEvt = createEvts[0]
}
}
authEvents, err := getEvents(evt.AuthEvents)
if err != nil {
return fmt.Errorf("%w: %w", ErrFailedToGetAuthEvents, err)
}
expectedAuthEvents := evt.AuthEventSelection(roomVersion)
deduplicator := make(map[pdu.StateKey]id.EventID, len(expectedAuthEvents))
// 3. Considering the events auth_events:
for i, ae := range authEvents {
authEvtID := evt.AuthEvents[i]
if ae == nil {
return fmt.Errorf("%w (%s)", ErrMissingAuthEvent, authEvtID)
} else if ae.StateKey == nil {
// This approximately falls under rule 3.2.
return fmt.Errorf("%w (%s)", ErrNonStateAuthEvent, authEvtID)
}
key := pdu.StateKey{Type: ae.Type, StateKey: *ae.StateKey}
if prevEvtID, alreadyFound := deduplicator[key]; alreadyFound {
// 3.1. If there are duplicate entries for a given type and state_key pair, reject.
return fmt.Errorf("%w for %s/%s: found %s and %s", ErrDuplicateAuthEvent, ae.Type, *ae.StateKey, prevEvtID, authEvtID)
} else if !expectedAuthEvents.Has(key) {
// 3.2. If there are entries whose type and state_key dont match those specified by
// the auth events selection algorithm described in the server specification, reject.
return fmt.Errorf("%w: found %s with key %s/%s", ErrUnexpectedAuthEvent, authEvtID, ae.Type, *ae.StateKey)
} else if isRejected(ae) {
// 3.3. If there are entries which were themselves rejected under the checks performed on receipt of a PDU, reject.
return fmt.Errorf("%w (%s)", ErrRejectedAuthEvent, authEvtID)
} else if ae.RoomID != evt.RoomID {
// 3.4. If any event in auth_events has a room_id which does not match that of the event being authorised, reject.
return fmt.Errorf("%w (%s)", ErrMismatchingRoomIDInAuthEvent, authEvtID)
} else {
deduplicator[key] = authEvtID
}
if ae.Type == event.StateCreate.Type {
if createEvt == nil {
createEvt = ae
} else {
// Duplicates are prevented by deduplicator, AuthEventSelection also won't allow a create event at all for v12+
panic(fmt.Errorf("impossible case: multiple create events found in auth events"))
}
}
}
if createEvt == nil {
// This comes either from auth_events or room_id depending on the room version.
// The checks above make sure it's from the right source.
return ErrNoCreateEvent
}
if federateVal := gjson.GetBytes(createEvt.Content, mFederatePath); federateVal.Type == gjson.False && createEvt.Sender.Homeserver() != evt.Sender.Homeserver() {
// 4. If the content of the m.room.create event in the room state has the property m.federate set to false,
// and the sender domain of the event does not match the sender domain of the create event, reject.
return ErrFederationDisabled
}
if evt.Type == event.StateMember.Type {
// 5. If type is m.room.member:
return authorizeMember(roomVersion, evt, createEvt, authEvents, getKey)
}
senderMembership := event.Membership(findEventAndReadString(authEvents, event.StateMember.Type, evt.Sender.String(), "membership", "leave"))
if senderMembership != event.MembershipJoin {
// 6. If the senders current membership state is not join, reject.
return ErrNotInRoom
}
powerLevels, err := getPowerLevels(roomVersion, authEvents, createEvt)
if err != nil {
return err
}
senderPL := powerLevels.GetUserLevel(evt.Sender)
if evt.Type == event.StateThirdPartyInvite.Type {
// 7.1. Allow if and only if senders current power level is greater than or equal to the invite level.
if senderPL >= powerLevels.Invite() {
return nil
}
return ErrInsufficientPowerForThirdPartyInvite
}
typeClass := event.MessageEventType
if evt.StateKey != nil {
typeClass = event.StateEventType
}
evtLevel := powerLevels.GetEventLevel(event.Type{Type: evt.Type, Class: typeClass})
if evtLevel > senderPL {
// 8. If the event types required power level is greater than the senders power level, reject.
return fmt.Errorf("%w (%d > %d)", ErrInsufficientPowerLevel, evtLevel, senderPL)
}
if evt.StateKey != nil && strings.HasPrefix(*evt.StateKey, "@") && *evt.StateKey != evt.Sender.String() {
// 9. If the event has a state_key that starts with an @ and does not match the sender, reject.
return ErrMismatchingPrivateStateKey
}
if evt.Type == event.StatePowerLevels.Type {
// 10. If type is m.room.power_levels:
return authorizePowerLevels(roomVersion, evt, createEvt, authEvents)
}
// 11. Otherwise, allow.
return nil
}
var ErrUserIDNotAString = errors.New("not a string")
var ErrUserIDNotValid = errors.New("not a valid user ID")
func isValidUserID(roomVersion id.RoomVersion, userID gjson.Result) error {
if userID.Type != gjson.String {
return ErrUserIDNotAString
}
// In a future room version, user IDs will have stricter validation
_, _, err := id.UserID(userID.Str).Parse()
if err != nil {
return ErrUserIDNotValid
}
return nil
}
func authorizeCreate(roomVersion id.RoomVersion, evt *pdu.PDU) error {
if len(evt.PrevEvents) > 0 {
// 1.1. If it has any prev_events, reject.
return ErrCreateHasPrevEvents
}
if roomVersion.RoomIDIsCreateEventID() {
if evt.RoomID != "" {
// 1.2. If the event has a room_id, reject.
return ErrCreateHasRoomID
}
} else {
_, _, server := id.ParseCommonIdentifier(evt.RoomID)
if server == "" || server != evt.Sender.Homeserver() {
// 1.2. (v11 and below) If the domain of the room_id does not match the domain of the sender, reject.
return ErrRoomIDDoesntMatchSender
}
}
if !roomVersion.IsKnown() {
// 1.3. If content.room_version is present and is not a recognised version, reject.
return fmt.Errorf("%w %s", ErrUnknownRoomVersion, roomVersion)
}
if roomVersion.PrivilegedRoomCreators() {
additionalCreators := gjson.GetBytes(evt.Content, "additional_creators")
if additionalCreators.Exists() {
if !additionalCreators.IsArray() {
return fmt.Errorf("%w: not an array", ErrInvalidAdditionalCreators)
}
for i, item := range additionalCreators.Array() {
// 1.4. If additional_creators is present in content and is not an array of strings
// where each string passes the same user ID validation applied to sender, reject.
if err := isValidUserID(roomVersion, item); err != nil {
return fmt.Errorf("%w: item #%d %w", ErrInvalidAdditionalCreators, i+1, err)
}
}
}
}
if roomVersion.CreatorInContent() {
// 1.4. (v10 and below) If content has no creator property, reject.
if !gjson.GetBytes(evt.Content, "creator").Exists() {
return ErrMissingCreator
}
}
// 1.5. Otherwise, allow.
return nil
}
func authorizeMember(roomVersion id.RoomVersion, evt, createEvt *pdu.PDU, authEvents []*pdu.PDU, getKey pdu.GetKeyFunc) error {
membership := event.Membership(gjson.GetBytes(evt.Content, "membership").Str)
if evt.StateKey == nil {
// 5.1. If there is no state_key property, or no membership property in content, reject.
return ErrMemberNotState
}
authorizedVia := id.UserID(gjson.GetBytes(evt.Content, "authorized_via_users_server").Str)
if authorizedVia != "" {
homeserver := authorizedVia.Homeserver()
err := evt.VerifySignature(roomVersion, homeserver, getKey)
if err != nil {
// 5.2. If content has a join_authorised_via_users_server key:
// 5.2.1. If the event is not validly signed by the homeserver of the user ID denoted by the key, reject.
return fmt.Errorf("%w: %w", ErrNotSignedByAuthoriser, err)
}
}
targetPrevMembership := event.Membership(findEventAndReadString(authEvents, event.StateMember.Type, *evt.StateKey, "membership", "leave"))
senderMembership := event.Membership(findEventAndReadString(authEvents, event.StateMember.Type, evt.Sender.String(), "membership", "leave"))
switch membership {
case event.MembershipJoin:
createEvtID, err := createEvt.GetEventID(roomVersion)
if err != nil {
return fmt.Errorf("failed to get create event ID: %w", err)
}
creator := createEvt.Sender.String()
if roomVersion.CreatorInContent() {
creator = gjson.GetBytes(evt.Content, "creator").Str
}
if len(evt.PrevEvents) == 1 &&
len(evt.AuthEvents) <= 1 &&
evt.PrevEvents[0] == createEvtID &&
*evt.StateKey == creator {
// 5.3.1. If the only previous event is an m.room.create and the state_key is the sender of the m.room.create, allow.
return nil
}
// Spec wart: this would make more sense before the check above.
// Now you can set anyone as the sender of the first join.
if evt.Sender.String() != *evt.StateKey {
// 5.3.2. If the sender does not match state_key, reject.
return ErrCantJoinOtherUser
}
if senderMembership == event.MembershipBan {
// 5.3.3. If the sender is banned, reject.
return ErrCantJoinBanned
}
joinRule := event.JoinRule(findEventAndReadString(authEvents, event.StateJoinRules.Type, "", "join_rule", "invite"))
switch joinRule {
case event.JoinRuleKnock:
if !roomVersion.Knocks() {
return ErrInvalidJoinRule
}
fallthrough
case event.JoinRuleInvite:
// 5.3.4. If the join_rule is invite or knock then allow if membership state is invite or join.
if targetPrevMembership == event.MembershipJoin || targetPrevMembership == event.MembershipInvite {
return nil
}
return ErrCantJoinWithoutInvite
case event.JoinRuleKnockRestricted:
if !roomVersion.KnockRestricted() {
return ErrInvalidJoinRule
}
fallthrough
case event.JoinRuleRestricted:
if joinRule == event.JoinRuleRestricted && !roomVersion.RestrictedJoins() {
return ErrInvalidJoinRule
}
if targetPrevMembership == event.MembershipJoin || targetPrevMembership == event.MembershipInvite {
// 5.3.5.1. If membership state is join or invite, allow.
return nil
}
powerLevels, err := getPowerLevels(roomVersion, authEvents, createEvt)
if err != nil {
return err
}
if powerLevels.GetUserLevel(authorizedVia) < powerLevels.Invite() {
// 5.3.5.2. If the join_authorised_via_users_server key in content is not a user with sufficient permission to invite other users, reject.
return ErrAuthoriserCantInvite
}
// 5.3.5.3. Otherwise, allow.
return nil
case event.JoinRulePublic:
// 5.3.6. If the join_rule is public, allow.
return nil
default:
// 5.3.7. Otherwise, reject.
return ErrInvalidJoinRule
}
case event.MembershipInvite:
tpiVal := gjson.GetBytes(evt.Content, "third_party_invite")
if tpiVal.Exists() {
if targetPrevMembership == event.MembershipBan {
return ErrThirdPartyInviteBanned
}
signed := tpiVal.Get("signed")
mxid := signed.Get("mxid").Str
token := signed.Get("token").Str
if mxid == "" || token == "" {
// 5.4.1.2. If content.third_party_invite does not have a signed property, reject.
// 5.4.1.3. If signed does not have mxid and token properties, reject.
return ErrThirdPartyInviteMissingFields
}
if mxid != *evt.StateKey {
// 5.4.1.4. If mxid does not match state_key, reject.
return ErrThirdPartyInviteMXIDMismatch
}
tpiEvt := findEvent(authEvents, event.StateThirdPartyInvite.Type, token)
if tpiEvt == nil {
// 5.4.1.5. If there is no m.room.third_party_invite event in the current room state with state_key matching token, reject.
return ErrThirdPartyInviteNotFound
}
if tpiEvt.Sender != evt.Sender {
// 5.4.1.6. If sender does not match sender of the m.room.third_party_invite, reject.
return ErrThirdPartyInviteSenderMismatch
}
var keys []id.Ed25519
const ed25519Base64Len = 43
oldPubKey := gjson.GetBytes(evt.Content, "public_key.token")
if oldPubKey.Type == gjson.String && len(oldPubKey.Str) == ed25519Base64Len {
keys = append(keys, id.Ed25519(oldPubKey.Str))
}
gjson.GetBytes(evt.Content, "public_keys").ForEach(func(key, value gjson.Result) bool {
if key.Type != gjson.Number {
return false
}
if value.Type == gjson.String && len(value.Str) == ed25519Base64Len {
keys = append(keys, id.Ed25519(value.Str))
}
return true
})
rawSigned := jsontext.Value(exstrings.UnsafeBytes(signed.Str))
var validated bool
for _, key := range keys {
if signutil.VerifyJSONAny(key, rawSigned) == nil {
validated = true
}
}
if validated {
// 4.4.1.7. If any signature in signed matches any public key in the m.room.third_party_invite event, allow.
return nil
}
// 4.4.1.8. Otherwise, reject.
return ErrThirdPartyInviteNotSigned
}
if senderMembership != event.MembershipJoin {
// 5.4.2. If the senders current membership state is not join, reject.
return ErrInviterNotInRoom
}
// 5.4.3. If target users current membership state is join or ban, reject.
if targetPrevMembership == event.MembershipJoin {
return ErrInviteTargetAlreadyInRoom
} else if targetPrevMembership == event.MembershipBan {
return ErrInviteTargetBanned
}
powerLevels, err := getPowerLevels(roomVersion, authEvents, createEvt)
if err != nil {
return err
}
if powerLevels.GetUserLevel(evt.Sender) >= powerLevels.Invite() {
// 5.4.4. If the senders power level is greater than or equal to the invite level, allow.
return nil
}
// 5.4.5. Otherwise, reject.
return ErrInsufficientPermissionForInvite
case event.MembershipLeave:
if evt.Sender.String() == *evt.StateKey {
// 5.5.1. If the sender matches state_key, allow if and only if that users current membership state is invite, join, or knock.
if senderMembership == event.MembershipInvite ||
senderMembership == event.MembershipJoin ||
(senderMembership == event.MembershipKnock && roomVersion.Knocks()) {
return nil
}
return ErrCantLeaveWithoutBeingInRoom
}
if senderMembership != event.MembershipLeave {
// 5.5.2. If the senders current membership state is not join, reject.
return ErrCantKickWithoutBeingInRoom
}
powerLevels, err := getPowerLevels(roomVersion, authEvents, createEvt)
if err != nil {
return err
}
senderLevel := powerLevels.GetUserLevel(evt.Sender)
if targetPrevMembership == event.MembershipBan && senderLevel < powerLevels.Ban() {
// 5.5.3. If the target users current membership state is ban, and the senders power level is less than the ban level, reject.
return ErrInsufficientPermissionForUnban
}
if senderLevel >= powerLevels.Kick() && powerLevels.GetUserLevel(id.UserID(*evt.StateKey)) < senderLevel {
// 5.5.4. If the senders power level is greater than or equal to the kick level, and the target users power level is less than the senders power level, allow.
return nil
}
// TODO separate errors for < kick and < target user level?
// 5.5.5. Otherwise, reject.
return ErrInsufficientPermissionForKick
case event.MembershipBan:
if senderMembership != event.MembershipLeave {
// 5.6.1. If the senders current membership state is not join, reject.
return ErrCantBanWithoutBeingInRoom
}
powerLevels, err := getPowerLevels(roomVersion, authEvents, createEvt)
if err != nil {
return err
}
senderLevel := powerLevels.GetUserLevel(evt.Sender)
if senderLevel >= powerLevels.Ban() && powerLevels.GetUserLevel(id.UserID(*evt.StateKey)) < senderLevel {
// 5.6.2. If the senders power level is greater than or equal to the ban level, and the target users power level is less than the senders power level, allow.
return nil
}
// 5.6.3. Otherwise, reject.
return ErrInsufficientPermissionForBan
case event.MembershipKnock:
joinRule := event.JoinRule(findEventAndReadString(authEvents, event.StateJoinRules.Type, "", "join_rule", "invite"))
validKnockRule := roomVersion.Knocks() && joinRule == event.JoinRuleKnock
validKnockRestrictedRule := roomVersion.KnockRestricted() && joinRule == event.JoinRuleKnockRestricted
if !validKnockRule && !validKnockRestrictedRule {
// 5.7.1. If the join_rule is anything other than knock or knock_restricted, reject.
return ErrNotKnockableRoom
}
if evt.Sender.String() != *evt.StateKey {
// 5.7.2. If the sender does not match state_key, reject.
return ErrCantKnockOtherUser
}
if senderMembership != event.MembershipBan && senderMembership != event.MembershipInvite && senderMembership != event.MembershipJoin {
// 5.7.3. If the senders current membership is not ban, invite, or join, allow.
return nil
}
// 5.7.4. Otherwise, reject.
return ErrCantKnockWhileInRoom
default:
// 5.8. Otherwise, the membership is unknown. Reject.
return ErrUnknownMembership
}
}
func authorizePowerLevels(roomVersion id.RoomVersion, evt, createEvt *pdu.PDU, authEvents []*pdu.PDU) error {
if roomVersion.ValidatePowerLevelInts() {
for _, key := range []string{"users_default", "events_default", "state_default", "ban", "redact", "kick", "invite"} {
res := gjson.GetBytes(evt.Content, key)
if !res.Exists() {
continue
}
if parseIntWithVersion(roomVersion, res) == nil {
// 10.1. If any of the properties users_default, events_default, state_default, ban, redact, kick, or invite in content are present and not an integer, reject.
return fmt.Errorf("%w %s", ErrTopLevelPLNotInteger, key)
}
}
for _, key := range []string{"events", "notifications"} {
obj := gjson.GetBytes(evt.Content, key)
if !obj.Exists() {
continue
}
// 10.2. If either of the properties events or notifications in content are present and not an object [...], reject.
if !obj.IsObject() {
return fmt.Errorf("%w %s", ErrTopLevelPLNotInteger, key)
}
var err error
// 10.2. [...] are not an object with values that are integers, reject.
obj.ForEach(func(innerKey, value gjson.Result) bool {
if parseIntWithVersion(roomVersion, value) == nil {
err = fmt.Errorf("%w %s.%s", ErrPLNotInteger, key, innerKey.Str)
return false
}
return true
})
if err != nil {
return err
}
}
}
var creators []id.UserID
if roomVersion.PrivilegedRoomCreators() {
creators = append(creators, createEvt.Sender)
gjson.GetBytes(createEvt.Content, "additional_creators").ForEach(func(key, value gjson.Result) bool {
creators = append(creators, id.UserID(value.Str))
return true
})
}
users := gjson.GetBytes(evt.Content, "users")
if users.Exists() {
if !users.IsObject() {
// 10.3. If the users property in content is not an object [...], reject.
return fmt.Errorf("%w users", ErrTopLevelPLNotInteger)
}
var err error
users.ForEach(func(key, value gjson.Result) bool {
if validatorErr := isValidUserID(roomVersion, key); validatorErr != nil {
// 10.3. [...] is not an object with keys that are valid user IDs [...], reject.
err = fmt.Errorf("%w: %q %w", ErrInvalidUserIDInPL, key.Str, validatorErr)
return false
}
if parseIntWithVersion(roomVersion, value) == nil {
// 10.3. [...] is not an object [...] with values that are integers, reject.
err = fmt.Errorf("%w %q", ErrUserPLNotInteger, key.Str)
return false
}
// creators is only filled if the room version has privileged room creators
if slices.Contains(creators, id.UserID(key.Str)) {
// 10.4. If the users property in content contains the sender of the m.room.create event or any of
// the additional_creators array (if present) from the content of the m.room.create event, reject.
err = fmt.Errorf("%w: %q", ErrCreatorInPowerLevels, key.Str)
return false
}
return true
})
if err != nil {
return err
}
}
oldPL := findEvent(authEvents, event.StatePowerLevels.Type, "")
if oldPL == nil {
// 10.5. If there is no previous m.room.power_levels event in the room, allow.
return nil
}
if slices.Contains(creators, evt.Sender) {
// Skip remaining checks for creators
return nil
}
senderPLPtr := parsePythonInt(gjson.GetBytes(oldPL.Content, exgjson.Path("users", evt.Sender.String())))
if senderPLPtr == nil {
senderPLPtr = parsePythonInt(gjson.GetBytes(oldPL.Content, "users_default"))
if senderPLPtr == nil {
senderPLPtr = ptr.Ptr(0)
}
}
for _, key := range []string{"users_default", "events_default", "state_default", "ban", "redact", "kick", "invite"} {
oldVal := gjson.GetBytes(oldPL.Content, key)
newVal := gjson.GetBytes(evt.Content, key)
if err := allowPowerChange(roomVersion, *senderPLPtr, key, oldVal, newVal); err != nil {
return err
}
}
if err := allowPowerChangeMap(
roomVersion, *senderPLPtr, "events", "",
gjson.GetBytes(oldPL.Content, "events"),
gjson.GetBytes(evt.Content, "events"),
); err != nil {
return err
}
if err := allowPowerChangeMap(
roomVersion, *senderPLPtr, "notifications", "",
gjson.GetBytes(oldPL.Content, "notifications"),
gjson.GetBytes(evt.Content, "notifications"),
); err != nil {
return err
}
if err := allowPowerChangeMap(
roomVersion, *senderPLPtr, "users", evt.Sender.String(),
gjson.GetBytes(oldPL.Content, "users"),
gjson.GetBytes(evt.Content, "users"),
); err != nil {
return err
}
return nil
}
func allowPowerChangeMap(roomVersion id.RoomVersion, maxVal int, path, ownID string, old, new gjson.Result) (err error) {
old.ForEach(func(key, value gjson.Result) bool {
newVal := new.Get(exgjson.Path(key.Str))
err = allowPowerChange(roomVersion, maxVal, path+"."+key.Str, value, newVal)
if err == nil && ownID != "" && key.Str != ownID {
parsedOldVal := parseIntWithVersion(roomVersion, value)
parsedNewVal := parseIntWithVersion(roomVersion, newVal)
if *parsedOldVal >= maxVal && *parsedOldVal != *parsedNewVal {
err = fmt.Errorf("%w: can't change users.%s from %s to %s with sender level %d", ErrInvalidUserPowerChange, key.Str, stringifyForError(value), stringifyForError(newVal), maxVal)
}
}
return err == nil
})
if err != nil {
return
}
new.ForEach(func(key, value gjson.Result) bool {
err = allowPowerChange(roomVersion, maxVal, path+"."+key.Str, old.Get(exgjson.Path(key.Str)), value)
return err == nil
})
return
}
func allowPowerChange(roomVersion id.RoomVersion, maxVal int, path string, old, new gjson.Result) error {
oldVal := parseIntWithVersion(roomVersion, old)
newVal := parseIntWithVersion(roomVersion, new)
if oldVal == nil {
if newVal == nil || *newVal <= maxVal {
return nil
}
} else if newVal == nil {
if *oldVal <= maxVal {
return nil
}
} else if *oldVal == *newVal || (*oldVal <= maxVal && *newVal <= maxVal) {
return nil
}
return fmt.Errorf("%w can't change %s from %s to %s with sender level %d", ErrInvalidPowerChange, path, stringifyForError(old), stringifyForError(new), maxVal)
}
func stringifyForError(val gjson.Result) string {
if !val.Exists() {
return "null"
}
return val.Raw
}
func findEvent(events []*pdu.PDU, evtType, stateKey string) *pdu.PDU {
for _, evt := range events {
if evt.Type == evtType && *evt.StateKey == stateKey {
return evt
}
}
return nil
}
func findEventAndReadData[T any](events []*pdu.PDU, evtType, stateKey string, reader func(evt *pdu.PDU) T) T {
return reader(findEvent(events, evtType, stateKey))
}
func findEventAndReadString(events []*pdu.PDU, evtType, stateKey, fieldPath, defVal string) string {
return findEventAndReadData(events, evtType, stateKey, func(evt *pdu.PDU) string {
if evt == nil {
return defVal
}
res := gjson.GetBytes(evt.Content, fieldPath)
if res.Type != gjson.String {
return defVal
}
return res.Str
})
}
func getPowerLevels(roomVersion id.RoomVersion, authEvents []*pdu.PDU, createEvt *pdu.PDU) (*event.PowerLevelsEventContent, error) {
var err error
powerLevels := findEventAndReadData(authEvents, event.StatePowerLevels.Type, "", func(evt *pdu.PDU) *event.PowerLevelsEventContent {
if evt == nil {
return nil
}
content := evt.Content
out := &event.PowerLevelsEventContent{}
if !roomVersion.ValidatePowerLevelInts() {
safeParsePowerLevels(content, out)
} else {
err = json.Unmarshal(content, out)
}
return out
})
if err != nil {
// This should never happen thanks to safeParsePowerLevels for v1-9 and strict validation in v10+
return nil, fmt.Errorf("%w: %w", ErrFailedToParsePowerLevels, err)
}
if roomVersion.PrivilegedRoomCreators() {
if powerLevels == nil {
powerLevels = &event.PowerLevelsEventContent{}
}
powerLevels.CreateEvent, err = createEvt.ToClientEvent(roomVersion)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrFailedToParsePowerLevels, err)
}
err = powerLevels.CreateEvent.Content.ParseRaw(powerLevels.CreateEvent.Type)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrFailedToParsePowerLevels, err)
}
} else if powerLevels == nil {
powerLevels = &event.PowerLevelsEventContent{
Users: map[id.UserID]int{
createEvt.Sender: 100,
},
}
}
return powerLevels, nil
}
func parseIntWithVersion(roomVersion id.RoomVersion, val gjson.Result) *int {
if roomVersion.ValidatePowerLevelInts() {
if val.Type != gjson.Number {
return nil
}
return ptr.Ptr(int(val.Int()))
}
return parsePythonInt(val)
}
func parsePythonInt(val gjson.Result) *int {
switch val.Type {
case gjson.True:
return ptr.Ptr(1)
case gjson.False:
return ptr.Ptr(0)
case gjson.Number:
return ptr.Ptr(int(val.Int()))
case gjson.String:
// strconv.Atoi accepts signs as well as leading zeroes, so we just need to trim spaces beforehand
num, err := strconv.Atoi(strings.TrimSpace(val.Str))
if err != nil {
return nil
}
return &num
default:
// Python int() doesn't accept nulls, arrays or dicts
return nil
}
}
func safeParsePowerLevels(content jsontext.Value, into *event.PowerLevelsEventContent) {
*into = event.PowerLevelsEventContent{
Users: make(map[id.UserID]int),
UsersDefault: ptr.Val(parsePythonInt(gjson.GetBytes(content, "users_default"))),
Events: make(map[string]int),
EventsDefault: ptr.Val(parsePythonInt(gjson.GetBytes(content, "events_default"))),
Notifications: nil, // irrelevant for event auth
StateDefaultPtr: parsePythonInt(gjson.GetBytes(content, "state_default")),
InvitePtr: parsePythonInt(gjson.GetBytes(content, "invite")),
KickPtr: parsePythonInt(gjson.GetBytes(content, "kick")),
BanPtr: parsePythonInt(gjson.GetBytes(content, "ban")),
RedactPtr: parsePythonInt(gjson.GetBytes(content, "redact")),
}
gjson.GetBytes(content, "events").ForEach(func(key, value gjson.Result) bool {
if key.Type != gjson.String {
return false
}
val := parsePythonInt(value)
if val != nil {
into.Events[key.Str] = *val
}
return true
})
gjson.GetBytes(content, "users").ForEach(func(key, value gjson.Result) bool {
if key.Type != gjson.String {
return false
}
val := parsePythonInt(value)
if val == nil {
return false
}
userID := id.UserID(key.Str)
if _, _, err := userID.Parse(); err != nil {
return false
}
into.Users[userID] = *val
return true
})
}