Implement third party identifier invites

This commit is contained in:
Daniel Wagner-Hall 2015-10-01 17:49:52 +01:00
parent 301141515a
commit 5b3e9713dd
11 changed files with 275 additions and 19 deletions

View file

@ -14,15 +14,19 @@
# limitations under the License.
"""This module contains classes for authenticating the user."""
from nacl.exceptions import BadSignatureError
from twisted.internet import defer
from synapse.api.constants import EventTypes, Membership, JoinRules
from synapse.api.errors import AuthError, Codes, SynapseError
from synapse.util.logutils import log_function
from synapse.util.thirdpartyinvites import ThirdPartyInvites
from synapse.types import UserID, EventID
from unpaddedbase64 import decode_base64
import logging
import nacl.signing
import pymacaroons
logger = logging.getLogger(__name__)
@ -31,6 +35,7 @@ logger = logging.getLogger(__name__)
AuthEventTypes = (
EventTypes.Create, EventTypes.Member, EventTypes.PowerLevels,
EventTypes.JoinRules, EventTypes.RoomHistoryVisibility,
EventTypes.ThirdPartyInvite,
)
@ -318,6 +323,7 @@ class Auth(object):
pass
elif join_rule == JoinRules.INVITE:
if not caller_in_room and not caller_invited:
if not self._verify_third_party_invite(event, auth_events):
raise AuthError(403, "You are not invited to this room.")
else:
# TODO (erikj): may_join list
@ -344,6 +350,31 @@ class Auth(object):
return True
def _verify_third_party_invite(self, event, auth_events):
for key in ThirdPartyInvites.JOIN_KEYS:
if key not in event.content:
return False
token = event.content["token"]
invite_event = auth_events.get(
(EventTypes.ThirdPartyInvite, token,)
)
if not invite_event:
return False
try:
public_key = event.content["public_key"]
key_validity_url = event.content["key_validity_url"]
if invite_event.content["public_key"] != public_key:
return False
if invite_event.content["key_validity_url"] != key_validity_url:
return False
verify_key = nacl.signing.VerifyKey(decode_base64(public_key))
encoded_signature = event.content["signature"]
signature = decode_base64(encoded_signature)
verify_key.verify(token, signature)
return True
except (KeyError, BadSignatureError,):
return False
def _get_power_level_event(self, auth_events):
key = (EventTypes.PowerLevels, "", )
return auth_events.get(key)

View file

@ -63,6 +63,7 @@ class EventTypes(object):
PowerLevels = "m.room.power_levels"
Aliases = "m.room.aliases"
Redaction = "m.room.redaction"
ThirdPartyInvite = "m.room.third_party_invite"
RoomHistoryVisibility = "m.room.history_visibility"
CanonicalAlias = "m.room.canonical_alias"

View file

@ -25,6 +25,7 @@ from synapse.api.errors import (
from synapse.util import unwrapFirstError
from synapse.util.caches.expiringcache import ExpiringCache
from synapse.util.logutils import log_function
from synapse.util.thirdpartyinvites import ThirdPartyInvites
from synapse.events import FrozenEvent
import synapse.metrics
@ -356,18 +357,22 @@ class FederationClient(FederationBase):
defer.returnValue(signed_auth)
@defer.inlineCallbacks
def make_join(self, destinations, room_id, user_id):
def make_join(self, destinations, room_id, user_id, content):
for destination in destinations:
if destination == self.server_name:
continue
args = {}
if ThirdPartyInvites.has_join_keys(content):
ThirdPartyInvites.copy_join_keys(content, args)
try:
ret = yield self.transport_layer.make_join(
destination, room_id, user_id
destination, room_id, user_id, args
)
pdu_dict = ret["event"]
logger.debug("Got response to make_join: %s", pdu_dict)
defer.returnValue(

View file

@ -23,10 +23,12 @@ from synapse.util.logutils import log_function
from synapse.events import FrozenEvent
import synapse.metrics
from synapse.api.errors import FederationError, SynapseError
from synapse.api.errors import FederationError, SynapseError, Codes
from synapse.crypto.event_signing import compute_event_signature
from synapse.util.thirdpartyinvites import ThirdPartyInvites
import simplejson as json
import logging
@ -228,8 +230,19 @@ class FederationServer(FederationBase):
)
@defer.inlineCallbacks
def on_make_join_request(self, room_id, user_id):
pdu = yield self.handler.on_make_join_request(room_id, user_id)
def on_make_join_request(self, room_id, user_id, query):
threepid_details = {}
if ThirdPartyInvites.has_join_keys(query):
for k in ThirdPartyInvites.JOIN_KEYS:
if not isinstance(query[k], list) or len(query[k]) != 1:
raise FederationError(
"FATAL",
Codes.MISSING_PARAM,
"key %s value %s" % (k, query[k],),
None
)
threepid_details[k] = query[k][0]
pdu = yield self.handler.on_make_join_request(room_id, user_id, threepid_details)
time_now = self._clock.time_msec()
defer.returnValue({"event": pdu.get_pdu_json(time_now)})

View file

@ -160,13 +160,14 @@ class TransportLayerClient(object):
@defer.inlineCallbacks
@log_function
def make_join(self, destination, room_id, user_id, retry_on_dns_fail=True):
def make_join(self, destination, room_id, user_id, args={}):
path = PREFIX + "/make_join/%s/%s" % (room_id, user_id)
content = yield self.client.get_json(
destination=destination,
path=path,
retry_on_dns_fail=retry_on_dns_fail,
args=args,
retry_on_dns_fail=True,
)
defer.returnValue(content)

View file

@ -292,7 +292,7 @@ class FederationMakeJoinServlet(BaseFederationServlet):
@defer.inlineCallbacks
def on_GET(self, origin, content, query, context, user_id):
content = yield self.handler.on_make_join_request(context, user_id)
content = yield self.handler.on_make_join_request(context, user_id, query)
defer.returnValue((200, content))

View file

@ -21,6 +21,7 @@ from synapse.api.constants import Membership, EventTypes
from synapse.types import UserID, RoomAlias
from synapse.util.logcontext import PreserveLoggingContext
from synapse.util.thirdpartyinvites import ThirdPartyInvites
import logging
@ -123,6 +124,16 @@ class BaseHandler(object):
)
)
if (
event.type == EventTypes.Member and
event.content["membership"] == Membership.JOIN and
ThirdPartyInvites.has_join_keys(event.content)
):
yield ThirdPartyInvites.check_key_valid(
self.hs.get_simple_http_client(),
event
)
(event_stream_id, max_stream_id) = yield self.store.persist_event(
event, context=context
)

