package auth import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "strings" "sync/atomic" "testing" "time" "github.com/dariuszjaniak/warp/server/internal/matrix/canonicaljson" "github.com/dariuszjaniak/warp/server/internal/matrix/event" "github.com/dariuszjaniak/warp/server/internal/matrix/roomversion" "github.com/dariuszjaniak/warp/server/internal/matrix/signing" ) // The users the tests below use. Alice creates every room; dave is on // another server, which matters for the m.federate rule. const ( alice = "@alice:warp.test" bob = "@bob:warp.test" carol = "@carol:warp.test" dave = "@dave:other.test" ) func ptr[T any](v T) *T { return &v } // roomCounter gives every test room a distinct ID. Two rooms that shared // one would make the rule about auth events from another room untestable, // and would hide a real confusion if the code ever stopped checking it. var roomCounter atomic.Int64 func mustJSON(t *testing.T, v any) json.RawMessage { t.Helper() raw, err := canonicaljson.Marshal(v) if err != nil { t.Fatalf("encode test content: %v", err) } return raw } // room drives a room forward one event at a time, filling in the // prev_events, auth_events and depth that the rules expect so that the // tests can say only what they are actually about. type room struct { t *testing.T rules roomversion.Rules key *signing.KeyPair roomID string state State prev []string depth int64 clock int64 } // newRoom creates a room and nothing else: the create event is in state, // but nobody has joined yet. func newRoom(t *testing.T, v roomversion.Version, createContent map[string]any) *room { t.Helper() key, err := signing.Generate("test") if err != nil { t.Fatalf("generate a signing key: %v", err) } r := &room{ t: t, rules: roomversion.MustGet(v), key: key, state: State{}, depth: 1, clock: 1700000000000, } content := map[string]any{"room_version": string(v)} if r.rules.CreateEventRequiresCreator { content["creator"] = alice } for k, value := range createContent { content[k] = value } if !r.rules.RoomIDIsCreateEventID { r.roomID = fmt.Sprintf("!room%d:warp.test", roomCounter.Add(1)) } create := r.build(event.Builder{ Type: event.TypeCreate, Sender: alice, StateKey: ptr(""), Content: mustJSON(t, content), }) if err := Check(context.Background(), create, r.state, nil); err != nil { t.Fatalf("the create event should be allowed: %v", err) } if r.rules.RoomIDIsCreateEventID { if r.roomID, err = event.RoomIDOfCreateEvent(create); err != nil { t.Fatalf("derive the room ID: %v", err) } } r.add(create) return r } // newJoinedRoom is newRoom plus Alice's join, which is where most tests // want to start. func newJoinedRoom(t *testing.T, v roomversion.Version) *room { t.Helper() r := newRoom(t, v, nil) r.mustSend(event.Builder{ Type: event.TypeMember, Sender: alice, StateKey: ptr(alice), Content: mustJSON(t, map[string]any{"membership": "join"}), }) return r } // build fills in everything about an event that follows from the room // rather than from the test. func (r *room) build(b event.Builder) *event.Event { r.t.Helper() derivesRoomID := r.rules.RoomIDIsCreateEventID && b.Type == event.TypeCreate if b.RoomID == "" && !derivesRoomID { b.RoomID = r.roomID } if b.PrevEvents == nil && b.Type != event.TypeCreate { b.PrevEvents = r.prev } if len(b.Content) == 0 { b.Content = json.RawMessage("{}") } if b.AuthEvents == nil && b.Type != event.TypeCreate { selected, err := SelectAuthEvents(r.rules, b.Type, b.Sender, b.StateKey, b.Content, r.state) if err != nil { r.t.Fatalf("select auth events for %s: %v", b.Type, err) } for _, e := range selected { b.AuthEvents = append(b.AuthEvents, e.EventID()) } } if b.Depth == 0 { b.Depth = r.depth } r.clock++ e, err := b.Build(time.UnixMilli(r.clock), r.key, r.rules) if err != nil { r.t.Fatalf("build a %s event: %v", b.Type, err) } return e } // check builds an event and runs the rules over it without adding it to // the room. func (r *room) check(b event.Builder) error { r.t.Helper() return Check(context.Background(), r.build(b), r.state, nil) } // checkWith is check with a Verifier, for the restricted join rule. func (r *room) checkWith(b event.Builder, verifier Verifier) error { r.t.Helper() return Check(context.Background(), r.build(b), r.state, verifier) } // mustSend requires an event to be allowed and adds it to the room. func (r *room) mustSend(b event.Builder) *event.Event { r.t.Helper() e := r.build(b) if err := Check(context.Background(), e, r.state, nil); err != nil { r.t.Fatalf("a %s event from %s should be allowed: %v", b.Type, b.Sender, err) } r.add(e) return e } // add puts an event into the room's state and makes it the latest event. func (r *room) add(e *event.Event) { if sk, ok := e.StateKey(); ok { r.state[StateKey{Type: e.Type(), StateKey: sk}] = e } r.prev = []string{e.EventID()} r.depth++ } // setPowerLevels sends a power levels event as Alice, who can always send // the first one. func (r *room) setPowerLevels(content map[string]any) *event.Event { r.t.Helper() if r.rules.CreatorsHaveInfinitePower { // From room version 12 naming a creator here is a rejection in // itself, and there is no need to: their level is already above // anything the event could say. Dropping them lets the tests that // run against every version write one power levels event. content = withoutCreators(r.t, content, r.state.Create()) } return r.mustSend(event.Builder{ Type: event.TypePowerLevels, Sender: alice, StateKey: ptr(""), Content: mustJSON(r.t, content), }) } // withoutCreators copies content with the room's creators removed from // the users map. func withoutCreators(t *testing.T, content map[string]any, create *event.Event) map[string]any { t.Helper() users, ok := content["users"].(map[string]any) if !ok { return content } creators, err := Creators(create) if err != nil { t.Fatalf("read the room's creators: %v", err) } trimmed := map[string]any{} for k, v := range users { trimmed[k] = v } for _, creator := range creators { delete(trimmed, creator) } out := map[string]any{} for k, v := range content { out[k] = v } out["users"] = trimmed return out } func (r *room) setJoinRule(rule string, extra map[string]any) *event.Event { r.t.Helper() content := map[string]any{"join_rule": rule} for k, v := range extra { content[k] = v } return r.mustSend(event.Builder{ Type: event.TypeJoinRules, Sender: alice, StateKey: ptr(""), Content: mustJSON(r.t, content), }) } func (r *room) member(sender, target, membership string, extra map[string]any) event.Builder { content := map[string]any{"membership": membership} for k, v := range extra { content[k] = v } return event.Builder{ Type: event.TypeMember, Sender: sender, StateKey: ptr(target), Content: mustJSON(r.t, content), } } // message is a plain message event, the thing rule 6 and rule 8 guard. func (r *room) message(sender string) event.Builder { return event.Builder{ Type: "m.room.message", Sender: sender, Content: mustJSON(r.t, map[string]any{"body": "hello", "msgtype": "m.text"}), } } // wantRejected asserts that err is a rejection mentioning want. func wantRejected(t *testing.T, err error, want string) { t.Helper() if err == nil { t.Fatalf("the event was allowed; expected a rejection mentioning %q", want) } if !errors.Is(err, ErrRejected) { t.Fatalf("error = %v, want it to wrap ErrRejected", err) } if want != "" && !strings.Contains(err.Error(), want) { t.Errorf("rejection = %q, want it to mention %q", err, want) } } func wantAllowed(t *testing.T, err error) { t.Helper() if err != nil { t.Fatalf("the event should have been allowed: %v", err) } } // supportedVersions is every version the tests run the shared rules // against, so that a rule that changed by accident in one version shows up. func supportedVersions() []roomversion.Version { return roomversion.Supported() } // --- Rule 1: m.room.create --- func TestCreateEventRules(t *testing.T) { t.Run("a create event with prev_events is rejected", func(t *testing.T) { r := newRoom(t, roomversion.V11, nil) e := r.build(event.Builder{ Type: event.TypeCreate, Sender: alice, StateKey: ptr(""), RoomID: "!other:warp.test", PrevEvents: []string{"$something"}, Content: mustJSON(t, map[string]any{"room_version": "11"}), }) wantRejected(t, Check(context.Background(), e, State{}, nil), "no prev_events") }) t.Run("the room_id domain must match the sender", func(t *testing.T) { r := newRoom(t, roomversion.V11, nil) e := r.build(event.Builder{ Type: event.TypeCreate, Sender: alice, StateKey: ptr(""), RoomID: "!room:elsewhere.test", Content: mustJSON(t, map[string]any{"room_version": "11"}), }) wantRejected(t, Check(context.Background(), e, State{}, nil), "does not match the sender") }) t.Run("an unrecognised room version is rejected", func(t *testing.T) { r := newRoom(t, roomversion.V11, nil) e := r.build(event.Builder{ Type: event.TypeCreate, Sender: alice, StateKey: ptr(""), RoomID: "!room:warp.test", Content: mustJSON(t, map[string]any{"room_version": "99-not-real"}), }) wantRejected(t, Check(context.Background(), e, State{}, nil), "not one this server recognises") }) t.Run("a room version 1 room is rejected because we cannot join it", func(t *testing.T) { r := newRoom(t, roomversion.V11, nil) e := r.build(event.Builder{ Type: event.TypeCreate, Sender: alice, StateKey: ptr(""), RoomID: "!room:warp.test", Content: mustJSON(t, map[string]any{"room_version": "1"}), }) wantRejected(t, Check(context.Background(), e, State{}, nil), "not one this server recognises") }) t.Run("version 10 requires a creator", func(t *testing.T) { r := newRoom(t, roomversion.V10, nil) e := r.build(event.Builder{ Type: event.TypeCreate, Sender: alice, StateKey: ptr(""), RoomID: "!room:warp.test", Content: mustJSON(t, map[string]any{"room_version": "10"}), }) wantRejected(t, Check(context.Background(), e, State{}, nil), "must have a creator") }) t.Run("version 11 does not require a creator", func(t *testing.T) { newRoom(t, roomversion.V11, nil) // newRoom checks the create event }) t.Run("version 12 rejects additional_creators that are not user IDs", func(t *testing.T) { r := newRoom(t, roomversion.V12, nil) e := r.build(event.Builder{ Type: event.TypeCreate, Sender: alice, StateKey: ptr(""), Content: mustJSON(t, map[string]any{"room_version": "12", "additional_creators": []string{"not-a-user"}}), }) wantRejected(t, Check(context.Background(), e, State{}, nil), "additional_creators") }) t.Run("version 11 ignores additional_creators", func(t *testing.T) { // The property has no meaning before version 12, so it is content // like any other and must not be validated. newRoom(t, roomversion.V11, map[string]any{"additional_creators": []string{"not-a-user"}}) }) } // --- Rule 2 (room version 12): the room ID is the create event --- func TestVersion12RoomIDMustMatchCreateEvent(t *testing.T) { r := newJoinedRoom(t, roomversion.V12) wrong := r.build(event.Builder{ Type: "m.room.message", Sender: alice, RoomID: "!NotTheCreateEventsID", Content: mustJSON(t, map[string]any{"body": "hi"}), }) wantRejected(t, Check(context.Background(), wrong, r.state, nil), "is not the ID of this room") } // --- Rule 3: auth_events --- func TestCheckAuthEvents(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setPowerLevels(map[string]any{"users": map[string]any{alice: 100}}) authFor := func(e *event.Event) []*event.Event { var out []*event.Event for _, want := range e.AuthEventIDs() { for _, candidate := range r.state { if candidate.EventID() == want { out = append(out, candidate) } } } return out } t.Run("a well-formed event passes", func(t *testing.T) { e := r.build(r.message(alice)) if err := CheckAuthEvents(e, authFor(e)); err != nil { t.Errorf("CheckAuthEvents: %v", err) } }) t.Run("a create event must cite nothing", func(t *testing.T) { create := r.state.Create() if err := CheckAuthEvents(create, nil); err != nil { t.Errorf("CheckAuthEvents on the create event: %v", err) } }) t.Run("two events for one piece of state are rejected", func(t *testing.T) { // Not the same event twice: two *different* member events for // Alice, which leaves it ambiguous which one authorised this. second := r.build(r.member(alice, alice, "join", map[string]any{"displayname": "Alice again"})) e := r.build(r.message(alice)) wantRejected(t, CheckAuthEvents(e, append(authFor(e), second)), "two entries") }) t.Run("an entry the selection algorithm does not ask for is rejected", func(t *testing.T) { joinRules := r.setJoinRule(JoinRulePublic, nil) e := r.build(r.message(alice)) // A message event never needs the join rules. wantRejected(t, CheckAuthEvents(e, append(authFor(e), joinRules)), "the auth events selection algorithm does not ask for") }) t.Run("an entry that resolved to nothing is rejected", func(t *testing.T) { e := r.build(r.message(alice)) auth := authFor(e) for i, a := range auth { if a.Type() != event.TypeCreate { auth[i] = nil // a storage lookup that found nothing break } } wantRejected(t, CheckAuthEvents(e, auth), "missing or were themselves rejected") }) t.Run("a missing create event is rejected", func(t *testing.T) { e := r.build(r.message(alice)) var withoutCreate []*event.Event for _, auth := range authFor(e) { if auth.Type() != event.TypeCreate { withoutCreate = append(withoutCreate, auth) } } wantRejected(t, CheckAuthEvents(e, withoutCreate), "no m.room.create event") }) t.Run("an auth event from another room is rejected", func(t *testing.T) { other := newJoinedRoom(t, roomversion.V11) e := r.build(r.message(alice)) auth := authFor(e) for i, a := range auth { if a.Type() == event.TypeMember { auth[i] = other.state.Member(alice) } } wantRejected(t, CheckAuthEvents(e, auth), "belongs to room") }) } func TestAuthEventKeys(t *testing.T) { rules := roomversion.MustGet(roomversion.V11) t.Run("a message event", func(t *testing.T) { keys, err := AuthEventKeysFor(rules, "m.room.message", alice, nil, json.RawMessage(`{}`)) if err != nil { t.Fatal(err) } want := []StateKey{ {Type: event.TypeCreate}, {Type: event.TypePowerLevels}, {Type: event.TypeMember, StateKey: alice}, } if fmt.Sprint(keys) != fmt.Sprint(want) { t.Errorf("keys = %v, want %v", keys, want) } }) t.Run("a join also needs the join rules", func(t *testing.T) { keys, err := AuthEventKeysFor(rules, event.TypeMember, bob, ptr(bob), json.RawMessage(`{"membership":"join"}`)) if err != nil { t.Fatal(err) } // The sender and target are the same user, so their member event // is cited once, not twice. want := []StateKey{ {Type: event.TypeCreate}, {Type: event.TypePowerLevels}, {Type: event.TypeMember, StateKey: bob}, {Type: event.TypeJoinRules}, } if fmt.Sprint(keys) != fmt.Sprint(want) { t.Errorf("keys = %v, want %v", keys, want) } }) t.Run("a restricted join cites the authorising user", func(t *testing.T) { keys, err := AuthEventKeysFor(rules, event.TypeMember, bob, ptr(bob), json.RawMessage(`{"membership":"join","join_authorised_via_users_server":"`+carol+`"}`)) if err != nil { t.Fatal(err) } if !containsKey(keys, StateKey{Type: event.TypeMember, StateKey: carol}) { t.Errorf("keys = %v, want the authorising user's member event", keys) } }) t.Run("a third-party invite cites the invite event", func(t *testing.T) { keys, err := AuthEventKeysFor(rules, event.TypeMember, alice, ptr(bob), json.RawMessage(`{"membership":"invite","third_party_invite":{"signed":{"token":"tok"}}}`)) if err != nil { t.Fatal(err) } if !containsKey(keys, StateKey{Type: event.TypeThirdPartyInvite, StateKey: "tok"}) { t.Errorf("keys = %v, want the third-party invite event", keys) } }) t.Run("a create event cites nothing", func(t *testing.T) { keys, err := AuthEventKeysFor(rules, event.TypeCreate, alice, ptr(""), json.RawMessage(`{}`)) if err != nil { t.Fatal(err) } if len(keys) != 0 { t.Errorf("keys = %v, want none", keys) } }) t.Run("version 12 leaves out the create event", func(t *testing.T) { keys, err := AuthEventKeysFor(roomversion.MustGet(roomversion.V12), "m.room.message", alice, nil, json.RawMessage(`{}`)) if err != nil { t.Fatal(err) } if containsKey(keys, StateKey{Type: event.TypeCreate}) { t.Errorf("keys = %v, want no create event in a version 12 room", keys) } }) } func containsKey(keys []StateKey, want StateKey) bool { for _, k := range keys { if k == want { return true } } return false } // --- Rule 4: m.federate --- func TestFederateFalseKeepsOtherServersOut(t *testing.T) { r := newRoom(t, roomversion.V11, map[string]any{"m.federate": false}) r.mustSend(r.member(alice, alice, "join", nil)) r.setJoinRule(JoinRulePublic, nil) wantRejected(t, r.check(r.member(dave, dave, "join", nil)), "does not federate") wantAllowed(t, r.check(r.member(bob, bob, "join", nil))) } func TestFederateTrueLetsOtherServersIn(t *testing.T) { r := newRoom(t, roomversion.V11, map[string]any{"m.federate": true}) r.mustSend(r.member(alice, alice, "join", nil)) r.setJoinRule(JoinRulePublic, nil) wantAllowed(t, r.check(r.member(dave, dave, "join", nil))) } // --- Rule 5.3: joining --- func TestJoinRules(t *testing.T) { for _, v := range supportedVersions() { t.Run(string(v), func(t *testing.T) { t.Run("public lets anybody in", func(t *testing.T) { r := newJoinedRoom(t, v) r.setJoinRule(JoinRulePublic, nil) wantAllowed(t, r.check(r.member(bob, bob, "join", nil))) }) t.Run("invite keeps the uninvited out", func(t *testing.T) { r := newJoinedRoom(t, v) r.setJoinRule(JoinRuleInvite, nil) wantRejected(t, r.check(r.member(bob, bob, "join", nil)), "has not been invited") }) t.Run("an invited user may join", func(t *testing.T) { r := newJoinedRoom(t, v) r.setJoinRule(JoinRuleInvite, nil) r.mustSend(r.member(alice, bob, "invite", nil)) wantAllowed(t, r.check(r.member(bob, bob, "join", nil))) }) t.Run("a banned user may not join a public room", func(t *testing.T) { r := newJoinedRoom(t, v) r.setPowerLevels(map[string]any{"users": map[string]any{alice: 100}}) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(alice, bob, "ban", nil)) wantRejected(t, r.check(r.member(bob, bob, "join", nil)), "banned") }) t.Run("you cannot join on somebody else's behalf", func(t *testing.T) { r := newJoinedRoom(t, v) r.setJoinRule(JoinRulePublic, nil) wantRejected(t, r.check(r.member(alice, bob, "join", nil)), "on their behalf") }) t.Run("a room with no join rules event is invite only", func(t *testing.T) { r := newJoinedRoom(t, v) wantRejected(t, r.check(r.member(bob, bob, "join", nil)), "has not been invited") }) t.Run("an unknown join rule keeps people out", func(t *testing.T) { r := newJoinedRoom(t, v) r.setJoinRule("com.example.secret_handshake", nil) wantRejected(t, r.check(r.member(bob, bob, "join", nil)), "does not allow joining") }) }) } } // TestCreatorJoinIsAllowedWithNoState covers rule 5.3.1, the join that has // nothing but the create event behind it. func TestCreatorJoinIsAllowedWithNoState(t *testing.T) { for _, v := range supportedVersions() { t.Run(string(v), func(t *testing.T) { r := newRoom(t, v, nil) // The room is invite-only by default and Alice has not been // invited, so only the special case can allow this. wantAllowed(t, r.check(r.member(alice, alice, "join", nil))) }) } } func TestCreatorJoinSpecialCaseIsNarrow(t *testing.T) { r := newRoom(t, roomversion.V11, nil) t.Run("it does not apply to another user", func(t *testing.T) { wantRejected(t, r.check(r.member(bob, bob, "join", nil)), "has not been invited") }) t.Run("it does not apply once there is another event", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.mustSend(r.member(alice, alice, "leave", nil)) // Alice has left; her join must now go through the join rules like // anybody else's, and the room is invite-only. wantRejected(t, r.check(r.member(alice, alice, "join", nil)), "has not been invited") }) } // --- Rule 5.3.5: restricted rooms --- type fakeVerifier struct{ err error } func (f fakeVerifier) VerifyEventSignature(context.Context, string, *event.Event) error { return f.err } func TestRestrictedJoinRule(t *testing.T) { setup := func(t *testing.T) *room { t.Helper() r := newJoinedRoom(t, roomversion.V11) r.setPowerLevels(map[string]any{"users": map[string]any{alice: 100}, "invite": 50}) r.setJoinRule(JoinRuleRestricted, map[string]any{ "allow": []any{map[string]any{"type": "m.room_membership", "room_id": "!space:warp.test"}}, }) return r } t.Run("an unauthorised join is rejected", func(t *testing.T) { r := setup(t) wantRejected(t, r.check(r.member(bob, bob, "join", nil)), "not authorised by any user") }) t.Run("a join authorised by a member who can invite is allowed", func(t *testing.T) { r := setup(t) wantAllowed(t, r.checkWith( r.member(bob, bob, "join", map[string]any{"join_authorised_via_users_server": alice}), fakeVerifier{})) }) t.Run("the authorising user must be in the room", func(t *testing.T) { r := setup(t) wantRejected(t, r.checkWith( r.member(bob, bob, "join", map[string]any{"join_authorised_via_users_server": carol}), fakeVerifier{}), "is not joined to the room") }) t.Run("the authorising user must be able to invite", func(t *testing.T) { r := setup(t) r.setJoinRule(JoinRuleInvite, nil) r.mustSend(r.member(alice, carol, "invite", nil)) r.mustSend(r.member(carol, carol, "join", nil)) r.setJoinRule(JoinRuleRestricted, map[string]any{"allow": []any{}}) wantRejected(t, r.checkWith( r.member(bob, bob, "join", map[string]any{"join_authorised_via_users_server": carol}), fakeVerifier{}), "below the invite level") }) t.Run("a bad signature on the authorisation is rejected", func(t *testing.T) { r := setup(t) wantRejected(t, r.checkWith( r.member(bob, bob, "join", map[string]any{"join_authorised_via_users_server": alice}), fakeVerifier{err: errors.New("no such key")}), "no valid signature") }) t.Run("without a verifier the join is not allowed", func(t *testing.T) { r := setup(t) err := r.check(r.member(bob, bob, "join", map[string]any{"join_authorised_via_users_server": alice})) if !errors.Is(err, ErrNoVerifier) { t.Errorf("error = %v, want ErrNoVerifier", err) } }) t.Run("an existing invite is enough on its own", func(t *testing.T) { r := setup(t) r.mustSend(r.member(alice, bob, "invite", nil)) wantAllowed(t, r.check(r.member(bob, bob, "join", nil))) }) t.Run("version 7 has no restricted join rule", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V7) r.setPowerLevels(map[string]any{"users": map[string]any{alice: 100}}) r.setJoinRule(JoinRuleRestricted, nil) wantRejected(t, r.check(r.member(bob, bob, "join", nil)), "does not have the restricted join rule") }) t.Run("version 9 has no knock_restricted join rule", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V9) r.setPowerLevels(map[string]any{"users": map[string]any{alice: 100}}) r.setJoinRule(JoinRuleKnockRestricted, nil) wantRejected(t, r.check(r.member(bob, bob, "join", nil)), "does not have the knock_restricted join rule") }) t.Run("version 10 has knock_restricted", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V10) r.setPowerLevels(map[string]any{"users": map[string]any{alice: 100}, "invite": 0}) r.setJoinRule(JoinRuleKnockRestricted, nil) wantAllowed(t, r.checkWith( r.member(bob, bob, "join", map[string]any{"join_authorised_via_users_server": alice}), fakeVerifier{})) }) } // --- Rule 5.4: inviting --- func TestInviteRules(t *testing.T) { t.Run("a stranger cannot invite", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) wantRejected(t, r.check(r.member(bob, carol, "invite", nil)), "not joined to the room") }) t.Run("inviting somebody already joined is rejected", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(bob, bob, "join", nil)) wantRejected(t, r.check(r.member(alice, bob, "invite", nil)), "already") }) t.Run("inviting somebody banned is rejected", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setPowerLevels(map[string]any{"users": map[string]any{alice: 100}}) r.mustSend(r.member(alice, bob, "ban", nil)) wantRejected(t, r.check(r.member(alice, bob, "invite", nil)), "already") }) t.Run("inviting needs the invite level", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setPowerLevels(map[string]any{"users": map[string]any{alice: 100, bob: 0}, "invite": 50}) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(bob, bob, "join", nil)) wantRejected(t, r.check(r.member(bob, carol, "invite", nil)), "below the invite level") wantAllowed(t, r.check(r.member(alice, carol, "invite", nil))) }) } // --- Rule 5.5 and 5.6: leaving, kicking and banning --- func TestLeaveRules(t *testing.T) { t.Run("a joined user may leave", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) wantAllowed(t, r.check(r.member(alice, alice, "leave", nil))) }) t.Run("an invited user may turn the invitation down", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.mustSend(r.member(alice, bob, "invite", nil)) wantAllowed(t, r.check(r.member(bob, bob, "leave", nil))) }) t.Run("a stranger cannot leave a room they were never in", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) wantRejected(t, r.check(r.member(bob, bob, "leave", nil)), "cannot leave from membership") }) t.Run("a knocking user may withdraw", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setJoinRule(JoinRuleKnock, nil) r.mustSend(r.member(bob, bob, "knock", nil)) wantAllowed(t, r.check(r.member(bob, bob, "leave", nil))) }) } func TestKickRules(t *testing.T) { setup := func(t *testing.T, levels map[string]any) *room { t.Helper() r := newJoinedRoom(t, roomversion.V11) r.setPowerLevels(levels) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(bob, bob, "join", nil)) r.mustSend(r.member(carol, carol, "join", nil)) return r } t.Run("a moderator may kick somebody below them", func(t *testing.T) { r := setup(t, map[string]any{"users": map[string]any{alice: 100, bob: 50}, "kick": 50}) wantAllowed(t, r.check(r.member(bob, carol, "leave", nil))) }) t.Run("below the kick level is a rejection", func(t *testing.T) { r := setup(t, map[string]any{"users": map[string]any{alice: 100, bob: 25}, "kick": 50}) wantRejected(t, r.check(r.member(bob, carol, "leave", nil)), "below the kick level") }) t.Run("you cannot kick somebody at your own level", func(t *testing.T) { r := setup(t, map[string]any{"users": map[string]any{alice: 100, bob: 50, carol: 50}, "kick": 50}) wantRejected(t, r.check(r.member(bob, carol, "leave", nil)), "does not exceed") }) t.Run("you cannot kick somebody above you", func(t *testing.T) { r := setup(t, map[string]any{"users": map[string]any{alice: 100, bob: 50, carol: 75}, "kick": 50}) wantRejected(t, r.check(r.member(bob, carol, "leave", nil)), "does not exceed") }) t.Run("a non-member cannot kick", func(t *testing.T) { r := setup(t, map[string]any{"users": map[string]any{alice: 100}, "kick": 0}) wantRejected(t, r.check(r.member(dave, carol, "leave", nil)), "not joined to the room") }) } func TestBanRules(t *testing.T) { setup := func(t *testing.T, levels map[string]any) *room { t.Helper() r := newJoinedRoom(t, roomversion.V11) r.setPowerLevels(levels) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(bob, bob, "join", nil)) r.mustSend(r.member(carol, carol, "join", nil)) return r } t.Run("a moderator may ban somebody below them", func(t *testing.T) { r := setup(t, map[string]any{"users": map[string]any{alice: 100, bob: 50}, "ban": 50}) wantAllowed(t, r.check(r.member(bob, carol, "ban", nil))) }) t.Run("below the ban level is a rejection", func(t *testing.T) { r := setup(t, map[string]any{"users": map[string]any{alice: 100, bob: 25}, "ban": 50}) wantRejected(t, r.check(r.member(bob, carol, "ban", nil)), "below the ban level") }) t.Run("you cannot ban a peer", func(t *testing.T) { r := setup(t, map[string]any{"users": map[string]any{alice: 100, bob: 50, carol: 50}, "ban": 50}) wantRejected(t, r.check(r.member(bob, carol, "ban", nil)), "does not exceed") }) // The spec spells this consequence out: unbanning takes both the kick // and the ban level, because it is a leave event over a ban. t.Run("unbanning needs both the kick and the ban level", func(t *testing.T) { r := setup(t, map[string]any{ "users": map[string]any{alice: 100, bob: 50}, "ban": 50, "kick": 100, }) r.mustSend(r.member(alice, carol, "ban", nil)) wantRejected(t, r.check(r.member(bob, carol, "leave", nil)), "below the kick level") r2 := setup(t, map[string]any{ "users": map[string]any{alice: 100, bob: 50}, "ban": 100, "kick": 50, }) r2.mustSend(r2.member(alice, carol, "ban", nil)) wantRejected(t, r2.check(r2.member(bob, carol, "leave", nil)), "below the ban level") }) } // --- Rule 5.7: knocking --- func TestKnockRules(t *testing.T) { t.Run("knocking works in a knock room", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setJoinRule(JoinRuleKnock, nil) wantAllowed(t, r.check(r.member(bob, bob, "knock", nil))) }) t.Run("knocking on an invite-only room is rejected", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setJoinRule(JoinRuleInvite, nil) wantRejected(t, r.check(r.member(bob, bob, "knock", nil)), "cannot be knocked on") }) t.Run("you cannot knock for somebody else", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setJoinRule(JoinRuleKnock, nil) wantRejected(t, r.check(r.member(bob, carol, "knock", nil)), "on behalf of") }) t.Run("a joined user cannot knock", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setJoinRule(JoinRuleKnock, nil) wantRejected(t, r.check(r.member(alice, alice, "knock", nil)), "while their membership is") }) t.Run("a banned user cannot knock", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setPowerLevels(map[string]any{"users": map[string]any{alice: 100}}) r.setJoinRule(JoinRuleKnock, nil) r.mustSend(r.member(alice, bob, "ban", nil)) wantRejected(t, r.check(r.member(bob, bob, "knock", nil)), "while their membership is") }) t.Run("version 6 does not allow knocking", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V6) r.setJoinRule(JoinRuleKnock, nil) wantRejected(t, r.check(r.member(bob, bob, "knock", nil)), "does not allow knocking") }) } func TestUnknownMembershipIsRejected(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) wantRejected(t, r.check(r.member(alice, alice, "sideways", nil)), "unknown membership") } func TestMemberEventNeedsStateKeyAndMembership(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) // The auth events selection algorithm cannot run on this event either, // so the harness is told not to try. wantRejected(t, r.check(event.Builder{ Type: event.TypeMember, Sender: alice, AuthEvents: []string{}, Content: mustJSON(t, map[string]any{"membership": "join"}), }), "must have a state_key") wantRejected(t, r.check(event.Builder{ Type: event.TypeMember, Sender: alice, StateKey: ptr(alice), Content: mustJSON(t, map[string]any{"displayname": "Alice"}), }), "must have a membership") } // --- Rules 6 to 9 --- func TestOnlyMembersMaySendEvents(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) wantAllowed(t, r.check(r.message(alice))) wantRejected(t, r.check(r.message(bob)), "not joined to the room") } func TestSendLevels(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setPowerLevels(map[string]any{ "users": map[string]any{alice: 100, bob: 0}, "events_default": 25, "events": map[string]any{"com.example.special": 75}, }) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(bob, bob, "join", nil)) wantRejected(t, r.check(r.message(bob)), "below the 25 needed") wantAllowed(t, r.check(r.message(alice))) wantRejected(t, r.check(event.Builder{Type: "com.example.special", Sender: bob}), "below the 75 needed") wantAllowed(t, r.check(event.Builder{Type: "com.example.special", Sender: alice})) } func TestStateDefaultAppliesWithNoPowerLevelsEvent(t *testing.T) { // With no power levels event the creator has 100 and state_default is // 50, so the creator can send state and nobody else can. r := newJoinedRoom(t, roomversion.V11) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(bob, bob, "join", nil)) wantAllowed(t, r.check(event.Builder{Type: "m.room.topic", Sender: alice, StateKey: ptr("")})) wantRejected(t, r.check(event.Builder{Type: "m.room.topic", Sender: bob, StateKey: ptr("")}), "below the 50 needed") } func TestStateKeyedByUserIDBelongsToThatUser(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setPowerLevels(map[string]any{"users": map[string]any{alice: 100}, "state_default": 0}) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(bob, bob, "join", nil)) wantAllowed(t, r.check(event.Builder{Type: "com.example.profile", Sender: bob, StateKey: ptr(bob)})) wantRejected(t, r.check(event.Builder{Type: "com.example.profile", Sender: bob, StateKey: ptr(alice)}), "is a user ID that is not the sender") // A state key that is not a user ID is not affected. wantAllowed(t, r.check(event.Builder{Type: "com.example.thing", Sender: bob, StateKey: ptr("anything")})) } func TestThirdPartyInviteEventNeedsInviteLevel(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setPowerLevels(map[string]any{"users": map[string]any{alice: 100, bob: 0}, "invite": 50}) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(bob, bob, "join", nil)) invite := func(sender string) event.Builder { return event.Builder{ Type: event.TypeThirdPartyInvite, Sender: sender, StateKey: ptr("token"), Content: mustJSON(t, map[string]any{"display_name": "someone", "public_key": "abc"}), } } wantAllowed(t, r.check(invite(alice))) wantRejected(t, r.check(invite(bob)), "below the invite level") } // --- Rule 10: power levels --- func TestPowerLevelChanges(t *testing.T) { setup := func(t *testing.T) *room { t.Helper() r := newJoinedRoom(t, roomversion.V11) r.setPowerLevels(map[string]any{ "users": map[string]any{alice: 100, bob: 50, carol: 50}, "events_default": 0, "state_default": 50, "ban": 50, "kick": 50, }) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(bob, bob, "join", nil)) r.mustSend(r.member(carol, carol, "join", nil)) return r } change := func(r *room, sender string, content map[string]any) event.Builder { return event.Builder{ Type: event.TypePowerLevels, Sender: sender, StateKey: ptr(""), Content: mustJSON(t, content), } } t.Run("the first power levels event is always allowed", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) // Alice's level is 100 by default, so she could not otherwise give // herself 100 and set state_default to 100. wantAllowed(t, r.check(change(r, alice, map[string]any{ "users": map[string]any{alice: 100}, "state_default": 100, }))) }) t.Run("you cannot raise yourself above your own level", func(t *testing.T) { r := setup(t) wantRejected(t, r.check(change(r, bob, map[string]any{ "users": map[string]any{alice: 100, bob: 75, carol: 50}, })), "would become 75") }) t.Run("you may lower your own level", func(t *testing.T) { r := setup(t) wantAllowed(t, r.check(change(r, bob, map[string]any{ "users": map[string]any{alice: 100, bob: 25, carol: 50}, }))) }) t.Run("you may promote somebody up to your own level", func(t *testing.T) { r := setup(t) wantAllowed(t, r.check(change(r, alice, map[string]any{ "users": map[string]any{alice: 100, bob: 100, carol: 50}, }))) }) t.Run("you cannot demote a peer", func(t *testing.T) { r := setup(t) wantRejected(t, r.check(change(r, bob, map[string]any{ "users": map[string]any{alice: 100, bob: 50, carol: 0}, })), "at the sender's own power level") }) t.Run("you cannot remove a peer", func(t *testing.T) { r := setup(t) wantRejected(t, r.check(change(r, bob, map[string]any{ "users": map[string]any{alice: 100, bob: 50}, })), "at the sender's own power level") }) t.Run("you cannot demote somebody above you", func(t *testing.T) { r := setup(t) wantRejected(t, r.check(change(r, bob, map[string]any{ "users": map[string]any{alice: 50, bob: 50, carol: 50}, })), "above the sender's power level") }) t.Run("an admin may demote anybody", func(t *testing.T) { r := setup(t) wantAllowed(t, r.check(change(r, alice, map[string]any{ "users": map[string]any{alice: 100, bob: 0, carol: 0}, }))) }) t.Run("you cannot raise a named level above your own", func(t *testing.T) { r := setup(t) wantRejected(t, r.check(change(r, bob, map[string]any{ "users": map[string]any{alice: 100, bob: 50, carol: 50}, "ban": 75, })), "ban would become 75") }) t.Run("you cannot lower a level that is above your own", func(t *testing.T) { r := setup(t) r.setPowerLevels(map[string]any{ "users": map[string]any{alice: 100, bob: 50, carol: 50}, "ban": 90, }) wantRejected(t, r.check(change(r, bob, map[string]any{ "users": map[string]any{alice: 100, bob: 50, carol: 50}, "ban": 50, })), "ban is 90") }) t.Run("the events map is protected the same way", func(t *testing.T) { r := setup(t) wantRejected(t, r.check(change(r, bob, map[string]any{ "users": map[string]any{alice: 100, bob: 50, carol: 50}, "events": map[string]any{"m.room.name": 75}, })), "events.m.room.name would become 75") wantAllowed(t, r.check(change(r, bob, map[string]any{ "users": map[string]any{alice: 100, bob: 50, carol: 50}, "events": map[string]any{"m.room.name": 50}, }))) }) t.Run("the notifications map is protected the same way", func(t *testing.T) { r := setup(t) wantRejected(t, r.check(change(r, bob, map[string]any{ "users": map[string]any{alice: 100, bob: 50, carol: 50}, "notifications": map[string]any{"room": 75}, })), "notifications.room would become 75") }) t.Run("an unchanged event is allowed", func(t *testing.T) { r := setup(t) wantAllowed(t, r.check(change(r, bob, map[string]any{ "users": map[string]any{alice: 100, bob: 50, carol: 50}, "events_default": 0, "state_default": 50, "ban": 50, "kick": 50, }))) }) } func TestPowerLevelContentValidation(t *testing.T) { t.Run("version 10 requires integers", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V10) wantRejected(t, r.check(event.Builder{ Type: event.TypePowerLevels, Sender: alice, StateKey: ptr(""), Content: mustJSON(t, map[string]any{"ban": "50"}), }), "is not an integer") }) t.Run("version 9 accepts a level written as a string", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V9) wantAllowed(t, r.check(event.Builder{ Type: event.TypePowerLevels, Sender: alice, StateKey: ptr(""), Content: mustJSON(t, map[string]any{"ban": "50"}), })) }) t.Run("version 9 still rejects a string that is not a number", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V9) wantRejected(t, r.check(event.Builder{ Type: event.TypePowerLevels, Sender: alice, StateKey: ptr(""), Content: mustJSON(t, map[string]any{"ban": "high"}), }), "not an integer") }) t.Run("a users key that is not a user ID is rejected", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) wantRejected(t, r.check(event.Builder{ Type: event.TypePowerLevels, Sender: alice, StateKey: ptr(""), Content: mustJSON(t, map[string]any{"users": map[string]any{"alice": 50}}), }), "users") }) t.Run("an events map with a non-integer value is rejected", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) wantRejected(t, r.check(event.Builder{ Type: event.TypePowerLevels, Sender: alice, StateKey: ptr(""), Content: mustJSON(t, map[string]any{"events": map[string]any{"m.room.name": true}}), }), "events") }) } // --- Room version 12: creators --- func TestVersion12CreatorsHaveInfinitePower(t *testing.T) { r := newRoom(t, roomversion.V12, map[string]any{"additional_creators": []string{bob}}) r.mustSend(r.member(alice, alice, "join", nil)) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(bob, bob, "join", nil)) r.mustSend(r.member(carol, carol, "join", nil)) // A power levels event that would strip everybody's power does not // touch the creators. r.setPowerLevels(map[string]any{ "users": map[string]any{carol: 100}, "state_default": 100, "events_default": 100, "kick": 100, "ban": 100, }) t.Run("a creator can still send state", func(t *testing.T) { wantAllowed(t, r.check(event.Builder{Type: "m.room.topic", Sender: alice, StateKey: ptr("")})) }) t.Run("an additional creator can too", func(t *testing.T) { wantAllowed(t, r.check(event.Builder{Type: "m.room.topic", Sender: bob, StateKey: ptr("")})) }) t.Run("a creator outranks a user with level 100", func(t *testing.T) { wantAllowed(t, r.check(r.member(alice, carol, "ban", nil))) }) t.Run("a user with level 100 cannot kick a creator", func(t *testing.T) { wantRejected(t, r.check(r.member(carol, bob, "leave", nil)), "does not exceed") }) t.Run("one creator cannot kick another", func(t *testing.T) { wantRejected(t, r.check(r.member(alice, bob, "leave", nil)), "does not exceed") }) t.Run("a creator cannot be given a level", func(t *testing.T) { wantRejected(t, r.check(event.Builder{ Type: event.TypePowerLevels, Sender: alice, StateKey: ptr(""), Content: mustJSON(t, map[string]any{"users": map[string]any{alice: 100}}), }), "must not give a level to the room creator") wantRejected(t, r.check(event.Builder{ Type: event.TypePowerLevels, Sender: alice, StateKey: ptr(""), Content: mustJSON(t, map[string]any{"users": map[string]any{bob: 50}}), }), "must not give a level to the room creator") }) } func TestVersion11CreatorIsOrdinaryOnceLevelsExist(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(carol, carol, "join", nil)) // The creator's 100 only applies while there is no power levels // event. Here Alice writes herself out of it. r.setPowerLevels(map[string]any{"users": map[string]any{carol: 100}, "state_default": 50}) wantRejected(t, r.check(event.Builder{Type: "m.room.topic", Sender: alice, StateKey: ptr("")}), "below the 50 needed") } // --- Third-party invites --- func TestThirdPartyInviteFlow(t *testing.T) { identityKey, err := signing.Generate("1") if err != nil { t.Fatal(err) } // The identity server signs a statement binding the token to a user. signedJSON, err := canonicaljson.Marshal(map[string]any{"mxid": bob, "token": "tok"}) if err != nil { t.Fatal(err) } signed, err := signing.SignJSON("identity.test", identityKey, signedJSON) if err != nil { t.Fatal(err) } setup := func(t *testing.T, publicKey string) *room { t.Helper() r := newJoinedRoom(t, roomversion.V11) r.mustSend(event.Builder{ Type: event.TypeThirdPartyInvite, Sender: alice, StateKey: ptr("tok"), Content: mustJSON(t, map[string]any{ "display_name": "bob@example.org", "key_validity_url": "https://identity.test/_matrix/identity/v2/pubkey/isvalid", "public_key": publicKey, }), }) return r } goodKey := base64.RawStdEncoding.EncodeToString(identityKey.Public) t.Run("a correctly signed invite is allowed", func(t *testing.T) { r := setup(t, goodKey) wantAllowed(t, r.check(r.member(alice, bob, "invite", map[string]any{ "third_party_invite": map[string]any{"display_name": "bob@example.org", "signed": json.RawMessage(signed)}, }))) }) t.Run("a signature from another key is rejected", func(t *testing.T) { other, err := signing.Generate("2") if err != nil { t.Fatal(err) } r := setup(t, base64.RawStdEncoding.EncodeToString(other.Public)) wantRejected(t, r.check(r.member(alice, bob, "invite", map[string]any{ "third_party_invite": map[string]any{"signed": json.RawMessage(signed)}, })), "no signature on the third-party invite matches") }) t.Run("public_keys is accepted as well as public_key", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.mustSend(event.Builder{ Type: event.TypeThirdPartyInvite, Sender: alice, StateKey: ptr("tok"), Content: mustJSON(t, map[string]any{ "display_name": "bob@example.org", "public_keys": []any{ map[string]any{"public_key": "AAAA"}, map[string]any{"public_key": goodKey}, }, }), }) wantAllowed(t, r.check(r.member(alice, bob, "invite", map[string]any{ "third_party_invite": map[string]any{"signed": json.RawMessage(signed)}, }))) }) t.Run("the mxid must match the invited user", func(t *testing.T) { r := setup(t, goodKey) wantRejected(t, r.check(r.member(alice, carol, "invite", map[string]any{ "third_party_invite": map[string]any{"signed": json.RawMessage(signed)}, })), "but the event invites") }) t.Run("there must be a matching invite event in state", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) wantRejected(t, r.check(r.member(alice, bob, "invite", map[string]any{ "third_party_invite": map[string]any{"signed": json.RawMessage(signed)}, })), "no m.room.third_party_invite in the room state") }) t.Run("the redeemer must be the issuer", func(t *testing.T) { r := setup(t, goodKey) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(carol, carol, "join", nil)) wantRejected(t, r.check(r.member(carol, bob, "invite", map[string]any{ "third_party_invite": map[string]any{"signed": json.RawMessage(signed)}, })), "was issued by") }) t.Run("signed needs both mxid and token", func(t *testing.T) { r := setup(t, goodKey) wantRejected(t, r.check(r.member(alice, bob, "invite", map[string]any{ "third_party_invite": map[string]any{"signed": map[string]any{"mxid": bob}}, })), "needs both mxid and token") }) t.Run("there must be a signed property", func(t *testing.T) { r := setup(t, goodKey) wantRejected(t, r.check(r.member(alice, bob, "invite", map[string]any{ "third_party_invite": map[string]any{"display_name": "x"}, })), "no signed property") }) t.Run("a banned user cannot be invited this way either", func(t *testing.T) { r := setup(t, goodKey) r.setPowerLevels(map[string]any{"users": map[string]any{alice: 100}}) r.mustSend(r.member(alice, bob, "ban", nil)) wantRejected(t, r.check(r.member(alice, bob, "invite", map[string]any{ "third_party_invite": map[string]any{"signed": json.RawMessage(signed)}, })), "banned") }) } // --- Redactions --- func TestRedactionsPassTheRulesButNeedASecondTest(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) r.setPowerLevels(map[string]any{ "users": map[string]any{alice: 100, bob: 0}, "redact": 50, "events_default": 0, }) r.setJoinRule(JoinRulePublic, nil) r.mustSend(r.member(bob, bob, "join", nil)) target := r.build(r.message(alice)) // Rule-wise a redaction from anybody in the room is fine: the redact // level is not an authorisation rule. redaction := event.Builder{ Type: event.TypeRedaction, Sender: bob, Redacts: target.EventID(), Content: mustJSON(t, map[string]any{"reason": "spam"}), } wantAllowed(t, r.check(redaction)) // Dave is in the room too, from another server, and also below the // redact level. r.mustSend(r.member(dave, dave, "join", nil)) pl, err := ResolvePowerLevels(r.state.PowerLevels(), r.state.Create(), r.rules) if err != nil { t.Fatal(err) } t.Run("whether it applies is a separate question", func(t *testing.T) { fromDave := r.build(event.Builder{ Type: event.TypeRedaction, Sender: dave, Redacts: target.EventID(), }) if ShouldApplyRedaction(fromDave, target, pl) { t.Error("Dave is below the redact level and is on another server, so his redaction should not apply") } }) t.Run("the redact level is enough", func(t *testing.T) { fromAlice := r.build(event.Builder{ Type: event.TypeRedaction, Sender: alice, Redacts: target.EventID(), }) if !ShouldApplyRedaction(fromAlice, target, pl) { t.Error("Alice is above the redact level, so her redaction should apply") } }) t.Run("the same server is enough", func(t *testing.T) { // Bob is on the same server as the event's sender, so his server // may take the event back whatever his power level. own := r.build(event.Builder{ Type: event.TypeRedaction, Sender: bob, Redacts: target.EventID(), }) if !ShouldApplyRedaction(own, target, pl) { t.Error("a redaction from the same server as the original should apply") } }) t.Run("another server without the level is not", func(t *testing.T) { remote := r.build(event.Builder{ Type: event.TypeRedaction, Sender: dave, Redacts: target.EventID(), }) if ShouldApplyRedaction(remote, target, pl) { t.Error("a redaction from another server below the redact level should not apply") } }) } // --- Power level resolution --- func TestResolvePowerLevels(t *testing.T) { t.Run("defaults with no power levels event", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) pl, err := ResolvePowerLevels(nil, r.state.Create(), r.rules) if err != nil { t.Fatal(err) } if got := pl.UserLevel(alice); got != CreatorPowerLevel { t.Errorf("the creator's level = %d, want %d", got, CreatorPowerLevel) } if got := pl.UserLevel(bob); got != 0 { t.Errorf("another user's level = %d, want 0", got) } // state_default is 50 even here, which is what stops a new member // rewriting the room's state. if got := pl.SendLevel("m.room.topic", true); got != 50 { t.Errorf("state_default = %d, want 50", got) } if got := pl.SendLevel("m.room.message", false); got != 0 { t.Errorf("events_default = %d, want 0", got) } for name, got := range map[string]int64{"ban": pl.Ban, "kick": pl.Kick, "redact": pl.Redact} { if got != 50 { t.Errorf("%s = %d, want 50", name, got) } } if pl.Invite != 0 { t.Errorf("invite = %d, want 0", pl.Invite) } if got := pl.NotificationLevel("room"); got != 50 { t.Errorf("notifications.room = %d, want 50", got) } }) t.Run("defaults fill in around what the event sets", func(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) plEvent := r.setPowerLevels(map[string]any{"users": map[string]any{alice: 42}}) pl, err := ResolvePowerLevels(plEvent, r.state.Create(), r.rules) if err != nil { t.Fatal(err) } if got := pl.UserLevel(alice); got != 42 { t.Errorf("alice = %d, want 42", got) } // The creator's 100 is gone: the event is authoritative. if got := pl.UserLevel(bob); got != 0 { t.Errorf("bob = %d, want 0", got) } if pl.Ban != 50 || pl.StateDefault != 50 || pl.Invite != 0 { t.Errorf("defaults were not filled in: ban=%d state_default=%d invite=%d", pl.Ban, pl.StateDefault, pl.Invite) } }) t.Run("version 12 creators are infinite", func(t *testing.T) { r := newRoom(t, roomversion.V12, map[string]any{"additional_creators": []string{bob}}) pl, err := ResolvePowerLevels(nil, r.state.Create(), r.rules) if err != nil { t.Fatal(err) } for _, creator := range []string{alice, bob} { if got := pl.UserLevel(creator); got != InfinitePowerLevel { t.Errorf("%s = %d, want InfinitePowerLevel", creator, got) } if !pl.IsCreator(creator) { t.Errorf("IsCreator(%s) = false", creator) } } if pl.IsCreator(carol) { t.Error("IsCreator(carol) = true") } }) } func TestCreators(t *testing.T) { t.Run("version 11 has one creator", func(t *testing.T) { r := newRoom(t, roomversion.V11, map[string]any{"additional_creators": []string{bob}}) creators, err := Creators(r.state.Create()) if err != nil { t.Fatal(err) } if len(creators) != 1 || creators[0] != alice { t.Errorf("creators = %v, want just %s", creators, alice) } }) t.Run("version 12 includes the additional creators", func(t *testing.T) { r := newRoom(t, roomversion.V12, map[string]any{"additional_creators": []string{bob, carol}}) creators, err := Creators(r.state.Create()) if err != nil { t.Fatal(err) } if fmt.Sprint(creators) != fmt.Sprint([]string{alice, bob, carol}) { t.Errorf("creators = %v", creators) } }) } func TestStateHelpers(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) if got := r.state.Membership(alice); got != event.MembershipJoin { t.Errorf("Membership(alice) = %q, want join", got) } // A user with no member event is treated exactly like one who left. if got := r.state.Membership(dave); got != event.MembershipLeave { t.Errorf("Membership(dave) = %q, want leave", got) } if got := r.state.JoinRule(); got != JoinRuleInvite { t.Errorf("JoinRule() = %q, want invite by default", got) } r.setJoinRule(JoinRulePublic, nil) if got := r.state.JoinRule(); got != JoinRulePublic { t.Errorf("JoinRule() = %q, want public", got) } if r.state.PowerLevels() != nil { t.Error("PowerLevels() should be nil in a room with no power levels event") } if r.state.Create() == nil { t.Error("Create() should not be nil") } } func TestCheckWithoutACreateEvent(t *testing.T) { r := newJoinedRoom(t, roomversion.V11) e := r.build(r.message(alice)) wantRejected(t, Check(context.Background(), e, State{}, nil), "no m.room.create event") }