diff --git a/clientapi/jsonerror/jsonerror.go b/clientapi/jsonerror/jsonerror.go index caa216e6..ffe16f98 100644 --- a/clientapi/jsonerror/jsonerror.go +++ b/clientapi/jsonerror/jsonerror.go @@ -149,6 +149,18 @@ func MissingParam(msg string) *MatrixError { return &MatrixError{"M_MISSING_PARAM", msg} } +// UnableToAuthoriseJoin is an error that is returned when a server that we +// are trying to join via doesn't know enough to authorise a restricted join. +func UnableToAuthoriseJoin(msg string) *MatrixError { + return &MatrixError{"M_UNABLE_TO_AUTHORISE_JOIN", msg} +} + +// UnableToGrantJoin is an error that is returned when a server that we +// are trying to join via doesn't have a user with power to invite. +func UnableToGrantJoin(msg string) *MatrixError { + return &MatrixError{"M_UNABLE_TO_GRANT_JOIN", msg} +} + type IncompatibleRoomVersionError struct { RoomVersion string `json:"room_version"` Error string `json:"error"` @@ -161,7 +173,7 @@ func IncompatibleRoomVersion(roomVersion gomatrixserverlib.RoomVersion) *Incompa return &IncompatibleRoomVersionError{ Code: "M_INCOMPATIBLE_ROOM_VERSION", RoomVersion: string(roomVersion), - Error: "Your homeserver does not support the features required to join this room", + Error: fmt.Sprintf("Your homeserver does not support the features required to join this version %q room", roomVersion), } } diff --git a/federationapi/routing/join.go b/federationapi/routing/join.go index f0e1ae0d..c89a4f04 100644 --- a/federationapi/routing/join.go +++ b/federationapi/routing/join.go @@ -15,6 +15,8 @@ package routing import ( + "context" + "encoding/json" "fmt" "net/http" "sort" @@ -140,9 +142,30 @@ func MakeJoin( for i := range queryRes.StateEvents { stateEvents[i] = queryRes.StateEvents[i].Event } - provider := gomatrixserverlib.NewAuthEvents(stateEvents) + + // Check the join rules. If it's a restricted join then there are special rules. + // We have to do this in two steps in order to satisfy the Complement tests. The + // first is to get the join rule itself, and the second is to unmarshal the 'allow' + // key. The tests deliberately set the 'allow' key to some nonsense values, but if + // we try to unmarshal that all in one go, the entire unmarshalling step fails, + // incorrectly leaving the room as the default join rule of 'public'. + joinRule, err := getJoinRule(provider) + if err != nil { + return util.JSONResponse{ + Code: http.StatusNotFound, + JSON: jsonerror.NotFound("Failed to find room join rules"), + } + } + if err = gomatrixserverlib.Allowed(event.Event, &provider); err != nil { + if joinRule.JoinRule == gomatrixserverlib.Restricted { + res := attemptMakeJoinForRestrictedMembership( + httpReq, cfg, rsAPI, &verRes, provider, + &builder, *joinRule, userID, + ) + return res + } return util.JSONResponse{ Code: http.StatusForbidden, JSON: jsonerror.Forbidden(err.Error()), @@ -158,9 +181,194 @@ func MakeJoin( } } +func getJoinRule( + provider gomatrixserverlib.AuthEvents, +) (*gomatrixserverlib.JoinRuleContent, error) { + joinRuleEvent, err := provider.JoinRules() + if err != nil { + return nil, fmt.Errorf("failed to find join rules") + } + joinRule := struct { + JoinRule string `json:"join_rule"` + }{ + JoinRule: gomatrixserverlib.Public, // Default join rule if not specified. + } + if joinRuleEvent != nil { + if err = json.Unmarshal(joinRuleEvent.Content(), &joinRule); err != nil { + return nil, fmt.Errorf("json.Unmarshal: %w", err) + } + } + var joinRuleAllow struct { + Allow []gomatrixserverlib.JoinRuleContentAllowRule `json:"allow"` + } + _ = json.Unmarshal(joinRuleEvent.Content(), &joinRuleAllow) + + return &gomatrixserverlib.JoinRuleContent{ + JoinRule: joinRule.JoinRule, + Allow: joinRuleAllow.Allow, + }, nil +} + +func attemptMakeJoinForRestrictedMembership( + httpReq *http.Request, + cfg *config.FederationAPI, + rsAPI api.RoomserverInternalAPI, + verRes *api.QueryRoomVersionForRoomResponse, + provider gomatrixserverlib.AuthEvents, + builder *gomatrixserverlib.EventBuilder, + joinRules gomatrixserverlib.JoinRuleContent, + userID string, +) util.JSONResponse { + logger := util.GetLogger(httpReq.Context()).WithField("restricted_join", userID) + foundUserInAnyRoom := false + ableToAuthoriseJoin := false + + // As a last effort, see if any of the restricted join rules match. + // If so, we might be able to modify and sign the event so that it + // does pass auth. + var powerLevels gomatrixserverlib.PowerLevelContent + if powerLevelsEvent, err := provider.PowerLevels(); err != nil { + logger.WithError(err).Error("Failed to get power levels from auth events") + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.UnableToAuthoriseJoin("Room power levels do not exist"), + } + } else if err := json.Unmarshal(powerLevelsEvent.Content(), &powerLevels); err != nil { + logger.WithError(err).Error("Failed to unmarshal power levels") + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.UnableToAuthoriseJoin("Failed to unmarshal room power levels"), + } + } + + // Let's see if we can validate the user being in + // any of the allowed rooms. + for _, allowed := range joinRules.Allow { + // Skip types that we don't know about. + if allowed.Type != gomatrixserverlib.MRoomMembership { + continue + } + if _, _, err := gomatrixserverlib.SplitID('!', allowed.RoomID); err != nil { + continue + } + + // Ask the room server if we know about the specified room ID. + queryReq := &api.QueryMembershipsForRoomRequest{ + RoomID: allowed.RoomID, + JoinedOnly: true, + } + queryRes := &api.QueryMembershipsForRoomResponse{} + if err := rsAPI.QueryMembershipsForRoom(httpReq.Context(), queryReq, queryRes); err != nil { + logger.WithError(err).Errorf("Failed to query membership for room %q", queryReq.RoomID) + continue + } + + // Now have a look and see if any of the joined users match the + // user who has initiated this join. + found := false + for _, member := range queryRes.JoinEvents { + if *member.StateKey == userID { + found = true + break + } + } + + // The user doesn't seem to exist in this room, try the next one. + if !found { + continue + } + + // Now look through all of the join events of the other members. Our goal + // is to try and find a user from our own server that has a suitable power + // level to popuate into the `join_authorised_via_users_server` field. + foundUserInAnyRoom = true + for _, member := range queryRes.JoinEvents { + // If the user doesn't come from our own server then it's no good, try + // the next one instead. + _, domain, err := gomatrixserverlib.SplitID('@', *member.StateKey) + if err != nil { + continue + } + if domain != cfg.Matrix.ServerName { + continue + } + + // We have a user who is joined to the room, so we can authorise joins. + // We will only be able to "grant" joins if any of our users have the + // power to invite other users — this flag helps us to return the right + // error code if not. + ableToAuthoriseJoin = true + + // If the user has the ability to invite to the room then they are a + // suitable candidate for the `join_authorised_via_users_server`. + if powerLevels.UserLevel(*member.StateKey) >= powerLevels.Invite { + // We'll set the event content again, this time including the + // `join_authorised_via_users_server` field for the chosen user. + err := builder.SetContent(map[string]interface{}{ + "membership": gomatrixserverlib.Join, + "join_authorised_via_users_server": *member.StateKey, + }) + if err != nil { + logger.WithError(err).Error("builder.SetContent failed") + return jsonerror.InternalServerError() + } + + // Then we'll build the event again. This is a second hit on the + // roomserver sadly, but it's a necessary evil. + queryRes := api.QueryLatestEventsAndStateResponse{ + RoomVersion: verRes.RoomVersion, + } + event, err := eventutil.QueryAndBuildEvent(httpReq.Context(), builder, cfg.Matrix, time.Now(), rsAPI, &queryRes) + if err != nil { + logger.WithError(err).Error("builder.SetContent failed") + return jsonerror.InternalServerError() + } + + // Sign and return the event. This is basically our seal of approval + // that other servers can use to verify that the user we put into the + // `join_authorised_via_users_server` field was actually checked + // and found by us. + return util.JSONResponse{ + Code: http.StatusOK, + JSON: map[string]interface{}{ + "event": event, + "room_version": verRes.RoomVersion, + }, + } + } + } + } + + switch { + case ableToAuthoriseJoin && foundUserInAnyRoom: + // We found ourselves in some of the allowed rooms, but none of our + // users had a suitable power level to invite other users, so we + // don't have the ability to grant joins. + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.UnableToGrantJoin("None of the users from this homeserver have the power to invite"), + } + case ableToAuthoriseJoin && !foundUserInAnyRoom: + // We found ourselves in some of the allowed rooms, but none of them + // seemed to contain the joining user. + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("You are not joined to any allowed rooms"), + } + default: + // We don't seem to be joined to any of the allowed rooms, so we + // can't even check if the join is supposed to be allowed or not. + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.UnableToAuthoriseJoin("This homeserver isn't joined to any of the allowed rooms"), + } + } +} + // 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, @@ -308,6 +516,33 @@ func SendJoin( } } + // If the room has a restricted join rule, we need to make sure that the + // 'join_authorised_by_users_server' makes some kind of sense. This means + // we need to, once again, repeat the checks. + provider := gomatrixserverlib.NewAuthEvents( + gomatrixserverlib.UnwrapEventHeaders(stateAndAuthChainResponse.StateEvents), + ) + joinRule, err := getJoinRule(provider) + if err != nil { + return util.JSONResponse{ + Code: http.StatusNotFound, + JSON: jsonerror.NotFound("Failed to find room join rules"), + } + } + if joinRule.JoinRule == gomatrixserverlib.Restricted { + if signedEvent, err := verifyRestrictedMembershipForSendJoin( + httpReq.Context(), cfg, rsAPI, provider, event, joinRule, + ); err == nil { + event = signedEvent + } else { + logrus.WithError(err).Error("Failed to verify restricted join") + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.Unknown("Failed to verify restricted join: " + err.Error()), + } + } + } + // 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 @@ -328,7 +563,7 @@ func SendJoin( util.GetLogger(httpReq.Context()).WithField(logrus.ErrorKey, response.ErrMsg).Error("SendEvents failed") if response.NotAllowed { return util.JSONResponse{ - Code: http.StatusBadRequest, + Code: http.StatusForbidden, JSON: jsonerror.Forbidden(response.ErrMsg), } } @@ -346,6 +581,7 @@ func SendJoin( return util.JSONResponse{ Code: http.StatusOK, JSON: gomatrixserverlib.RespSendJoin{ + Event: event, StateEvents: gomatrixserverlib.UnwrapEventHeaders(stateAndAuthChainResponse.StateEvents), AuthEvents: gomatrixserverlib.UnwrapEventHeaders(stateAndAuthChainResponse.AuthChainEvents), Origin: cfg.Matrix.ServerName, @@ -353,6 +589,95 @@ func SendJoin( } } +func verifyRestrictedMembershipForSendJoin( + ctx context.Context, + cfg *config.FederationAPI, + rsAPI api.RoomserverInternalAPI, + provider gomatrixserverlib.AuthEvents, + event *gomatrixserverlib.Event, + joinRules *gomatrixserverlib.JoinRuleContent, +) (*gomatrixserverlib.Event, error) { + // Extract the membership content. + var memberContent gomatrixserverlib.MemberContent + if err := json.Unmarshal(event.Content(), &memberContent); err != nil { + return nil, fmt.Errorf("json.Unmarshal(memberContent): %w", err) + } + + // If there's no `join_authorised_via_users_server` key then there's + // nothing else to do. This might be because it's a join -> join transition + // or the response to an invite. Return the original event and it'll either + // pass auth for some other reason or it will fail auth correctly. + if memberContent.AuthorisedVia == "" { + return event, nil + } + + // As a last effort, see if any of the restricted join rules match. + // If so, we might be able to modify and sign the event so that it + // does pass auth. + var powerLevels gomatrixserverlib.PowerLevelContent + if powerLevelsEvent, err := provider.PowerLevels(); err != nil { + return nil, fmt.Errorf("provider.PowerLevels: %w", err) + } else if err := json.Unmarshal(powerLevelsEvent.Content(), &powerLevels); err != nil { + return nil, fmt.Errorf("json.Unmarshal(powerLevels): %w", err) + } + + // Let's see if we can validate the user being in + // any of the allowed rooms. + for _, allowed := range joinRules.Allow { + // Skip types that we don't know about. + if allowed.Type != gomatrixserverlib.MRoomMembership { + continue + } + if _, _, err := gomatrixserverlib.SplitID('!', allowed.RoomID); err != nil { + continue + } + + // Ask the room server if we know about the specified room ID. + queryReq := &api.QueryMembershipsForRoomRequest{ + RoomID: allowed.RoomID, + JoinedOnly: true, + } + queryRes := &api.QueryMembershipsForRoomResponse{} + if err := rsAPI.QueryMembershipsForRoom(ctx, queryReq, queryRes); err != nil { + continue + } + + // Now have a look and see if any of the joined users match the + // user who has initiated this join. + found := false + for _, member := range queryRes.JoinEvents { + if event.StateKeyEquals(*member.StateKey) { + found = true + break + } + } + + // The user doesn't seem to exist in this room, try the next one. + if !found { + continue + } + + // Now look through all of the join events of the nominated user. + for _, member := range queryRes.JoinEvents { + // Check if the user is the selected user from the join event. + if *member.StateKey != memberContent.AuthorisedVia { + continue + } + + // If the user has the ability to invite to the room then they are a + // suitable candidate for the `join_authorised_via_users_server`. + if powerLevels.UserLevel(*member.StateKey) >= powerLevels.Invite { + // We'll set the event content again, this time including the + // `join_authorised_via_users_server` field for the chosen user. + signed := event.Sign(string(cfg.Matrix.ServerName), cfg.Matrix.KeyID, cfg.Matrix.PrivateKey) + return &signed, nil + } + } + } + + return nil, fmt.Errorf("the required memberships were not satisfied") +} + type eventsByDepth []*gomatrixserverlib.HeaderedEvent func (e eventsByDepth) Len() int { diff --git a/federationsender/internal/perform.go b/federationsender/internal/perform.go index 53fa974b..ce21d0ed 100644 --- a/federationsender/internal/perform.go +++ b/federationsender/internal/perform.go @@ -132,6 +132,7 @@ func (r *FederationSenderInternalAPI) PerformJoin( ) } +// nolint:gocyclo func (r *FederationSenderInternalAPI) performJoinUsingServer( ctx context.Context, roomID, userID string, @@ -149,6 +150,11 @@ func (r *FederationSenderInternalAPI) performJoinUsingServer( supportedVersions, ) if err != nil { + // A well-formed HTTP error response that isn't in the 500s isn't fatal, + // so we shouldn't punish the server by backing off. + if httpErr, ok := err.(gomatrix.HTTPError); ok && httpErr.Code < 500 { + return httpErr + } // TODO: Check if the user was not allowed to join the room. r.statistics.ForServer(serverName).Failure() return fmt.Errorf("r.federation.MakeJoin: %w", err) @@ -157,21 +163,28 @@ func (r *FederationSenderInternalAPI) performJoinUsingServer( // Set all the fields to be what they should be, this should be a no-op // but it's possible that the remote server returned us something "odd" - respMakeJoin.JoinEvent.Type = gomatrixserverlib.MRoomMember - respMakeJoin.JoinEvent.Sender = userID - respMakeJoin.JoinEvent.StateKey = &userID - respMakeJoin.JoinEvent.RoomID = roomID - respMakeJoin.JoinEvent.Redacts = "" - if content == nil { - content = map[string]interface{}{} + switch { + case respMakeJoin.JoinEvent.Type != gomatrixserverlib.MRoomMember: + fallthrough + case respMakeJoin.JoinEvent.Sender != userID: + fallthrough + case respMakeJoin.JoinEvent.StateKey == nil: + fallthrough + case *respMakeJoin.JoinEvent.StateKey != userID: + fallthrough + case respMakeJoin.JoinEvent.RoomID != roomID: + fallthrough + case respMakeJoin.JoinEvent.Redacts != "": + fallthrough + case len(respMakeJoin.JoinEvent.Content) == 0: + return fmt.Errorf("respMakeJoin.JoinEvent contains invalid values") + } + if err = json.Unmarshal(respMakeJoin.JoinEvent.Content, &content); err != nil { + return fmt.Errorf("json.Unmarshal: %w", err) } - content["membership"] = "join" if err = respMakeJoin.JoinEvent.SetContent(content); err != nil { return fmt.Errorf("respMakeJoin.JoinEvent.SetContent: %w", err) } - if err = respMakeJoin.JoinEvent.SetUnsigned(struct{}{}); err != nil { - return fmt.Errorf("respMakeJoin.JoinEvent.SetUnsigned: %w", err) - } // Work out if we support the room version that has been supplied in // the make_join response. @@ -227,9 +240,18 @@ func (r *FederationSenderInternalAPI) performJoinUsingServer( // to complete, but if the client does give up waiting, we'll // still continue to process the join anyway so that we don't // waste the effort. + waiterr := make(chan error, 1) go func() { + defer close(waiterr) defer cancel() + // If the remote server returned a signed membership event then + // we will use that instead. That is necessary for restricted + // joins to work. + if respSendJoin.Event != nil { + event = respSendJoin.Event + } + // TODO: Can we expand Check here to return a list of missing auth // events rather than failing one at a time? respState, err := respSendJoin.Check(ctx, r.keyRing, event, federatedAuthProvider(ctx, r.federation, r.keyRing, serverName)) @@ -238,6 +260,7 @@ func (r *FederationSenderInternalAPI) performJoinUsingServer( "room_id": roomID, "user_id": userID, }).WithError(err).Error("Failed to process room join response") + waiterr <- err return } @@ -255,12 +278,17 @@ func (r *FederationSenderInternalAPI) performJoinUsingServer( "room_id": roomID, "user_id": userID, }).WithError(err).Error("Failed to send room join response to roomserver") + waiterr <- err return } }() - <-ctx.Done() - return nil + select { + case <-ctx.Done(): + return nil + case err := <-waiterr: + return err + } } // PerformOutboundPeekRequest implements api.FederationSenderInternalAPI diff --git a/go.mod b/go.mod index 41c66a69..eeeef9b5 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/matrix-org/go-http-js-libp2p v0.0.0-20200518170932-783164aeeda4 github.com/matrix-org/go-sqlite3-js v0.0.0-20210709140738-b0d1ba599a6d github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 - github.com/matrix-org/gomatrixserverlib v0.0.0-20211112151542-af2616bf4c80 + github.com/matrix-org/gomatrixserverlib v0.0.0-20211115130817-ba4a3b9a6f12 github.com/matrix-org/naffka v0.0.0-20210623111924-14ff508b58e0 github.com/matrix-org/pinecone v0.0.0-20211022090602-08a50945ac89 github.com/matrix-org/util v0.0.0-20200807132607-55161520e1d4 diff --git a/go.sum b/go.sum index ca604c9f..096a2191 100644 --- a/go.sum +++ b/go.sum @@ -993,8 +993,8 @@ github.com/matrix-org/go-sqlite3-js v0.0.0-20210709140738-b0d1ba599a6d/go.mod h1 github.com/matrix-org/gomatrix v0.0.0-20190528120928-7df988a63f26/go.mod h1:3fxX6gUjWyI/2Bt7J1OLhpCzOfO/bB3AiX0cJtEKud0= github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 h1:ZtO5uywdd5dLDCud4r0r55eP4j9FuUNpl60Gmntcop4= github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= -github.com/matrix-org/gomatrixserverlib v0.0.0-20211112151542-af2616bf4c80 h1:8Fm/nsRuWsdCWNMfAji/Pi6ynUOch9eLkN15cqXOFx0= -github.com/matrix-org/gomatrixserverlib v0.0.0-20211112151542-af2616bf4c80/go.mod h1:rB8tBUUUo1rzUqpzklRDSooxZ6YMhoaEPx4SO5fGeUc= +github.com/matrix-org/gomatrixserverlib v0.0.0-20211115130817-ba4a3b9a6f12 h1:vpZov9fXOydP3U5dexANC5yttnMXaSdStlb2mlbKqbs= +github.com/matrix-org/gomatrixserverlib v0.0.0-20211115130817-ba4a3b9a6f12/go.mod h1:rB8tBUUUo1rzUqpzklRDSooxZ6YMhoaEPx4SO5fGeUc= github.com/matrix-org/naffka v0.0.0-20210623111924-14ff508b58e0 h1:HZCzy4oVzz55e+cOMiX/JtSF2UOY1evBl2raaE7ACcU= github.com/matrix-org/naffka v0.0.0-20210623111924-14ff508b58e0/go.mod h1:sjyPyRxKM5uw1nD2cJ6O2OxI6GOqyVBfNXqKjBZTBZE= github.com/matrix-org/pinecone v0.0.0-20211022090602-08a50945ac89 h1:6JkIymZ1vxfI0shSpg6gNPTJaF4/95Evy34slPVZGKM= @@ -1884,7 +1884,6 @@ gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/roomserver/internal/perform/perform_join.go b/roomserver/internal/perform/perform_join.go index 772c9d7d..eec1903a 100644 --- a/roomserver/internal/perform/perform_join.go +++ b/roomserver/internal/perform/perform_join.go @@ -16,6 +16,7 @@ package perform import ( "context" + "encoding/json" "errors" "fmt" "strings" @@ -150,6 +151,7 @@ func (r *Joiner) performJoinRoomByAlias( } // TODO: Break this function up a bit +// nolint:gocyclo func (r *Joiner) performJoinRoomByID( ctx context.Context, req *rsAPI.PerformJoinRequest, @@ -175,8 +177,8 @@ func (r *Joiner) performJoinRoomByID( // If the server name in the room ID isn't ours then it's a // possible candidate for finding the room via federation. Add - // it to the list of servers to try. - if domain != r.Cfg.Matrix.ServerName { + // it to the list of servers to try if we have no better ideas. + if len(req.ServerNames) == 0 && domain != r.Cfg.Matrix.ServerName { req.ServerNames = append(req.ServerNames, domain) } @@ -240,6 +242,76 @@ func (r *Joiner) performJoinRoomByID( return req.RoomIDOrAlias, joinedVia, err } + // Check if the room is a restricted room. If so, update the event + // builder content. If we can validate that we have a user in one of + // the restricted rooms then populate 'join_authorised_via_users_server', + // which will allow the event to pass event auth. If we can't then we + // leave the event as it is, which will fail auth. + if restricted, roomIDs, rerr := r.checkIfRestrictedJoin(ctx, req); rerr != nil { + return "", "", err + } else if restricted { + // Try to satisfy the join using our own users. This only works if + // we have users that are joined to the rooms with suitable power levels + // to issue invites. + success := false + for _, roomID := range roomIDs { + if err = r.attemptRestrictedJoinUsingRoomID(ctx, req, roomID, &eb); err == nil { + success = true + break + } + } + + // If we don't, then we need to resort to doing a federated join. + // We'll use the users from the power level event to work out which + // servers are suitable candidates to join via. Only servers that have + // the power to issue invites are any help to us. + if !success { + var powerLevelsEvent *gomatrixserverlib.HeaderedEvent + var powerLevelsContent gomatrixserverlib.PowerLevelContent + powerLevelsEvent, err = r.DB.GetStateEvent(ctx, req.RoomIDOrAlias, gomatrixserverlib.MRoomPowerLevels, "") + if err != nil { + return "", "", &rsAPI.PerformError{ + Code: rsAPI.PerformErrorNotAllowed, + Msg: fmt.Sprintf("Unable to retrieve the power levels: %s", err), + } + } + if err = json.Unmarshal(powerLevelsEvent.Content(), &powerLevelsContent); err != nil { + return "", "", &rsAPI.PerformError{ + Code: rsAPI.PerformErrorNotAllowed, + Msg: fmt.Sprintf("Unable to parse the power levels: %s", err), + } + } + + // Don't use any of the server names from the request - they are + // no longer relevant. + var serverName gomatrixserverlib.ServerName + req.ServerNames = req.ServerNames[:0] + joinEvents: + for userID, pl := range powerLevelsContent.Users { + if pl <= powerLevelsContent.Invite { + continue + } + _, serverName, err = gomatrixserverlib.SplitID('@', userID) + if err != nil { + continue + } + for _, s := range req.ServerNames { + if s == r.Cfg.Matrix.ServerName { + continue + } + if s == serverName { + continue joinEvents + } + } + req.ServerNames = append(req.ServerNames, serverName) + } + + // Attempt a federated join via the found servers. + joinedVia, err = r.performFederatedJoinRoomByID(ctx, req) + return req.RoomIDOrAlias, joinedVia, err + } + } + // Try to construct an actual join event from the template. // If this succeeds then it is a sign that the room already exists // locally on the homeserver. @@ -281,7 +353,7 @@ func (r *Joiner) performJoinRoomByID( if err = inputRes.Err(); err != nil { return "", "", &rsAPI.PerformError{ Code: rsAPI.PerformErrorNotAllowed, - Msg: fmt.Sprintf("InputRoomEvents auth failed: %s", err), + Msg: fmt.Sprintf("Failed to join the room: %s", err), } } } @@ -297,7 +369,7 @@ func (r *Joiner) performJoinRoomByID( if len(req.ServerNames) == 0 { return "", "", &rsAPI.PerformError{ Code: rsAPI.PerformErrorNoRoom, - Msg: fmt.Sprintf("room ID %q does not exist", req.RoomIDOrAlias), + Msg: fmt.Sprintf("Room ID %q does not exist!", req.RoomIDOrAlias), } } } @@ -308,7 +380,10 @@ func (r *Joiner) performJoinRoomByID( default: // Something else went wrong. - return "", "", fmt.Errorf("error joining local room: %q", err) + return "", "", &rsAPI.PerformError{ + Code: rsAPI.PerformErrorNotAllowed, + Msg: fmt.Sprintf("Failed to join the room: %s", err), + } } // By this point, if req.RoomIDOrAlias contained an alias, then @@ -318,6 +393,137 @@ func (r *Joiner) performJoinRoomByID( return req.RoomIDOrAlias, r.Cfg.Matrix.ServerName, nil } +func (r *Joiner) checkIfRestrictedJoin( + ctx context.Context, + req *rsAPI.PerformJoinRequest, +) (bool, []string, error) { + // Look up the join rules event for the room, so we can check if it is a + // restricted room or not. + joinRuleEvent, err := r.DB.GetStateEvent(ctx, req.RoomIDOrAlias, gomatrixserverlib.MRoomJoinRules, "") + if err != nil { + return false, nil, &rsAPI.PerformError{ + Code: rsAPI.PerformErrorNotAllowed, + Msg: fmt.Sprintf("Unable to retrieve the join rules: %s", err), + } + } + + // First unmarshal the join rule itself. It might seem strange that this is + // a two-step process, but the Complement tests specifically populate the + // 'allow' field with a nonsense value that won't unmarshal and therefore + // trying to unmarshal a gomatrixserverlib.JoinRuleContent fails entirely. + // We need to get the join rule first to check if the room is restricted + // though, regardless of what the 'allow' key contains. + joinRule := struct { + JoinRule string `json:"join_rule"` + }{ + JoinRule: gomatrixserverlib.Public, + } + if err = json.Unmarshal(joinRuleEvent.Content(), &joinRule); err != nil { + return false, nil, &rsAPI.PerformError{ + Code: rsAPI.PerformErrorNotAllowed, + Msg: fmt.Sprintf("The room join rules are invalid: %s", err), + } + } + if joinRule.JoinRule != gomatrixserverlib.Restricted { + return false, nil, nil + } + + // Then try and extract the join rule 'allow' key. It's possible that this + // step can fail but we need to be OK with that — if we do, we will just + // treat it as if it is an empty list. + var joinRuleAllow struct { + Allow []gomatrixserverlib.JoinRuleContentAllowRule `json:"allow"` + } + _ = json.Unmarshal(joinRuleEvent.Content(), &joinRuleAllow) + + // Now create a list of room IDs that we can check in order to validate + // that the restricted join can be completed. + roomIDs := make([]string, 0, len(joinRuleAllow.Allow)) + for _, allowed := range joinRuleAllow.Allow { + if allowed.Type != gomatrixserverlib.MRoomMembership { + continue + } + roomIDs = append(roomIDs, allowed.RoomID) + } + return true, roomIDs, nil +} + +func (r *Joiner) attemptRestrictedJoinUsingRoomID( + ctx context.Context, + req *rsAPI.PerformJoinRequest, + roomID string, + eb *gomatrixserverlib.EventBuilder, +) error { + // Dig out information from the room, including the power levels and + // our local members joined to that room. + roomInfo, err := r.DB.RoomInfo(ctx, roomID) + if err != nil { + return fmt.Errorf("r.DB.RoomInfo: %w", err) + } + powerLevelEvent, err := r.DB.GetStateEvent(ctx, roomID, gomatrixserverlib.MRoomPowerLevels, "") + if err != nil { + return fmt.Errorf("r.DB.GetStateEvent: %w", err) + } + powerLevels, err := powerLevelEvent.PowerLevels() + if err != nil { + return fmt.Errorf("powerLevelEvent.PowerLevels: %w", err) + } + eventNIDs, err := r.DB.GetMembershipEventNIDsForRoom(ctx, roomInfo.RoomNID, true, true) + if err != nil { + return fmt.Errorf("r.DB.GetMembershipEventNIDsForRoom: %w", err) + } + events, err := r.DB.Events(ctx, eventNIDs) + if err != nil { + return fmt.Errorf("r.DB.Events: %w", err) + } + + // First of all, look and see if the joining user is joined to the + // allowed room. If they aren't then there's no point in doing anything + // else. + foundInAllowedRoom := false + for _, event := range events { + userID := *event.StateKey() + if userID == req.UserID { + foundInAllowedRoom = true + break + } + } + if !foundInAllowedRoom { + return fmt.Errorf("the user is not joined to this room") + } + + // Now that we've confirmed that the user is joined to the allowed + // room, we now need to try and find an authorising user. This needs + // to be one of our own users with a power level sufficient to issue + // invites. If we find one then we can place that user ID into the + // `join_authorised_via_users_server` field. + for _, event := range events { + userID := *event.StateKey() + if userID == req.UserID { + continue + } + _, domain, err := gomatrixserverlib.SplitID('@', userID) + if err != nil || domain != r.ServerName { + continue + } + if powerLevels.UserLevel(userID) < powerLevels.Invite { + continue + } + if err := eb.SetContent(map[string]string{ + "membership": gomatrixserverlib.Join, + "join_authorised_via_users_server": userID, + }); err != nil { + return fmt.Errorf("eb.SetContent: %w", err) + } + return nil + } + + // If we've reached this point then we don't have any of our own + // users in the room able to issue invites, so we need to give up + // and hope that we have a suitable user in another room (if any). + return fmt.Errorf("no suitable power level users found in the room") +} + func (r *Joiner) performFederatedJoinRoomByID( ctx context.Context, req *rsAPI.PerformJoinRequest,