View file

@ -39,7 +39,7 @@ from twisted.internet import defer
import itertools
import logging
from synapse.util.thirdpartyinvites import ThirdPartyInvites
logger = logging.getLogger(__name__)
@ -572,7 +572,8 @@ class FederationHandler(BaseHandler):
origin, pdu = yield self.replication_layer.make_join(
target_hosts,
room_id,
joinee
joinee,
content
)
logger.debug("Got response to make_join: %s", pdu)
@ -712,14 +713,18 @@ class FederationHandler(BaseHandler):
@defer.inlineCallbacks
@log_function
def on_make_join_request(self, room_id, user_id):
def on_make_join_request(self, room_id, user_id, query):
""" We've received a /make_join/ request, so we create a partial
join event for the room and return that. We don *not* persist or
process it until the other server has signed it and sent it back.
"""
event_content = {"membership": Membership.JOIN}
if ThirdPartyInvites.has_join_keys(query):
ThirdPartyInvites.copy_join_keys(query, event_content)
builder = self.event_builder_factory.new({
"type": EventTypes.Member,
"content": {"membership": Membership.JOIN},
"content": event_content,
"room_id": room_id,
"sender": user_id,
"state_key": user_id,
@ -731,6 +736,9 @@ class FederationHandler(BaseHandler):
self.auth.check(event, auth_events=context.current_state)
if ThirdPartyInvites.has_join_keys(event.content):
ThirdPartyInvites.check_key_valid(self.hs.get_simple_http_client(), event)
defer.returnValue(event)
@defer.inlineCallbacks

View file

@ -462,6 +462,10 @@ class RoomMemberHandler(BaseHandler):
if prev_state and prev_state.membership == Membership.INVITE:
inviter = UserID.from_string(prev_state.user_id)
should_do_dance = not self.hs.is_mine(inviter)
room_hosts = [inviter.domain]
elif "sender" in event.content:
inviter = UserID.from_string(event.content["sender"])
should_do_dance = not self.hs.is_mine(inviter)
room_hosts = [inviter.domain]
else:

View file

@ -17,7 +17,7 @@
from twisted.internet import defer
from base import ClientV1RestServlet, client_path_pattern
from synapse.api.errors import SynapseError, Codes
from synapse.api.errors import SynapseError, Codes, AuthError
from synapse.streams.config import PaginationConfig
from synapse.api.constants import EventTypes, Membership
from synapse.types import UserID, RoomID, RoomAlias
@ -26,7 +26,7 @@ from synapse.events.utils import serialize_event
import simplejson as json
import logging
import urllib
from synapse.util.thirdpartyinvites import ThirdPartyInvites
logger = logging.getLogger(__name__)
@ -415,9 +415,35 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
# target user is you unless it is an invite
state_key = user.to_string()
if membership_action in ["invite", "ban", "kick"]:
if "user_id" not in content:
raise SynapseError(400, "Missing user_id key.")
try:
state_key = content["user_id"]
except KeyError:
if (
membership_action != "invite" or
not ThirdPartyInvites.has_invite_keys(content)
):
raise SynapseError(400, "Missing user_id key.")
id_server = content["id_server"]
medium = content["medium"]
address = content["address"]
display_name = content["display_name"]
state_key = yield self._lookup_3pid_user(id_server, medium, address)
if not state_key:
yield self._make_and_store_3pid_invite(
id_server,
display_name,
medium,
address,
room_id,
user,
token_id,
txn_id=txn_id
)
defer.returnValue((200, {}))
return
# make sure it looks like a user ID; it'll throw if it's invalid.
UserID.from_string(state_key)
@ -425,10 +451,18 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
membership_action = "leave"
msg_handler = self.handlers.message_handler
event_content = {
"membership": unicode(membership_action),
}
if membership_action == "join" and ThirdPartyInvites.has_join_keys(content):
ThirdPartyInvites.copy_join_keys(content, event_content)
yield msg_handler.create_and_send_event(
{
"type": EventTypes.Member,
"content": {"membership": unicode(membership_action)},
"content": event_content,
"room_id": room_id,
"sender": user.to_string(),
"state_key": state_key,
@ -439,6 +473,92 @@ class RoomMembershipRestServlet(ClientV1RestServlet):
defer.returnValue((200, {}))
@defer.inlineCallbacks
def _lookup_3pid_user(self, id_server, medium, address):
"""Looks up a 3pid in the passed identity server.
Args:
id_server (str): The server name (including port, if required)
of the identity server to use.
medium (str): The type of the third party identifier (e.g. "email").
address (str): The third party identifier (e.g. "foo@example.com").
Returns:
(str) the matrix ID of the 3pid, or None if it is not recognized.
"""
try:
data = yield self.hs.get_simple_http_client().get_json(
"https://%s/_matrix/identity/api/v1/lookup" % (id_server,),
{
"medium": medium,
"address": address,
}
)
if "mxid" in data:
# TODO: Validate the response signature and such
defer.returnValue(data["mxid"])
except IOError:
# TODO: Log something maybe?
defer.returnValue(None)
@defer.inlineCallbacks
def _make_and_store_3pid_invite(
self,
id_server,
display_name,
medium,
address,
room_id,
user,
token_id,
txn_id
):
token, public_key, key_validity_url = (
yield self._ask_id_server_for_third_party_invite(
id_server,
medium,
address,
room_id,
user.to_string()
)
)
msg_handler = self.handlers.message_handler
yield msg_handler.create_and_send_event(
{
"type": EventTypes.ThirdPartyInvite,
"content": {
"display_name": display_name,
"key_validity_url": key_validity_url,
"public_key": public_key,
},
"room_id": room_id,
"sender": user.to_string(),
"state_key": token,
},
token_id=token_id,
txn_id=txn_id,
)
@defer.inlineCallbacks
def _ask_id_server_for_third_party_invite(
self, id_server, medium, address, room_id, sender):
is_url = "https://%s/_matrix/identity/api/v1/nonce-it-up" % (id_server,)
data = yield self.hs.get_simple_http_client().post_urlencoded_get_json(
is_url,
{
"medium": medium,
"address": address,
"room_id": room_id,
"sender": sender,
}
)
# TODO: Check for success
token = data["token"]
public_key = data["public_key"]
key_validity_url = "https://%s/_matrix/identity/api/v1/pubkey/isvalid" % (id_server,)
defer.returnValue((token, public_key, key_validity_url))
@defer.inlineCallbacks
def on_PUT(self, request, room_id, membership_action, txn_id):
try:

View file

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Copyright 2015 OpenMarket 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.
from twisted.internet import defer
from synapse.api.errors import AuthError
class ThirdPartyInvites(object):
INVITE_KEYS = {"id_server", "medium", "address", "display_name"}
JOIN_KEYS = {
"token",
"public_key",
"key_validity_url",
"signature",
"sender",
}
@classmethod
def has_invite_keys(cls, content):
for key in cls.INVITE_KEYS:
if key not in content:
return False
return True
@classmethod
def has_join_keys(cls, content):
for key in cls.JOIN_KEYS:
if key not in content:
return False
return True
@classmethod
def copy_join_keys(cls, src, dst):
for key in cls.JOIN_KEYS:
if key in src:
dst[key] = src[key]
@classmethod
@defer.inlineCallbacks
def check_key_valid(cls, http_client, event):
try:
response = yield http_client.get_json(
event.content["key_validity_url"],
{"public_key": event.content["public_key"]}
)
if not response["valid"]:
raise AuthError(403, "Third party certificate was invalid")
except IOError:
raise AuthError(403, "Third party certificate could not be checked")