mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-14 14:25:53 +01:00
federation/eventauth: add most power level checks
This commit is contained in:
parent
7778f0ce7e
commit
aa4779bd59
1 changed files with 198 additions and 12 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue