Processing of pending invites on 3PID binding (#218)

* Add missing file headers

* Move the ID server's signatures verification to common

* Allow verification without specifying a server name

* Add third-party structs to membership events content

* Add processing of 3PID onbind requests

* Use reference for third party invite data

* Fix return arguments order

* Revert "Move the ID server's signatures verification to common"

This reverts commit 93442010316ce71a77ac58ffd3613754ce8fe969.

* Revert "Allow verification without specifying a server name"

This reverts commit fd27afbf82eac50fe9f7b83b26cfce3c66d530d2.

* Remove checks that are already occurring in gomatrixserverlib

* Change return type of createInviteFrom3PIDInvite

* Add doc, add checks in fillDisplayName

* Use MakeFedAPI

* Invert condition

* Use AuthEvents to retrieve the 3PID invite

* Update comment

* Remove unused parameter

* gb vendor update github.com/matrix-org/gomatrixserverlib
main
Brendan Abolivier 2017-09-08 15:17:12 +01:00 committed by Mark Haines
parent fad997303b
commit 4d1d503d43
13 changed files with 532 additions and 23 deletions

View File

@ -22,11 +22,24 @@ type CreateContent struct {
// MemberContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-member
type MemberContent struct {
Membership string `json:"membership"`
DisplayName string `json:"displayname,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
Reason string `json:"reason,omitempty"`
// TODO: ThirdPartyInvite string `json:"third_party_invite,omitempty"`
Membership string `json:"membership"`
DisplayName string `json:"displayname,omitempty"`
AvatarURL string `json:"avatar_url,omitempty"`
Reason string `json:"reason,omitempty"`
ThirdPartyInvite *TPInvite `json:"third_party_invite,omitempty"`
}
// TPInvite is the "Invite" structure defined at http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-member
type TPInvite struct {
DisplayName string `json:"display_name"`
Signed TPInviteSigned `json:"signed"`
}
// TPInviteSigned is the "signed" structure defined at http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-member
type TPInviteSigned struct {
MXID string `json:"mxid"`
Signatures map[string]map[string]string `json:"signatures"`
Token string `json:"token"`
}
// ThirdPartyInviteContent is the content event for https://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-third-party-invite

View File

@ -79,6 +79,13 @@ func Setup(
},
))
v1fedmux.Handle("/3pid/onbind", common.MakeFedAPI(
"3pid_onbind", cfg.Matrix.ServerName, keys,
func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest) util.JSONResponse {
return writers.CreateInvitesFrom3PIDInvites(httpReq, query, cfg, producer)
},
))
v1fedmux.Handle("/event/{eventID}", common.MakeFedAPI(
"federation_get_event", cfg.Matrix.ServerName, keys,
func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest) util.JSONResponse {

View File

@ -1,3 +1,17 @@
// Copyright 2017 Vector Creations Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package writers
import (

View File

@ -1,3 +1,17 @@
// Copyright 2017 Vector Creations Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package writers
import (

View File

@ -0,0 +1,192 @@
// Copyright 2017 Vector Creations Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package writers
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/producers"
"github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
)
type invite struct {
MXID string `json:"mxid"`
RoomID string `json:"room_id"`
Sender string `json:"sender"`
Token string `json:"token"`
Signed common.TPInviteSigned `json:"signed"`
}
type invites struct {
Medium string `json:"medium"`
Address string `json:"address"`
MXID string `json:"mxid"`
Invites []invite `json:"invites"`
}
// CreateInvitesFrom3PIDInvites implements POST /_matrix/federation/v1/3pid/onbind
func CreateInvitesFrom3PIDInvites(
req *http.Request, queryAPI api.RoomserverQueryAPI, cfg config.Dendrite,
producer *producers.RoomserverProducer,
) util.JSONResponse {
var body invites
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
return *reqErr
}
evs := []gomatrixserverlib.Event{}
for _, inv := range body.Invites {
event, err := createInviteFrom3PIDInvite(queryAPI, cfg, inv)
if err != nil {
return httputil.LogThenError(req, err)
}
if event != nil {
evs = append(evs, *event)
}
}
// Send all the events
if err := producer.SendEvents(evs, cfg.Matrix.ServerName); err != nil {
return httputil.LogThenError(req, err)
}
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}
// createInviteFrom3PIDInvite processes an invite provided by the identity server
// and creates a m.room.member event (with "invite" membership) from it.
// Returns an error if there was a problem building the event or fetching the
// necessary data to do so.
func createInviteFrom3PIDInvite(
queryAPI api.RoomserverQueryAPI, cfg config.Dendrite, inv invite,
) (*gomatrixserverlib.Event, error) {
// Build the event
builder := &gomatrixserverlib.EventBuilder{
Type: "m.room.member",
Sender: inv.Sender,
RoomID: inv.RoomID,
StateKey: &inv.MXID,
}
content := common.MemberContent{
// TODO: Load the profile
Membership: "invite",
ThirdPartyInvite: &common.TPInvite{
Signed: inv.Signed,
},
}
if err := builder.SetContent(content); err != nil {
return nil, err
}
eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder)
if err != nil {
return nil, err
}
// Ask the roomserver for information about this room
queryReq := api.QueryLatestEventsAndStateRequest{
RoomID: builder.RoomID,
StateToFetch: eventsNeeded.Tuples(),
}
var queryRes api.QueryLatestEventsAndStateResponse
if err = queryAPI.QueryLatestEventsAndState(&queryReq, &queryRes); err != nil {
return nil, err
}
if !queryRes.RoomExists {
// TODO: Use federation to auth the event
return nil, nil
}
// Finish building the event
builder.Depth = queryRes.Depth
builder.PrevEvents = queryRes.LatestEvents
authEvents := gomatrixserverlib.NewAuthEvents(nil)
for i := range queryRes.StateEvents {
authEvents.AddEvent(&queryRes.StateEvents[i])
}
if err = fillDisplayName(builder, content, authEvents); err != nil {
return nil, err
}
refs, err := eventsNeeded.AuthEventReferences(&authEvents)
if err != nil {
return nil, err
}
builder.AuthEvents = refs
eventID := fmt.Sprintf("$%s:%s", util.RandomString(16), cfg.Matrix.ServerName)
now := time.Now()
event, err := builder.Build(eventID, now, cfg.Matrix.ServerName, cfg.Matrix.KeyID, cfg.Matrix.PrivateKey)
if err != nil {
return nil, err
}
return &event, nil
}
// fillDisplayName looks in a list of auth events for a m.room.third_party_invite
// event with the state key matching a given m.room.member event's content's token.
// If such an event is found, fills the "display_name" attribute of the
// "third_party_invite" structure in the m.room.member event with the display_name
// from the m.room.third_party_invite event.
// Returns an error if there was a problem parsing the m.room.third_party_invite
// event's content or updating the m.room.member event's content.
// Returns nil if no m.room.third_party_invite with a matching token could be
// found. Returning an error isn't necessary in this case as the event will be
// rejected by gomatrixserverlib.
func fillDisplayName(
builder *gomatrixserverlib.EventBuilder, content common.MemberContent,
authEvents gomatrixserverlib.AuthEvents,
) error {
// Look for the m.room.third_party_invite event
thirdPartyInviteEvent, _ := authEvents.ThirdPartyInvite(content.ThirdPartyInvite.Signed.Token)
if thirdPartyInviteEvent == nil {
// If the third party invite event doesn't exist then we can't use it to set the display name.
return nil
}
var thirdPartyInviteContent common.ThirdPartyInviteContent
if err := json.Unmarshal(thirdPartyInviteEvent.Content(), &thirdPartyInviteContent); err != nil {
return err
}
// Use the m.room.third_party_invite event to fill the "displayname" and
// update the m.room.member event's content with it
content.ThirdPartyInvite.DisplayName = thirdPartyInviteContent.DisplayName
if err := builder.SetContent(content); err != nil {
return err
}
return nil
}

2
vendor/manifest vendored
View File

@ -116,7 +116,7 @@
{
"importpath": "github.com/matrix-org/gomatrixserverlib",
"repository": "https://github.com/matrix-org/gomatrixserverlib",
"revision": "768a8767051a4aca7f5e41f912954ae04d5f1efb",
"revision": "2e9caead882bcdeb999cf4677cfff47e39d3271e",
"branch": "master"
},
{

View File

@ -18,6 +18,7 @@ package gomatrixserverlib
import (
"encoding/base64"
"encoding/json"
"strings"
)
// A Base64String is a string of bytes that are base64 encoded when used in JSON.
@ -43,6 +44,12 @@ func (b64 *Base64String) UnmarshalJSON(raw []byte) (err error) {
if err = json.Unmarshal(raw, &str); err != nil {
return
}
*b64, err = base64.RawStdEncoding.DecodeString(str)
// We must check whether the string was encoded in a URL-safe way in order
// to use the appropriate encoding.
if strings.ContainsAny(str, "-_") {
*b64, err = base64.RawURLEncoding.DecodeString(str)
} else {
*b64, err = base64.RawStdEncoding.DecodeString(str)
}
return
}

View File

@ -45,6 +45,19 @@ func TestUnmarshalBase64(t *testing.T) {
}
}
func TestUnmarshalUrlSafeBase64(t *testing.T) {
input := []byte(`"dGhpc_9pc_9h_3Rlc3Q"`)
want := "this\xffis\xffa\xfftest"
var got Base64String
err := json.Unmarshal(input, &got)
if err != nil {
t.Fatal(err)
}
if string(got) != want {
t.Fatalf("json.Unmarshal(%q): wanted %q got %q", string(input), want, string(got))
}
}
func TestMarshalBase64Struct(t *testing.T) {
input := struct{ Value Base64String }{Base64String("this\xffis\xffa\xfftest")}
want := `{"Value":"dGhpc/9pc/9h/3Rlc3Q"}`

View File

@ -422,7 +422,7 @@ func (e Event) CheckFields() error {
if e.fields.Type != MRoomMember {
return fmt.Errorf(
"gomatrixserverlib: sender domain doesn't match origin: %q != %q",
eventDomain, e.fields.Origin,
senderDomain, e.fields.Origin,
)
}
c, err := newMemberContentFromEvent(e)
@ -432,7 +432,7 @@ func (e Event) CheckFields() error {
if c.Membership != invite || c.ThirdPartyInvite == nil {
return fmt.Errorf(
"gomatrixserverlib: sender domain doesn't match origin: %q != %q",
eventDomain, e.fields.Origin,
senderDomain, e.fields.Origin,
)
}
}

View File

@ -0,0 +1,27 @@
/* Copyright 2016-2017 Vector Creations Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package gomatrixserverlib
import "fmt"
func ExampleSplitID() {
localpart, domain, err := SplitID('@', "@alice:localhost:8080")
if err != nil {
panic(err)
}
fmt.Println(localpart, domain)
// Output: alice localhost:8080
}

View File

@ -18,6 +18,9 @@ package gomatrixserverlib
import (
"encoding/json"
"fmt"
"strings"
"golang.org/x/crypto/ed25519"
"github.com/matrix-org/util"
)
@ -224,15 +227,7 @@ func accumulateStateNeeded(result *StateNeeded, eventType, sender string, stateK
}
// thirdPartyInviteToken extracts the token from the third_party_invite.
func thirdPartyInviteToken(thirdPartyInviteData rawJSON) (string, error) {
var thirdPartyInvite struct {
Signed struct {
Token string `json:"token"`
} `json:"signed"`
}
if err := json.Unmarshal(thirdPartyInviteData, &thirdPartyInvite); err != nil {
return "", err
}
func thirdPartyInviteToken(thirdPartyInvite *memberThirdPartyInvite) (string, error) {
if thirdPartyInvite.Signed.Token == "" {
return "", fmt.Errorf("missing 'third_party_invite.signed.token' JSON key")
}
@ -774,6 +769,8 @@ type membershipAllower struct {
powerLevels powerLevelContent
// The m.room.join_rules content for the room.
joinRule joinRuleContent
// The m.room.third_party_invite content referenced by this event.
thirdPartyInvite thirdPartyInviteContent
}
// newMembershipAllower loads the information needed to authenticate the m.room.member event
@ -808,6 +805,13 @@ func newMembershipAllower(authEvents AuthEventProvider, event Event) (m membersh
return
}
}
// If this event comes from a third_party_invite, we need to check it against the original event.
if m.newMember.ThirdPartyInvite != nil {
token := m.newMember.ThirdPartyInvite.Signed.Token
if m.thirdPartyInvite, err = newThirdPartyInviteContentFromAuthEvents(authEvents, token); err != nil {
return
}
}
return
}
@ -840,10 +844,10 @@ func (m *membershipAllower) membershipAllowed(event Event) error {
// Otherwise fall back to the normal checks.
}
if m.newMember.Membership == invite && len(m.newMember.ThirdPartyInvite) != 0 {
if m.newMember.Membership == invite && m.newMember.ThirdPartyInvite != nil {
// Special case third party invites
// https://github.com/matrix-org/synapse/blob/v0.18.5/synapse/api/auth.py#L393
panic(fmt.Errorf("ThirdPartyInvite not implemented"))
return m.membershipAllowedFromThirdPartyInvite()
}
if m.targetID == m.senderID {
@ -855,6 +859,42 @@ func (m *membershipAllower) membershipAllowed(event Event) error {
return m.membershipAllowedOther()
}
// membershipAllowedFronThirdPartyInvite determines if the member events is following
// up the third_party_invite event it claims.
func (m *membershipAllower) membershipAllowedFromThirdPartyInvite() error {
// Check if the event's target matches with the Matrix ID provided by the
// identity server.
if m.targetID != m.newMember.ThirdPartyInvite.Signed.MXID {
return errorf(
"The invite target %s doesn't match with the Matrix ID provided by the identity server %s",
m.targetID, m.newMember.ThirdPartyInvite.Signed.MXID,
)
}
// Marshal the "signed" so it can be verified by VerifyJSON.
marshalledSigned, err := json.Marshal(m.newMember.ThirdPartyInvite.Signed)
if err != nil {
return err
}
// Check each signature with each public key. If one signature could be
// verified with one public key, accept the event.
for _, publicKey := range m.thirdPartyInvite.PublicKeys {
for domain, signatures := range m.newMember.ThirdPartyInvite.Signed.Signatures {
for keyID := range signatures {
if strings.HasPrefix(keyID, "ed25519") {
if err = VerifyJSON(
domain, KeyID(keyID),
ed25519.PublicKey(publicKey.PublicKey),
marshalledSigned,
); err == nil {
return nil
}
}
}
}
}
return errorf("Couldn't verify signature on third-party invite for %s", m.targetID)
}
// membershipAllowedSelf determines if the change made by the user to their own membership is allowed.
func (m *membershipAllower) membershipAllowedSelf() error {
if m.newMember.Membership == join {

View File

@ -169,7 +169,12 @@ func TestStateNeededForInvite3PID(t *testing.T) {
StateKey: &skey,
Sender: "@u1:a",
}
b.SetContent(memberContent{"invite", rawJSON(`{"signed":{"token":"my_token"}}`)})
b.SetContent(memberContent{"invite", &memberThirdPartyInvite{
Signed: memberThirdPartyInviteSigned{
Token: "my_token",
},
}})
testStateNeededForAuth(t, `[{
"type": "m.room.member",
"state_key": "@u2:b",
@ -280,7 +285,7 @@ func testEventAllowed(t *testing.T, testCaseJSON string) {
panic(err)
}
if err := Allowed(event, &tc.AuthEvents); err == nil {
t.Fatalf("Expected %q to not be allowed but it was: %q", string(data), err)
t.Fatalf("Expected %q to not be allowed but it was", string(data))
}
}
}
@ -532,6 +537,146 @@ func TestAllowedWithNoPowerLevels(t *testing.T) {
}`)
}
func TestAllowedInviteFrom3PID(t *testing.T) {
testEventAllowed(t, `{
"auth_events": {
"create": {
"type": "m.room.create",
"state_key": "",
"sender": "@u1:a",
"room_id": "!r1:a",
"event_id": "$e1:a",
"content": {"creator": "@u1:a"}
},
"member": {
"@u1:a": {
"type": "m.room.member",
"sender": "@u1:a",
"room_id": "!r1:a",
"state_key": "@u1:a",
"event_id": "$e2:a",
"content": {"membership": "join"}
}
},
"third_party_invite": {
"my_token": {
"type": "m.room.third_party_invite",
"sender": "@u1:a",
"room_id": "!r1:a",
"state_key": "my_token",
"event_id": "$e3:a",
"content": {
"display_name": "foo...@bar...",
"public_key": "pubkey",
"key_validity_url": "https://example.tld/isvalid",
"public_keys": [
{
"public_key": "mrV51jApZKahGjfMhlevp+QtSSTDKCLaLVCzYc4HELY",
"key_validity_url": "https://example.tld/isvalid"
}
]
}
}
}
},
"allowed": [{
"type": "m.room.member",
"sender": "@u1:a",
"room_id": "!r1:a",
"state_key": "@u2:a",
"event_id": "$e4:a",
"content": {
"membership": "invite",
"third_party_invite": {
"display_name": "foo...@bar...",
"signed": {
"token": "my_token",
"mxid": "@u2:a",
"signatures": {
"example.tld": {
"ed25519:0": "CibGFS0vX93quJFppsQbYQKJFIwxiYEK87lNmekS/fdetUMXPdR2wwNDd09J1jJ28GCH3GogUTuFDB1ScPFxBg"
}
}
}
}
}
}],
"not_allowed": [{
"type": "m.room.member",
"sender": "@u1:a",
"room_id": "!r1:a",
"state_key": "@u2:a",
"event_id": "$e4:a",
"content": {
"membership": "invite",
"third_party_invite": {
"display_name": "foo...@bar...",
"signed": {
"token": "my_token",
"mxid": "@u2:a",
"signatures": {
"example.tld": {
"ed25519:0": "some_signature"
}
}
}
}
},
"unsigned": {
"not_allowed": "Bad signature"
}
}, {
"type": "m.room.member",
"sender": "@u1:a",
"room_id": "!r1:a",
"state_key": "@u2:a",
"event_id": "$e5:a",
"content": {
"membership": "invite",
"third_party_invite": {
"display_name": "foo...@bar...",
"signed": {
"token": "my_token",
"mxid": "@u3:a",
"signatures": {
"example.tld": {
"ed25519:0": "CibGFS0vX93quJFppsQbYQKJFIwxiYEK87lNmekS/fdetUMXPdR2wwNDd09J1jJ28GCH3GogUTuFDB1ScPFxBg"
}
}
}
}
},
"unsigned": {
"not_allowed": "MXID doesn't match state key"
}
}, {
"type": "m.room.member",
"sender": "@u1:a",
"room_id": "!r1:a",
"state_key": "@u2:a",
"event_id": "$e6:a",
"content": {
"membership": "invite",
"third_party_invite": {
"display_name": "foo...@bar...",
"signed": {
"token": "my_other_token",
"mxid": "@u2:a",
"signatures": {
"example.tld": {
"ed25519:0": "CibGFS0vX93quJFppsQbYQKJFIwxiYEK87lNmekS/fdetUMXPdR2wwNDd09J1jJ28GCH3GogUTuFDB1ScPFxBg"
}
}
}
}
},
"unsigned": {
"not_allowed": "Token doesn't refer to a known third-party invite"
}
}]
}`)
}
func TestAllowedNoFederation(t *testing.T) {
testEventAllowed(t, `{
"auth_events": {

View File

@ -108,7 +108,18 @@ type memberContent struct {
// We use the membership key in order to check if the user is in the room.
Membership string `json:"membership"`
// We use the third_party_invite key to special case thirdparty invites.
ThirdPartyInvite rawJSON `json:"third_party_invite,omitempty"`
ThirdPartyInvite *memberThirdPartyInvite `json:"third_party_invite,omitempty"`
}
type memberThirdPartyInvite struct {
DisplayName string `json:"display_name"`
Signed memberThirdPartyInviteSigned `json:"signed"`
}
type memberThirdPartyInviteSigned struct {
MXID string `json:"mxid"`
Signatures map[string]map[string]string `json:"signatures"`
Token string `json:"token"`
}
// newMemberContentFromAuthEvents loads the member content from the member event for the user ID in the auth events.
@ -137,6 +148,32 @@ func newMemberContentFromEvent(event Event) (c memberContent, err error) {
return
}
// thirdPartyInviteContent is the JSON content of a m.room.third_party_invite event needed for auth checks.
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-third-party-invite for descriptions of the fields.
type thirdPartyInviteContent struct {
// Public keys are used to verify the signature of a m.room.member event that
// came from a m.room.third_party_invite event
PublicKeys []struct {
PublicKey Base64String `json:"public_key"`
} `json:"public_keys"`
}
func newThirdPartyInviteContentFromAuthEvents(authEvents AuthEventProvider, token string) (t thirdPartyInviteContent, err error) {
var thirdPartyInviteEvent *Event
if thirdPartyInviteEvent, err = authEvents.ThirdPartyInvite(token); err != nil {
return
}
if thirdPartyInviteEvent == nil {
// If there isn't a third_party_invite event, then we return with an error
err = errorf("Couldn't find third party invite event")
return
}
if err = json.Unmarshal(thirdPartyInviteEvent.Content(), &t); err != nil {
err = errorf("unparsable third party invite event content: %s", err.Error())
}
return
}
// joinRuleContent is the JSON content of a m.room.join_rules event needed for auth checks.
// See https://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-join-rules for descriptions of the fields.
type joinRuleContent struct {