aboutsummaryrefslogtreecommitdiff
path: root/federationapi
diff options
context:
space:
mode:
authorNeil Alexander <neilalexander@users.noreply.github.com>2022-05-25 10:05:30 +0100
committerGitHub <noreply@github.com>2022-05-25 10:05:30 +0100
commit81843e8836e6f8f334c7b5fd1433c427c10a9443 (patch)
treeda86bc862d46103b955431961df95fab90c199b6 /federationapi
parentd621dd2986fa0b8cce9d164a7249456d0be47c81 (diff)
Restricted join support on `/make_join`, `/send_join` (#2478)
* Add `QueryRestrictedJoinAllowed` * Add `Resident` flag to `QueryRestrictedJoinAllowedResponse` * Check restricted joins on federation API * Return `Restricted` to determine if the room was restricted or not * Populate `AuthorisedVia` properly * Sign the event on `/send_join`, return it in the `/send_join` response in the `"event"` key * Kick back joins with invalid authorising user IDs, use event from `"event"` key if returned in `RespSendJoin` * Use invite helper in `QueryRestrictedJoinAllowed` * Only use users with the power to invite, change error bubbling a bit * Placate the almighty linter One day I will nuke `gocyclo` from orbit and everything in the world will be much better for it. * Review comments
Diffstat (limited to 'federationapi')
-rw-r--r--federationapi/internal/perform.go30
-rw-r--r--federationapi/routing/join.go122
2 files changed, 148 insertions, 4 deletions
diff --git a/federationapi/internal/perform.go b/federationapi/internal/perform.go
index c176a6ea..970a6465 100644
--- a/federationapi/internal/perform.go
+++ b/federationapi/internal/perform.go
@@ -210,10 +210,18 @@ func (r *FederationInternalAPI) performJoinUsingServer(
}
r.statistics.ForServer(serverName).Success()
- authEvents := respSendJoin.AuthEvents.UntrustedEvents(respMakeJoin.RoomVersion)
+ // If the remote server returned an event in the "event" key of
+ // the send_join request then we should use that instead. It may
+ // contain signatures that we don't know about.
+ if respSendJoin.Event != nil && isWellFormedMembershipEvent(
+ respSendJoin.Event, roomID, userID, r.cfg.Matrix.ServerName,
+ ) {
+ event = respSendJoin.Event
+ }
// Sanity-check the join response to ensure that it has a create
// event, that the room version is known, etc.
+ authEvents := respSendJoin.AuthEvents.UntrustedEvents(respMakeJoin.RoomVersion)
if err = sanityCheckAuthChain(authEvents); err != nil {
return fmt.Errorf("sanityCheckAuthChain: %w", err)
}
@@ -271,6 +279,26 @@ func (r *FederationInternalAPI) performJoinUsingServer(
return nil
}
+// isWellFormedMembershipEvent returns true if the event looks like a legitimate
+// membership event.
+func isWellFormedMembershipEvent(event *gomatrixserverlib.Event, roomID, userID string, origin gomatrixserverlib.ServerName) bool {
+ if membership, err := event.Membership(); err != nil {
+ return false
+ } else if membership != gomatrixserverlib.Join {
+ return false
+ }
+ if event.RoomID() != roomID {
+ return false
+ }
+ if event.Origin() != origin {
+ return false
+ }
+ if !event.StateKeyEquals(userID) {
+ return false
+ }
+ return true
+}
+
// PerformOutboundPeekRequest implements api.FederationInternalAPI
func (r *FederationInternalAPI) PerformOutboundPeek(
ctx context.Context,
diff --git a/federationapi/routing/join.go b/federationapi/routing/join.go
index 76769972..9f8d9410 100644
--- a/federationapi/routing/join.go
+++ b/federationapi/routing/join.go
@@ -15,6 +15,7 @@
package routing
import (
+ "encoding/json"
"fmt"
"net/http"
"sort"
@@ -103,6 +104,16 @@ func MakeJoin(
}
}
+ // Check if the restricted join is allowed. If the room doesn't
+ // support restricted joins then this is effectively a no-op.
+ res, authorisedVia, err := checkRestrictedJoin(httpReq, rsAPI, verRes.RoomVersion, roomID, userID)
+ if err != nil {
+ util.GetLogger(httpReq.Context()).WithError(err).Error("checkRestrictedJoin failed")
+ return jsonerror.InternalServerError()
+ } else if res != nil {
+ return *res
+ }
+
// Try building an event for the server
builder := gomatrixserverlib.EventBuilder{
Sender: userID,
@@ -110,8 +121,11 @@ func MakeJoin(
Type: "m.room.member",
StateKey: &userID,
}
- err = builder.SetContent(map[string]interface{}{"membership": gomatrixserverlib.Join})
- if err != nil {
+ content := gomatrixserverlib.MemberContent{
+ Membership: gomatrixserverlib.Join,
+ AuthorisedVia: authorisedVia,
+ }
+ if err = builder.SetContent(content); err != nil {
util.GetLogger(httpReq.Context()).WithError(err).Error("builder.SetContent failed")
return jsonerror.InternalServerError()
}
@@ -161,6 +175,7 @@ func MakeJoin(
// SendJoin implements the /send_join API
// The make-join send-join dance makes much more sense as a single
// flow so the cyclomatic complexity is high:
+// nolint:gocyclo
func SendJoin(
httpReq *http.Request,
request *gomatrixserverlib.FederationRequest,
@@ -314,6 +329,40 @@ func SendJoin(
}
}
+ // If the membership content contains a user ID for a server that is not
+ // ours then we should kick it back.
+ var memberContent gomatrixserverlib.MemberContent
+ if err := json.Unmarshal(event.Content(), &memberContent); err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON(err.Error()),
+ }
+ }
+ if memberContent.AuthorisedVia != "" {
+ _, domain, err := gomatrixserverlib.SplitID('@', memberContent.AuthorisedVia)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON(fmt.Sprintf("The authorising username %q is invalid.", memberContent.AuthorisedVia)),
+ }
+ }
+ if domain != cfg.Matrix.ServerName {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON(fmt.Sprintf("The authorising username %q does not belong to this server.", memberContent.AuthorisedVia)),
+ }
+ }
+ }
+
+ // Sign the membership event. This is required for restricted joins to work
+ // in the case that the authorised via user is one of our own users. It also
+ // doesn't hurt to do it even if it isn't a restricted join.
+ signed := event.Sign(
+ string(cfg.Matrix.ServerName),
+ cfg.Matrix.KeyID,
+ cfg.Matrix.PrivateKey,
+ )
+
// Send the events to the room server.
// We are responsible for notifying other servers that the user has joined
// the room, so set SendAsServer to cfg.Matrix.ServerName
@@ -323,7 +372,7 @@ func SendJoin(
InputRoomEvents: []api.InputRoomEvent{
{
Kind: api.KindNew,
- Event: event.Headered(stateAndAuthChainResponse.RoomVersion),
+ Event: signed.Headered(stateAndAuthChainResponse.RoomVersion),
SendAsServer: string(cfg.Matrix.ServerName),
TransactionID: nil,
},
@@ -354,10 +403,77 @@ func SendJoin(
StateEvents: gomatrixserverlib.NewEventJSONsFromHeaderedEvents(stateAndAuthChainResponse.StateEvents),
AuthEvents: gomatrixserverlib.NewEventJSONsFromHeaderedEvents(stateAndAuthChainResponse.AuthChainEvents),
Origin: cfg.Matrix.ServerName,
+ Event: &signed,
},
}
}
+// checkRestrictedJoin finds out whether or not we can assist in processing
+// a restricted room join. If the room version does not support restricted
+// joins then this function returns with no side effects. This returns three
+// values:
+// * an optional JSON response body (i.e. M_UNABLE_TO_AUTHORISE_JOIN) which
+// should always be sent back to the client if one is specified
+// * a user ID of an authorising user, typically a user that has power to
+// issue invites in the room, if one has been found
+// * an error if there was a problem finding out if this was allowable,
+// like if the room version isn't known or a problem happened talking to
+// the roomserver
+func checkRestrictedJoin(
+ httpReq *http.Request,
+ rsAPI api.FederationRoomserverAPI,
+ roomVersion gomatrixserverlib.RoomVersion,
+ roomID, userID string,
+) (*util.JSONResponse, string, error) {
+ if allowRestricted, err := roomVersion.AllowRestrictedJoinsInEventAuth(); err != nil {
+ return nil, "", err
+ } else if !allowRestricted {
+ return nil, "", nil
+ }
+ req := &api.QueryRestrictedJoinAllowedRequest{
+ RoomID: roomID,
+ UserID: userID,
+ }
+ res := &api.QueryRestrictedJoinAllowedResponse{}
+ if err := rsAPI.QueryRestrictedJoinAllowed(httpReq.Context(), req, res); err != nil {
+ return nil, "", err
+ }
+
+ switch {
+ case !res.Restricted:
+ // The join rules for the room don't restrict membership.
+ return nil, "", nil
+
+ case !res.Resident:
+ // The join rules restrict membership but our server isn't currently
+ // joined to all of the allowed rooms, so we can't actually decide
+ // whether or not to allow the user to join. This error code should
+ // tell the joining server to try joining via another resident server
+ // instead.
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.UnableToAuthoriseJoin("This server cannot authorise the join."),
+ }, "", nil
+
+ case !res.Allowed:
+ // The join rules restrict membership, our server is in the relevant
+ // rooms and the user wasn't joined to join any of the allowed rooms
+ // and therefore can't join this room.
+ return &util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("You are not joined to any matching rooms."),
+ }, "", nil
+
+ default:
+ // The join rules restrict membership, our server is in the relevant
+ // rooms and the user was allowed to join because they belong to one
+ // of the allowed rooms. We now need to pick one of our own local users
+ // from within the room to use as the authorising user ID, so that it
+ // can be referred to from within the membership content.
+ return nil, res.AuthorisedVia, nil
+ }
+}
+
type eventsByDepth []*gomatrixserverlib.HeaderedEvent
func (e eventsByDepth) Len() int {