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>
271 lines
8.1 KiB
Go
271 lines
8.1 KiB
Go
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
|
|
}
|