federation/eventauth: add most power level checks
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

This commit is contained in:
Tulir Asokan 2025-08-23 14:11:31 +03:00
commit aa4779bd59

View file

@ -13,6 +13,7 @@ import (
"encoding/json/jsontext" "encoding/json/jsontext"
"errors" "errors"
"fmt" "fmt"
"slices"
"strconv" "strconv"
"strings" "strings"
@ -111,6 +112,13 @@ var (
ErrInsufficientPowerLevel = ErrAuthFail{Index: "8", Message: "sender does not have sufficient power level to send event"} ErrInsufficientPowerLevel = ErrAuthFail{Index: "8", Message: "sender does not have sufficient power level to send event"}
ErrMismatchingPrivateStateKey = ErrAuthFail{Index: "9", Message: "state keys starting with @ must match sender user ID"} ErrMismatchingPrivateStateKey = ErrAuthFail{Index: "9", Message: "state keys starting with @ must match sender user ID"}
ErrTopLevelPLNotInteger = ErrAuthFail{Index: "10.1", Message: "invalid type for top-level power level field"}
ErrPLNotInteger = ErrAuthFail{Index: "10.2", Message: "invalid type for power level"}
ErrInvalidUserIDInPL = ErrAuthFail{Index: "10.3", Message: "invalid user ID in power levels"}
ErrUserPLNotInteger = ErrAuthFail{Index: "10.3", Message: "invalid type for user power level"}
ErrCreatorInPowerLevels = ErrAuthFail{Index: "10.4", Message: "room creators must not be specified in power levels"}
ErrInvalidPowerChange = ErrAuthFail{Index: "10.x", Message: "illegal power level change"}
) )
func isRejected(evt *pdu.PDU) bool { func isRejected(evt *pdu.PDU) bool {
@ -235,6 +243,21 @@ func Authorize(roomVersion id.RoomVersion, evt *pdu.PDU, getEvents GetEventsFunc
return nil 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 { func authorizeCreate(roomVersion id.RoomVersion, evt *pdu.PDU) error {
if len(evt.PrevEvents) > 0 { if len(evt.PrevEvents) > 0 {
// 1.1. If it has any prev_events, reject. // 1.1. If it has any prev_events, reject.
@ -265,12 +288,8 @@ func authorizeCreate(roomVersion id.RoomVersion, evt *pdu.PDU) error {
for i, item := range additionalCreators.Array() { for i, item := range additionalCreators.Array() {
// 1.4. If additional_creators is present in content and is not an array of strings // 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. // where each string passes the same user ID validation applied to sender, reject.
if item.Type != gjson.String { if err := isValidUserID(roomVersion, item); err != nil {
return fmt.Errorf("%w: item #%d is not a string", ErrInvalidAdditionalCreators, i+1) return fmt.Errorf("%w: item #%d %w", ErrInvalidAdditionalCreators, i+1, err)
}
_, _, err := id.UserID(item.Str).Parse()
if err != nil {
return fmt.Errorf("%w: item #%d is not a valid user ID", ErrInvalidAdditionalCreators, i+1)
} }
} }
} }
@ -285,7 +304,7 @@ func authorizeCreate(roomVersion id.RoomVersion, evt *pdu.PDU) error {
return nil return nil
} }
func authorizeMember(roomVersion id.RoomVersion, evt *pdu.PDU, createEvt *pdu.PDU, authEvents []*pdu.PDU, getKey pdu.GetKeyFunc) error { 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) membership := event.Membership(gjson.GetBytes(evt.Content, "membership").Str)
if evt.StateKey == nil { if evt.StateKey == nil {
// 5.1. If there is no state_key property, or no membership property in content, reject. // 5.1. If there is no state_key property, or no membership property in content, reject.
@ -381,9 +400,9 @@ func authorizeMember(roomVersion id.RoomVersion, evt *pdu.PDU, createEvt *pdu.PD
if targetPrevMembership == event.MembershipBan { if targetPrevMembership == event.MembershipBan {
return ErrThirdPartyInviteBanned return ErrThirdPartyInviteBanned
} }
signed := gjson.Get(tpiVal.Raw, "signed") signed := tpiVal.Get("signed")
mxid := gjson.Get(signed.Raw, "mxid").Str mxid := signed.Get("mxid").Str
token := gjson.Get(signed.Raw, "token").Str token := signed.Get("token").Str
if mxid == "" || token == "" { if mxid == "" || token == "" {
// 5.4.1.2. If content.third_party_invite does not have a signed property, reject. // 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. // 5.4.1.3. If signed does not have mxid and token properties, reject.
@ -521,8 +540,165 @@ func authorizeMember(roomVersion id.RoomVersion, evt *pdu.PDU, createEvt *pdu.PD
} }
} }
func authorizePowerLevels(version id.RoomVersion, evt *pdu.PDU, evt2 *pdu.PDU, events []*pdu.PDU) error { func authorizePowerLevels(roomVersion id.RoomVersion, evt, createEvt *pdu.PDU, authEvents []*pdu.PDU) error {
panic("not implemented") 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
}
// FIXME don't allow demoting users with equal PL
if err := allowPowerChangeMap(
roomVersion, *senderPLPtr, "users",
gjson.GetBytes(oldPL.Content, "users"),
gjson.GetBytes(evt.Content, "users"),
); err != nil {
return err
}
return nil
}
func allowPowerChangeMap(roomVersion id.RoomVersion, maxVal int, path string, old, new gjson.Result) (err error) {
old.ForEach(func(key, value gjson.Result) bool {
err = allowPowerChange(roomVersion, maxVal, path+"."+key.Str, value, new.Get(exgjson.Path(key.Path(key.Str))))
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.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 { func findEvent(events []*pdu.PDU, evtType, stateKey string) *pdu.PDU {
@ -578,6 +754,16 @@ func getPowerLevels(roomVersion id.RoomVersion, authEvents []*pdu.PDU, createEvt
return &powerLevels, nil 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 { func parsePythonInt(val gjson.Result) *int {
switch val.Type { switch val.Type {
case gjson.True: case gjson.True: