From d98660a60daaf1cc8d83cb2d64daa5f20a34139c Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Mar 2015 14:20:28 +0000 Subject: [PATCH 01/47] Implement password changing (finally) along with a start on making client/server auth more general. --- synapse/handlers/__init__.py | 2 + synapse/handlers/auth.py | 109 +++++++++++++++++++++++ synapse/handlers/login.py | 49 ++-------- synapse/rest/client/v2_alpha/__init__.py | 4 +- synapse/rest/client/v2_alpha/_base.py | 12 +++ synapse/rest/client/v2_alpha/password.py | 76 ++++++++++++++++ synapse/storage/registration.py | 33 ++++++- 7 files changed, 236 insertions(+), 49 deletions(-) create mode 100644 synapse/handlers/auth.py create mode 100644 synapse/rest/client/v2_alpha/password.py diff --git a/synapse/handlers/__init__.py b/synapse/handlers/__init__.py index 8d345bf936..336ce15701 100644 --- a/synapse/handlers/__init__.py +++ b/synapse/handlers/__init__.py @@ -29,6 +29,7 @@ from .typing import TypingNotificationHandler from .admin import AdminHandler from .appservice import ApplicationServicesHandler from .sync import SyncHandler +from .auth import AuthHandler class Handlers(object): @@ -58,3 +59,4 @@ class Handlers(object): hs, ApplicationServiceApi(hs) ) self.sync_handler = SyncHandler(hs) + self.auth_handler = AuthHandler(hs) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py new file mode 100644 index 0000000000..e4a73da9a7 --- /dev/null +++ b/synapse/handlers/auth.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# Copyright 2014, 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 ._base import BaseHandler +from synapse.api.constants import LoginType +from synapse.types import UserID +from synapse.api.errors import LoginError, Codes + +import logging +import bcrypt + + +logger = logging.getLogger(__name__) + + +class AuthHandler(BaseHandler): + + def __init__(self, hs): + super(AuthHandler, self).__init__(hs) + + @defer.inlineCallbacks + def check_auth(self, flows, clientdict): + """ + Takes a dictionary sent by the client in the login / registration + protocol and handles the login flow. + + Args: + flows: list of list of stages + authdict: The dictionary from the client root level, not the + 'auth' key: this method prompts for auth if none is sent. + Returns: + A tuple of authed, dict where authed is true if the client + has successfully completed an auth flow. If it is true, the dict + contains the authenticated credentials of each stage. + If authed is false, the dictionary is the server response to the + login request and should be passed back to the client. + """ + types = { + LoginType.PASSWORD: self.check_password_auth + } + + if 'auth' not in clientdict: + defer.returnValue((False, auth_dict_for_flows(flows))) + + authdict = clientdict['auth'] + + # In future: support sessions & retrieve previously succeeded + # login types + creds = {} + + # check auth type currently being presented + if 'type' not in authdict: + raise LoginError(400, "", Codes.MISSING_PARAM) + if authdict['type'] not in types: + raise LoginError(400, "", Codes.UNRECOGNIZED) + result = yield types[authdict['type']](authdict) + if result: + creds[authdict['type']] = result + + for f in flows: + if len(set(f) - set(creds.keys())) == 0: + logger.info("Auth completed with creds: %r", creds) + defer.returnValue((True, creds)) + + ret = auth_dict_for_flows(flows) + ret['completed'] = creds.keys() + defer.returnValue((False, ret)) + + @defer.inlineCallbacks + def check_password_auth(self, authdict): + if "user" not in authdict or "password" not in authdict: + raise LoginError(400, "", Codes.MISSING_PARAM) + + user = authdict["user"] + password = authdict["password"] + if not user.startswith('@'): + user = UserID.create(user, self.hs.hostname).to_string() + + user_info = yield self.store.get_user_by_id(user_id=user) + if not user_info: + logger.warn("Attempted to login as %s but they do not exist", user) + raise LoginError(403, "", errcode=Codes.FORBIDDEN) + + stored_hash = user_info[0]["password_hash"] + if bcrypt.checkpw(password, stored_hash): + defer.returnValue(user) + else: + logger.warn("Failed password login for user %s", user) + raise LoginError(403, "", errcode=Codes.FORBIDDEN) + + +def auth_dict_for_flows(flows): + return { + "flows": {"stages": f for f in flows} + } diff --git a/synapse/handlers/login.py b/synapse/handlers/login.py index 7447800460..19b560d91e 100644 --- a/synapse/handlers/login.py +++ b/synapse/handlers/login.py @@ -69,48 +69,9 @@ class LoginHandler(BaseHandler): raise LoginError(403, "", errcode=Codes.FORBIDDEN) @defer.inlineCallbacks - def reset_password(self, user_id, email): - is_valid = yield self._check_valid_association(user_id, email) - logger.info("reset_password user=%s email=%s valid=%s", user_id, email, - is_valid) - if is_valid: - try: - # send an email out - emailutils.send_email( - smtp_server=self.hs.config.email_smtp_server, - from_addr=self.hs.config.email_from_address, - to_addr=email, - subject="Password Reset", - body="TODO." - ) - except EmailException as e: - logger.exception(e) + def set_password(self, user_id, newpassword, token_id=None): + password_hash = bcrypt.hashpw(newpassword, bcrypt.gensalt()) - @defer.inlineCallbacks - def _check_valid_association(self, user_id, email): - identity = yield self._query_email(email) - if identity and "mxid" in identity: - if identity["mxid"] == user_id: - defer.returnValue(True) - return - defer.returnValue(False) - - @defer.inlineCallbacks - def _query_email(self, email): - http_client = SimpleHttpClient(self.hs) - try: - data = yield http_client.get_json( - # TODO FIXME This should be configurable. - # XXX: ID servers need to use HTTPS - "http://%s%s" % ( - "matrix.org:8090", "/_matrix/identity/api/v1/lookup" - ), - { - 'medium': 'email', - 'address': email - } - ) - defer.returnValue(data) - except CodeMessageException as e: - data = json.loads(e.msg) - defer.returnValue(data) + yield self.store.user_set_password_hash(user_id, password_hash) + yield self.store.user_delete_access_tokens_apart_from(user_id, token_id) + yield self.store.flush_user(user_id) diff --git a/synapse/rest/client/v2_alpha/__init__.py b/synapse/rest/client/v2_alpha/__init__.py index bca65f2a6a..041f538e20 100644 --- a/synapse/rest/client/v2_alpha/__init__.py +++ b/synapse/rest/client/v2_alpha/__init__.py @@ -15,7 +15,8 @@ from . import ( sync, - filter + filter, + password ) from synapse.http.server import JsonResource @@ -32,3 +33,4 @@ class ClientV2AlphaRestResource(JsonResource): def register_servlets(client_resource, hs): sync.register_servlets(hs, client_resource) filter.register_servlets(hs, client_resource) + password.register_servlets(hs, client_resource) diff --git a/synapse/rest/client/v2_alpha/_base.py b/synapse/rest/client/v2_alpha/_base.py index 22dc5cb862..c772cc986f 100644 --- a/synapse/rest/client/v2_alpha/_base.py +++ b/synapse/rest/client/v2_alpha/_base.py @@ -17,9 +17,11 @@ """ from synapse.api.urls import CLIENT_V2_ALPHA_PREFIX +from synapse.api.errors import SynapseError import re import logging +import simplejson logger = logging.getLogger(__name__) @@ -36,3 +38,13 @@ def client_v2_pattern(path_regex): SRE_Pattern """ return re.compile("^" + CLIENT_V2_ALPHA_PREFIX + path_regex) + + +def parse_json_dict_from_request(request): + try: + content = simplejson.loads(request.content.read()) + if type(content) != dict: + raise SynapseError(400, "Content must be a JSON object.") + return content + except simplejson.JSONDecodeError: + raise SynapseError(400, "Content not JSON.") diff --git a/synapse/rest/client/v2_alpha/password.py b/synapse/rest/client/v2_alpha/password.py new file mode 100644 index 0000000000..3663781c95 --- /dev/null +++ b/synapse/rest/client/v2_alpha/password.py @@ -0,0 +1,76 @@ +# -*- 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.constants import LoginType +from synapse.api.errors import LoginError, SynapseError, Codes +from synapse.http.servlet import RestServlet + +from ._base import client_v2_pattern, parse_json_dict_from_request + +import simplejson as json +import logging + + +logger = logging.getLogger(__name__) + + +class PasswordRestServlet(RestServlet): + PATTERN = client_v2_pattern("/account/password") + + def __init__(self, hs): + super(PasswordRestServlet, self).__init__() + self.hs = hs + self.auth = hs.get_auth() + self.auth_handler = hs.get_handlers().auth_handler + self.login_handler = hs.get_handlers().login_handler + + @defer.inlineCallbacks + def on_POST(self, request): + body = parse_json_dict_from_request(request) + + authed, result = yield self.auth_handler.check_auth([ + [LoginType.PASSWORD] + ], body) + + if not authed: + defer.returnValue((401, result)) + + auth_user = None + + if LoginType.PASSWORD in result: + # if using password, they should also be logged in + auth_user, client = yield self.auth.get_user_by_req(request) + if auth_user.to_string() != result[LoginType.PASSWORD]: + raise LoginError(400, "", Codes.UNKNOWN) + else: + logger.error("Auth succeeded but no known type!", result.keys()) + raise SynapseError(500, "", Codes.UNKNOWN) + + user_id = auth_user.to_string() + + if 'new_password' not in body: + raise SynapseError(400, "", Codes.MISSING_PARAM) + new_password = body['new_password'] + + self.login_handler.set_password( + user_id, new_password, client.token_id + ) + + defer.returnValue((200, {})) + +def register_servlets(hs, http_server): + PasswordRestServlet(hs).register(http_server) \ No newline at end of file diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index f24154f146..7e60dc3951 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -95,11 +95,36 @@ class RegistrationStore(SQLBaseStore): "get_user_by_id", self.cursor_to_dict, query, user_id ) + def user_set_password_hash(self, user_id, password_hash): + """ + NB. This does *not* evict any cache because the one use for this + removes most of the entries subsequently anyway so it would be + pointless. Use flush_user separately. + """ + return self._simple_update_one('users', { + 'name': user_id + }, { + 'password_hash': password_hash + }) + + def user_delete_access_tokens_apart_from(self, user_id, token_id): + return self._execute( + "delete_access_tokens_apart_from", None, + "DELETE FROM access_tokens WHERE user_id = ? AND id != ?", + user_id, token_id + ) + + @defer.inlineCallbacks + def flush_user(self, user_id): + rows = yield self._execute( + 'user_delete_access_tokens_apart_from', None, + "SELECT token FROM access_tokens WHERE user_id = ?", + user_id + ) + for r in rows: + self.get_user_by_token.invalidate(r) + @cached() - # TODO(paul): Currently there's no code to invalidate this cache. That - # means if/when we ever add internal ways to invalidate access tokens or - # change whether a user is a server admin, those will need to invoke - # store.get_user_by_token.invalidate(token) def get_user_by_token(self, token): """Get a user from the given access token. From 78adccfaf497dcb75451adfc5d366d5ff26cad52 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 23 Mar 2015 14:23:51 +0000 Subject: [PATCH 02/47] pep8 / pyflakes --- synapse/handlers/login.py | 6 +----- synapse/rest/client/v2_alpha/password.py | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/synapse/handlers/login.py b/synapse/handlers/login.py index 19b560d91e..7aff2e69e6 100644 --- a/synapse/handlers/login.py +++ b/synapse/handlers/login.py @@ -16,13 +16,9 @@ from twisted.internet import defer from ._base import BaseHandler -from synapse.api.errors import LoginError, Codes, CodeMessageException -from synapse.http.client import SimpleHttpClient -from synapse.util.emailutils import EmailException -import synapse.util.emailutils as emailutils +from synapse.api.errors import LoginError, Codes import bcrypt -import json import logging logger = logging.getLogger(__name__) diff --git a/synapse/rest/client/v2_alpha/password.py b/synapse/rest/client/v2_alpha/password.py index 3663781c95..1277532110 100644 --- a/synapse/rest/client/v2_alpha/password.py +++ b/synapse/rest/client/v2_alpha/password.py @@ -21,7 +21,6 @@ from synapse.http.servlet import RestServlet from ._base import client_v2_pattern, parse_json_dict_from_request -import simplejson as json import logging @@ -72,5 +71,6 @@ class PasswordRestServlet(RestServlet): defer.returnValue((200, {})) + def register_servlets(hs, http_server): - PasswordRestServlet(hs).register(http_server) \ No newline at end of file + PasswordRestServlet(hs).register(http_server) From d19e79ecc956e5ba7ed6b6fd37e80ec6a737b048 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Mar 2015 15:33:48 +0000 Subject: [PATCH 03/47] Make deleting other access tokens when you change your password actually work --- synapse/rest/client/v2_alpha/password.py | 5 ++++- synapse/storage/registration.py | 16 +++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/synapse/rest/client/v2_alpha/password.py b/synapse/rest/client/v2_alpha/password.py index 1277532110..85954c71cd 100644 --- a/synapse/rest/client/v2_alpha/password.py +++ b/synapse/rest/client/v2_alpha/password.py @@ -65,12 +65,15 @@ class PasswordRestServlet(RestServlet): raise SynapseError(400, "", Codes.MISSING_PARAM) new_password = body['new_password'] - self.login_handler.set_password( + yield self.login_handler.set_password( user_id, new_password, client.token_id ) defer.returnValue((200, {})) + def on_OPTIONS(self, _): + return 200, {} + def register_servlets(hs, http_server): PasswordRestServlet(hs).register(http_server) diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index 7e60dc3951..0364d10858 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -89,35 +89,41 @@ class RegistrationStore(SQLBaseStore): "VALUES (?,?)", [txn.lastrowid, token]) def get_user_by_id(self, user_id): - query = ("SELECT users.name, users.password_hash FROM users" + query = ("SELECT users.id, users.name, users.password_hash FROM users" " WHERE users.name = ?") return self._execute( "get_user_by_id", self.cursor_to_dict, query, user_id ) + @defer.inlineCallbacks def user_set_password_hash(self, user_id, password_hash): """ NB. This does *not* evict any cache because the one use for this removes most of the entries subsequently anyway so it would be pointless. Use flush_user separately. """ - return self._simple_update_one('users', { + yield self._simple_update_one('users', { 'name': user_id }, { 'password_hash': password_hash }) + @defer.inlineCallbacks def user_delete_access_tokens_apart_from(self, user_id, token_id): - return self._execute( + rows = yield self.get_user_by_id(user_id) + if len(rows) == 0: + raise Exception("No such user!") + + yield self._execute( "delete_access_tokens_apart_from", None, "DELETE FROM access_tokens WHERE user_id = ? AND id != ?", - user_id, token_id + rows[0]['id'], token_id ) @defer.inlineCallbacks def flush_user(self, user_id): rows = yield self._execute( - 'user_delete_access_tokens_apart_from', None, + 'flush_user', None, "SELECT token FROM access_tokens WHERE user_id = ?", user_id ) From c7023f21555a0adf0d8bb5040c817a8198bbf5a8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Mar 2015 17:24:15 +0000 Subject: [PATCH 04/47] 1) Pushers are now associated with an access token 2) Change places where we mean unauthenticated to 401, not 403, in C/S v2: hack so it stays as 403 in v1 because web client relies on it. --- synapse/api/auth.py | 43 ++++++++++++++----------- synapse/push/pusherpool.py | 9 +++--- synapse/rest/client/v1/base.py | 2 +- synapse/rest/client/v1/pusher.py | 3 +- synapse/server.py | 10 ++++++ synapse/storage/pusher.py | 3 +- synapse/storage/registration.py | 2 +- synapse/storage/schema/delta/15/v15.sql | 2 ++ 8 files changed, 48 insertions(+), 26 deletions(-) create mode 100644 synapse/storage/schema/delta/15/v15.sql diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 64f605b962..d08faf23f1 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -40,6 +40,7 @@ class Auth(object): self.hs = hs self.store = hs.get_datastore() self.state = hs.get_state_handler() + self.TOKEN_NOT_FOUND_HTTP_STATUS = 401 def check(self, event, auth_events): """ Checks if this event is correctly authed. @@ -373,7 +374,9 @@ class Auth(object): defer.returnValue((user, ClientInfo(device_id, token_id))) except KeyError: - raise AuthError(403, "Missing access token.") + raise AuthError( + self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token." + ) @defer.inlineCallbacks def get_user_by_token(self, token): @@ -387,21 +390,20 @@ class Auth(object): Raises: AuthError if no user by that token exists or the token is invalid. """ - try: - ret = yield self.store.get_user_by_token(token) - if not ret: - raise StoreError(400, "Unknown token") - user_info = { - "admin": bool(ret.get("admin", False)), - "device_id": ret.get("device_id"), - "user": UserID.from_string(ret.get("name")), - "token_id": ret.get("token_id", None), - } + ret = yield self.store.get_user_by_token(token) + if not ret: + raise AuthError( + self.TOKEN_NOT_FOUND_HTTP_STATUS, "Unrecognised access token.", + errcode=Codes.UNKNOWN_TOKEN + ) + user_info = { + "admin": bool(ret.get("admin", False)), + "device_id": ret.get("device_id"), + "user": UserID.from_string(ret.get("name")), + "token_id": ret.get("token_id", None), + } - defer.returnValue(user_info) - except StoreError: - raise AuthError(403, "Unrecognised access token.", - errcode=Codes.UNKNOWN_TOKEN) + defer.returnValue(user_info) @defer.inlineCallbacks def get_appservice_by_req(self, request): @@ -409,11 +411,16 @@ class Auth(object): token = request.args["access_token"][0] service = yield self.store.get_app_service_by_token(token) if not service: - raise AuthError(403, "Unrecognised access token.", - errcode=Codes.UNKNOWN_TOKEN) + raise AuthError( + self.TOKEN_NOT_FOUND_HTTP_STATUS, + "Unrecognised access token.", + errcode=Codes.UNKNOWN_TOKEN + ) defer.returnValue(service) except KeyError: - raise AuthError(403, "Missing access token.") + raise AuthError( + self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token." + ) def is_server_admin(self, user): return self.store.is_server_admin(user) diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 90babd7224..f75eebf8bf 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -57,7 +57,7 @@ class PusherPool: self._start_pushers(pushers) @defer.inlineCallbacks - def add_pusher(self, user_name, profile_tag, kind, app_id, + def add_pusher(self, user_name, access_token, profile_tag, kind, app_id, app_display_name, device_display_name, pushkey, lang, data): # we try to create the pusher just to validate the config: it # will then get pulled out of the database, @@ -79,17 +79,18 @@ class PusherPool: "failing_since": None }) yield self._add_pusher_to_store( - user_name, profile_tag, kind, app_id, + user_name, access_token, profile_tag, kind, app_id, app_display_name, device_display_name, pushkey, lang, data ) @defer.inlineCallbacks - def _add_pusher_to_store(self, user_name, profile_tag, kind, app_id, - app_display_name, device_display_name, + def _add_pusher_to_store(self, user_name, access_token, profile_tag, kind, + app_id, app_display_name, device_display_name, pushkey, lang, data): yield self.store.add_pusher( user_name=user_name, + access_token=access_token, profile_tag=profile_tag, kind=kind, app_id=app_id, diff --git a/synapse/rest/client/v1/base.py b/synapse/rest/client/v1/base.py index 72332bdb10..504a5e432f 100644 --- a/synapse/rest/client/v1/base.py +++ b/synapse/rest/client/v1/base.py @@ -48,5 +48,5 @@ class ClientV1RestServlet(RestServlet): self.hs = hs self.handlers = hs.get_handlers() self.builder_factory = hs.get_event_builder_factory() - self.auth = hs.get_auth() + self.auth = hs.get_v1auth() self.txns = HttpTransactionStore() diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/v1/pusher.py index 6045e86f34..87e89c9305 100644 --- a/synapse/rest/client/v1/pusher.py +++ b/synapse/rest/client/v1/pusher.py @@ -27,7 +27,7 @@ class PusherRestServlet(ClientV1RestServlet): @defer.inlineCallbacks def on_POST(self, request): - user, _ = yield self.auth.get_user_by_req(request) + user, client = yield self.auth.get_user_by_req(request) content = _parse_json(request) @@ -54,6 +54,7 @@ class PusherRestServlet(ClientV1RestServlet): try: yield pusher_pool.add_pusher( user_name=user.to_string(), + access_token=client.token_id, profile_tag=content['profile_tag'], kind=content['kind'], app_id=content['app_id'], diff --git a/synapse/server.py b/synapse/server.py index c7772244ba..4c4f6ca239 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -65,6 +65,7 @@ class BaseHomeServer(object): 'replication_layer', 'datastore', 'handlers', + 'v1auth', 'auth', 'rest_servlet_factory', 'state_handler', @@ -182,6 +183,15 @@ class HomeServer(BaseHomeServer): def build_auth(self): return Auth(self) + def build_v1auth(self): + orf = Auth(self) + # Matrix spec makes no reference to what HTTP status code is returned, + # but the V1 API uses 403 where it means 401, and the webclient + # relies on this behaviour, so V1 gets its own copy of the auth + # with backwards compat behaviour. + orf.TOKEN_NOT_FOUND_HTTP_STATUS = 403 + return orf + def build_state_handler(self): return StateHandler(self) diff --git a/synapse/storage/pusher.py b/synapse/storage/pusher.py index 000502b4ff..1ef8e06ac6 100644 --- a/synapse/storage/pusher.py +++ b/synapse/storage/pusher.py @@ -95,7 +95,7 @@ class PusherStore(SQLBaseStore): defer.returnValue(ret) @defer.inlineCallbacks - def add_pusher(self, user_name, profile_tag, kind, app_id, + def add_pusher(self, user_name, access_token, profile_tag, kind, app_id, app_display_name, device_display_name, pushkey, pushkey_ts, lang, data): try: @@ -107,6 +107,7 @@ class PusherStore(SQLBaseStore): ), dict( user_name=user_name, + access_token=access_token, kind=kind, profile_tag=profile_tag, app_display_name=app_display_name, diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index 0364d10858..f61d8fdb6a 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -174,4 +174,4 @@ class RegistrationStore(SQLBaseStore): if rows: return rows[0] - raise StoreError(404, "Token not found.") + return None diff --git a/synapse/storage/schema/delta/15/v15.sql b/synapse/storage/schema/delta/15/v15.sql new file mode 100644 index 0000000000..fc3e436877 --- /dev/null +++ b/synapse/storage/schema/delta/15/v15.sql @@ -0,0 +1,2 @@ +ALTER TABLE pushers ADD COLUMN access_token INTEGER DEFAULT NULL; + From 9aa0224cdf6ae9243903090d0a264e684f557da2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Mar 2015 17:25:59 +0000 Subject: [PATCH 05/47] unused import --- synapse/api/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/api/auth.py b/synapse/api/auth.py index d08faf23f1..0bf35109cd 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -18,7 +18,7 @@ from twisted.internet import defer from synapse.api.constants import EventTypes, Membership, JoinRules -from synapse.api.errors import AuthError, StoreError, Codes, SynapseError +from synapse.api.errors import AuthError, Codes, SynapseError from synapse.util.logutils import log_function from synapse.util.async import run_on_reactor from synapse.types import UserID, ClientInfo From 438a21c87bec6386be1a9ed92d61b1b467a40bcd Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Mar 2015 18:21:54 +0000 Subject: [PATCH 06/47] Don't test exact equality of the list: as long as it has the fields we expect, that's just fine. I added the user_id (as in database pkey) and it broke: no point testing what that comes out as: it's determined by the db. --- tests/storage/test_registration.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/storage/test_registration.py b/tests/storage/test_registration.py index e0b81f2b57..93be02f8c3 100644 --- a/tests/storage/test_registration.py +++ b/tests/storage/test_registration.py @@ -38,13 +38,12 @@ class RegistrationStoreTestCase(unittest.TestCase): @defer.inlineCallbacks def test_register(self): yield self.store.register(self.user_id, self.tokens[0], self.pwhash) + u = yield self.store.get_user_by_id(self.user_id)[0] - self.assertEquals( - # TODO(paul): Surely this field should be 'user_id', not 'name' - # Additionally surely it shouldn't come in a 1-element list - [{"name": self.user_id, "password_hash": self.pwhash}], - (yield self.store.get_user_by_id(self.user_id)) - ) + # TODO(paul): Surely this field should be 'user_id', not 'name' + # Additionally surely it shouldn't come in a 1-element list + self.assertEquals(self.user_id, u['name']) + self.assertEquals(self.pwhash, u['password_hash']) self.assertEquals( {"admin": 0, From ce2766d19c985d0100cc143477100c2723be1844 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Mar 2015 18:56:51 +0000 Subject: [PATCH 07/47] Fix tests --- tests/rest/client/v1/test_presence.py | 6 +++--- tests/rest/client/v1/test_profile.py | 2 +- tests/rest/client/v1/test_rooms.py | 16 ++++++++-------- tests/rest/client/v1/test_typing.py | 2 +- tests/storage/test_registration.py | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/rest/client/v1/test_presence.py b/tests/rest/client/v1/test_presence.py index b9c03383a2..8e0c5fa630 100644 --- a/tests/rest/client/v1/test_presence.py +++ b/tests/rest/client/v1/test_presence.py @@ -75,7 +75,7 @@ class PresenceStateTestCase(unittest.TestCase): "token_id": 1, } - hs.get_auth().get_user_by_token = _get_user_by_token + hs.get_v1auth().get_user_by_token = _get_user_by_token room_member_handler = hs.handlers.room_member_handler = Mock( spec=[ @@ -170,7 +170,7 @@ class PresenceListTestCase(unittest.TestCase): ] ) - hs.get_auth().get_user_by_token = _get_user_by_token + hs.get_v1auth().get_user_by_token = _get_user_by_token presence.register_servlets(hs, self.mock_resource) @@ -277,7 +277,7 @@ class PresenceEventStreamTestCase(unittest.TestCase): def _get_user_by_req(req=None): return (UserID.from_string(myid), "") - hs.get_auth().get_user_by_req = _get_user_by_req + hs.get_v1auth().get_user_by_req = _get_user_by_req presence.register_servlets(hs, self.mock_resource) events.register_servlets(hs, self.mock_resource) diff --git a/tests/rest/client/v1/test_profile.py b/tests/rest/client/v1/test_profile.py index 5cd5767f2e..929e5e5dd4 100644 --- a/tests/rest/client/v1/test_profile.py +++ b/tests/rest/client/v1/test_profile.py @@ -55,7 +55,7 @@ class ProfileTestCase(unittest.TestCase): def _get_user_by_req(request=None): return (UserID.from_string(myid), "") - hs.get_auth().get_user_by_req = _get_user_by_req + hs.get_v1auth().get_user_by_req = _get_user_by_req hs.get_handlers().profile_handler = self.mock_handler diff --git a/tests/rest/client/v1/test_rooms.py b/tests/rest/client/v1/test_rooms.py index 72fb4576b1..c83348acf9 100644 --- a/tests/rest/client/v1/test_rooms.py +++ b/tests/rest/client/v1/test_rooms.py @@ -61,7 +61,7 @@ class RoomPermissionsTestCase(RestTestCase): "device_id": None, "token_id": 1, } - hs.get_auth().get_user_by_token = _get_user_by_token + hs.get_v1auth().get_user_by_token = _get_user_by_token def _insert_client_ip(*args, **kwargs): return defer.succeed(None) @@ -71,7 +71,7 @@ class RoomPermissionsTestCase(RestTestCase): synapse.rest.client.v1.room.register_servlets(hs, self.mock_resource) - self.auth = hs.get_auth() + self.auth = hs.get_v1auth() # create some rooms under the name rmcreator_id self.uncreated_rmid = "!aa:test" @@ -448,7 +448,7 @@ class RoomsMemberListTestCase(RestTestCase): "device_id": None, "token_id": 1, } - hs.get_auth().get_user_by_token = _get_user_by_token + hs.get_v1auth().get_user_by_token = _get_user_by_token def _insert_client_ip(*args, **kwargs): return defer.succeed(None) @@ -528,7 +528,7 @@ class RoomsCreateTestCase(RestTestCase): "device_id": None, "token_id": 1, } - hs.get_auth().get_user_by_token = _get_user_by_token + hs.get_v1auth().get_user_by_token = _get_user_by_token def _insert_client_ip(*args, **kwargs): return defer.succeed(None) @@ -622,7 +622,7 @@ class RoomTopicTestCase(RestTestCase): "token_id": 1, } - hs.get_auth().get_user_by_token = _get_user_by_token + hs.get_v1auth().get_user_by_token = _get_user_by_token def _insert_client_ip(*args, **kwargs): return defer.succeed(None) @@ -728,7 +728,7 @@ class RoomMemberStateTestCase(RestTestCase): "device_id": None, "token_id": 1, } - hs.get_auth().get_user_by_token = _get_user_by_token + hs.get_v1auth().get_user_by_token = _get_user_by_token def _insert_client_ip(*args, **kwargs): return defer.succeed(None) @@ -855,7 +855,7 @@ class RoomMessagesTestCase(RestTestCase): "device_id": None, "token_id": 1, } - hs.get_auth().get_user_by_token = _get_user_by_token + hs.get_v1auth().get_user_by_token = _get_user_by_token def _insert_client_ip(*args, **kwargs): return defer.succeed(None) @@ -952,7 +952,7 @@ class RoomInitialSyncTestCase(RestTestCase): "device_id": None, "token_id": 1, } - hs.get_auth().get_user_by_token = _get_user_by_token + hs.get_v1auth().get_user_by_token = _get_user_by_token def _insert_client_ip(*args, **kwargs): return defer.succeed(None) diff --git a/tests/rest/client/v1/test_typing.py b/tests/rest/client/v1/test_typing.py index 80f2ec9ddf..3442e6ede5 100644 --- a/tests/rest/client/v1/test_typing.py +++ b/tests/rest/client/v1/test_typing.py @@ -67,7 +67,7 @@ class RoomTypingTestCase(RestTestCase): "token_id": 1, } - hs.get_auth().get_user_by_token = _get_user_by_token + hs.get_v1auth().get_user_by_token = _get_user_by_token def _insert_client_ip(*args, **kwargs): return defer.succeed(None) diff --git a/tests/storage/test_registration.py b/tests/storage/test_registration.py index 93be02f8c3..2f8953f518 100644 --- a/tests/storage/test_registration.py +++ b/tests/storage/test_registration.py @@ -38,7 +38,7 @@ class RegistrationStoreTestCase(unittest.TestCase): @defer.inlineCallbacks def test_register(self): yield self.store.register(self.user_id, self.tokens[0], self.pwhash) - u = yield self.store.get_user_by_id(self.user_id)[0] + u = (yield self.store.get_user_by_id(self.user_id))[0] # TODO(paul): Surely this field should be 'user_id', not 'name' # Additionally surely it shouldn't come in a 1-element list From c1a256cc4c82ce746eae8e719b9aa2344fe66177 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 25 Mar 2015 19:06:22 +0000 Subject: [PATCH 08/47] Allow multiple pushers for a single app ID & pushkey, honouring the 'append' flag in the API. --- synapse/push/__init__.py | 10 ++++-- synapse/push/pusherpool.py | 47 +++++++++++++++++++------ synapse/rest/client/v1/pusher.py | 13 ++++++- synapse/storage/pusher.py | 27 +++++++------- synapse/storage/schema/delta/15/v15.sql | 27 ++++++++++++-- 5 files changed, 96 insertions(+), 28 deletions(-) diff --git a/synapse/push/__init__.py b/synapse/push/__init__.py index 0727f772a5..5575c847f9 100644 --- a/synapse/push/__init__.py +++ b/synapse/push/__init__.py @@ -253,7 +253,8 @@ class Pusher(object): self.user_name, config, timeout=0) self.last_token = chunk['end'] self.store.update_pusher_last_token( - self.app_id, self.pushkey, self.last_token) + self.app_id, self.pushkey, self.user_name, self.last_token + ) logger.info("Pusher %s for user %s starting from token %s", self.pushkey, self.user_name, self.last_token) @@ -314,7 +315,7 @@ class Pusher(object): pk ) yield self.hs.get_pusherpool().remove_pusher( - self.app_id, pk + self.app_id, pk, self.user_name ) if not self.alive: @@ -326,6 +327,7 @@ class Pusher(object): self.store.update_pusher_last_token_and_success( self.app_id, self.pushkey, + self.user_name, self.last_token, self.clock.time_msec() ) @@ -334,6 +336,7 @@ class Pusher(object): self.store.update_pusher_failing_since( self.app_id, self.pushkey, + self.user_name, self.failing_since) else: if not self.failing_since: @@ -341,6 +344,7 @@ class Pusher(object): self.store.update_pusher_failing_since( self.app_id, self.pushkey, + self.user_name, self.failing_since ) @@ -358,6 +362,7 @@ class Pusher(object): self.store.update_pusher_last_token( self.app_id, self.pushkey, + self.user_name, self.last_token ) @@ -365,6 +370,7 @@ class Pusher(object): self.store.update_pusher_failing_since( self.app_id, self.pushkey, + self.user_name, self.failing_since ) else: diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index f75eebf8bf..cda072839c 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -84,6 +84,21 @@ class PusherPool: pushkey, lang, data ) + @defer.inlineCallbacks + def remove_pushers_by_app_id_and_pushkey_not_user(self, app_id, pushkey, + not_user_id): + to_remove = yield self.store.get_pushers_by_app_id_and_pushkey( + app_id, pushkey + ) + for p in to_remove: + if p['user_name'] != not_user_id: + logger.info( + "Removing pusher for app id %s, pushkey %s, user %s", + app_id, pushkey, p['user_name'] + ) + self.remove_pusher(p['app_id'], p['pushkey'], p['user_name']) + + @defer.inlineCallbacks def _add_pusher_to_store(self, user_name, access_token, profile_tag, kind, app_id, app_display_name, device_display_name, @@ -101,7 +116,7 @@ class PusherPool: lang=lang, data=encode_canonical_json(data).decode("UTF-8"), ) - self._refresh_pusher((app_id, pushkey)) + self._refresh_pusher(app_id, pushkey, user_name) def _create_pusher(self, pusherdict): if pusherdict['kind'] == 'http': @@ -126,30 +141,42 @@ class PusherPool: ) @defer.inlineCallbacks - def _refresh_pusher(self, app_id_pushkey): - p = yield self.store.get_pushers_by_app_id_and_pushkey( - app_id_pushkey + def _refresh_pusher(self, app_id, pushkey, user_name): + resultlist = yield self.store.get_pushers_by_app_id_and_pushkey( + app_id, pushkey ) - p['data'] = json.loads(p['data']) + p = None + for r in resultlist: + if r['user_name'] == user_name: + p = r - self._start_pushers([p]) + if p: + p['data'] = json.loads(p['data']) + + self._start_pushers([p]) def _start_pushers(self, pushers): logger.info("Starting %d pushers", len(pushers)) for pusherdict in pushers: p = self._create_pusher(pusherdict) if p: - fullid = "%s:%s" % (pusherdict['app_id'], pusherdict['pushkey']) + fullid = "%s:%s:%s" % ( + pusherdict['app_id'], + pusherdict['pushkey'], + pusherdict['user_name'] + ) if fullid in self.pushers: self.pushers[fullid].stop() self.pushers[fullid] = p p.start() @defer.inlineCallbacks - def remove_pusher(self, app_id, pushkey): - fullid = "%s:%s" % (app_id, pushkey) + def remove_pusher(self, app_id, pushkey, user_name): + fullid = "%s:%s:%s" % (app_id, pushkey, user_name) if fullid in self.pushers: logger.info("Stopping pusher %s", fullid) self.pushers[fullid].stop() del self.pushers[fullid] - yield self.store.delete_pusher_by_app_id_pushkey(app_id, pushkey) + yield self.store.delete_pusher_by_app_id_pushkey_user_name( + app_id, pushkey, user_name + ) diff --git a/synapse/rest/client/v1/pusher.py b/synapse/rest/client/v1/pusher.py index 87e89c9305..c83287c028 100644 --- a/synapse/rest/client/v1/pusher.py +++ b/synapse/rest/client/v1/pusher.py @@ -37,7 +37,7 @@ class PusherRestServlet(ClientV1RestServlet): and 'kind' in content and content['kind'] is None): yield pusher_pool.remove_pusher( - content['app_id'], content['pushkey'] + content['app_id'], content['pushkey'], user_name=user.to_string() ) defer.returnValue((200, {})) @@ -51,6 +51,17 @@ class PusherRestServlet(ClientV1RestServlet): raise SynapseError(400, "Missing parameters: "+','.join(missing), errcode=Codes.MISSING_PARAM) + append = False + if 'append' in content: + append = content['append'] + + if not append: + yield pusher_pool.remove_pushers_by_app_id_and_pushkey_not_user( + app_id=content['app_id'], + pushkey=content['pushkey'], + not_user_id=user.to_string() + ) + try: yield pusher_pool.add_pusher( user_name=user.to_string(), diff --git a/synapse/storage/pusher.py b/synapse/storage/pusher.py index 1ef8e06ac6..423878c6a0 100644 --- a/synapse/storage/pusher.py +++ b/synapse/storage/pusher.py @@ -27,7 +27,7 @@ logger = logging.getLogger(__name__) class PusherStore(SQLBaseStore): @defer.inlineCallbacks - def get_pushers_by_app_id_and_pushkey(self, app_id_and_pushkey): + def get_pushers_by_app_id_and_pushkey(self, app_id, pushkey): sql = ( "SELECT id, user_name, kind, profile_tag, app_id," "app_display_name, device_display_name, pushkey, ts, data, " @@ -38,7 +38,7 @@ class PusherStore(SQLBaseStore): rows = yield self._execute( "get_pushers_by_app_id_and_pushkey", None, sql, - app_id_and_pushkey[0], app_id_and_pushkey[1] + app_id, pushkey ) ret = [ @@ -60,7 +60,7 @@ class PusherStore(SQLBaseStore): for r in rows ] - defer.returnValue(ret[0]) + defer.returnValue(ret) @defer.inlineCallbacks def get_all_pushers(self): @@ -104,9 +104,9 @@ class PusherStore(SQLBaseStore): dict( app_id=app_id, pushkey=pushkey, + user_name=user_name, ), dict( - user_name=user_name, access_token=access_token, kind=kind, profile_tag=profile_tag, @@ -123,37 +123,38 @@ class PusherStore(SQLBaseStore): raise StoreError(500, "Problem creating pusher.") @defer.inlineCallbacks - def delete_pusher_by_app_id_pushkey(self, app_id, pushkey): + def delete_pusher_by_app_id_pushkey_user_name(self, app_id, pushkey, user_name): yield self._simple_delete_one( PushersTable.table_name, - {"app_id": app_id, "pushkey": pushkey}, - desc="delete_pusher_by_app_id_pushkey", + {"app_id": app_id, "pushkey": pushkey, 'user_name': user_name}, + desc="delete_pusher_by_app_id_pushkey_user_name", ) @defer.inlineCallbacks - def update_pusher_last_token(self, app_id, pushkey, last_token): + def update_pusher_last_token(self, app_id, pushkey, user_name, last_token): yield self._simple_update_one( PushersTable.table_name, - {'app_id': app_id, 'pushkey': pushkey}, + {'app_id': app_id, 'pushkey': pushkey, 'user_name': user_name}, {'last_token': last_token}, desc="update_pusher_last_token", ) @defer.inlineCallbacks - def update_pusher_last_token_and_success(self, app_id, pushkey, + def update_pusher_last_token_and_success(self, app_id, pushkey, user_name, last_token, last_success): yield self._simple_update_one( PushersTable.table_name, - {'app_id': app_id, 'pushkey': pushkey}, + {'app_id': app_id, 'pushkey': pushkey, 'user_name': user_name}, {'last_token': last_token, 'last_success': last_success}, desc="update_pusher_last_token_and_success", ) @defer.inlineCallbacks - def update_pusher_failing_since(self, app_id, pushkey, failing_since): + def update_pusher_failing_since(self, app_id, pushkey, user_name, + failing_since): yield self._simple_update_one( PushersTable.table_name, - {'app_id': app_id, 'pushkey': pushkey}, + {'app_id': app_id, 'pushkey': pushkey, 'user_name': user_name}, {'failing_since': failing_since}, desc="update_pusher_failing_since", ) diff --git a/synapse/storage/schema/delta/15/v15.sql b/synapse/storage/schema/delta/15/v15.sql index fc3e436877..f5b2a08ca4 100644 --- a/synapse/storage/schema/delta/15/v15.sql +++ b/synapse/storage/schema/delta/15/v15.sql @@ -1,2 +1,25 @@ -ALTER TABLE pushers ADD COLUMN access_token INTEGER DEFAULT NULL; - +-- Drop, copy & recreate pushers table to change unique key +-- Also add access_token column at the same time +CREATE TABLE IF NOT EXISTS pushers2 ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_name TEXT NOT NULL, + access_token INTEGER DEFAULT NULL, + profile_tag varchar(32) NOT NULL, + kind varchar(8) NOT NULL, + app_id varchar(64) NOT NULL, + app_display_name varchar(64) NOT NULL, + device_display_name varchar(128) NOT NULL, + pushkey blob NOT NULL, + ts BIGINT NOT NULL, + lang varchar(8), + data blob, + last_token TEXT, + last_success BIGINT, + failing_since BIGINT, + FOREIGN KEY(user_name) REFERENCES users(name), + UNIQUE (app_id, pushkey, user_name) +); +INSERT INTO pushers2 (id, user_name, profile_tag, kind, app_id, app_display_name, device_display_name, pushkey, ts, lang, data, last_token, last_success, failing_since) + SELECT id, user_name, profile_tag, kind, app_id, app_display_name, device_display_name, pushkey, ts, lang, data, last_token, last_success, failing_since FROM pushers; +DROP TABLE pushers; +ALTER TABLE pushers2 RENAME TO pushers; From df4c12c76285fa8d423f1545453356a008690155 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 25 Mar 2015 19:08:17 +0000 Subject: [PATCH 09/47] pep8 blank lines --- synapse/push/pusherpool.py | 1 - 1 file changed, 1 deletion(-) diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index cda072839c..46444157c9 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -98,7 +98,6 @@ class PusherPool: ) self.remove_pusher(p['app_id'], p['pushkey'], p['user_name']) - @defer.inlineCallbacks def _add_pusher_to_store(self, user_name, access_token, profile_tag, kind, app_id, app_display_name, device_display_name, From a32e876ef43df22cec37aad748c32c0cda30428a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 26 Mar 2015 13:40:16 +0000 Subject: [PATCH 10/47] Delete pushers when changing password --- synapse/handlers/login.py | 3 +++ synapse/push/pusherpool.py | 20 +++++++++++++++-- synapse/storage/pusher.py | 45 +++++++++----------------------------- 3 files changed, 31 insertions(+), 37 deletions(-) diff --git a/synapse/handlers/login.py b/synapse/handlers/login.py index 7aff2e69e6..04f6dbb95e 100644 --- a/synapse/handlers/login.py +++ b/synapse/handlers/login.py @@ -70,4 +70,7 @@ class LoginHandler(BaseHandler): yield self.store.user_set_password_hash(user_id, password_hash) yield self.store.user_delete_access_tokens_apart_from(user_id, token_id) + yield self.hs.get_pusherpool().remove_pushers_by_user_access_token( + user_id, token_id + ) yield self.store.flush_user(user_id) diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 46444157c9..0fdd7ea786 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -71,7 +71,7 @@ class PusherPool: "app_display_name": app_display_name, "device_display_name": device_display_name, "pushkey": pushkey, - "pushkey_ts": self.hs.get_clock().time_msec(), + "ts": self.hs.get_clock().time_msec(), "lang": lang, "data": data, "last_token": None, @@ -98,6 +98,22 @@ class PusherPool: ) self.remove_pusher(p['app_id'], p['pushkey'], p['user_name']) + @defer.inlineCallbacks + def remove_pushers_by_user_access_token(self, user_id, not_access_token_id): + all = yield self.store.get_all_pushers() + logger.info( + "Removing all pushers for user %s except access token %s", + user_id, not_access_token_id + ) + for p in all: + if (p['user_name'] == user_id and + p['access_token'] != not_access_token_id): + logger.info( + "Removing pusher for app id %s, pushkey %s, user %s", + p['app_id'], p['pushkey'], p['user_name'] + ) + self.remove_pusher(p['app_id'], p['pushkey'], p['user_name']) + @defer.inlineCallbacks def _add_pusher_to_store(self, user_name, access_token, profile_tag, kind, app_id, app_display_name, device_display_name, @@ -127,7 +143,7 @@ class PusherPool: app_display_name=pusherdict['app_display_name'], device_display_name=pusherdict['device_display_name'], pushkey=pusherdict['pushkey'], - pushkey_ts=pusherdict['pushkey_ts'], + pushkey_ts=pusherdict['ts'], data=pusherdict['data'], last_token=pusherdict['last_token'], last_success=pusherdict['last_success'], diff --git a/synapse/storage/pusher.py b/synapse/storage/pusher.py index 423878c6a0..1c657beddb 100644 --- a/synapse/storage/pusher.py +++ b/synapse/storage/pusher.py @@ -28,11 +28,9 @@ logger = logging.getLogger(__name__) class PusherStore(SQLBaseStore): @defer.inlineCallbacks def get_pushers_by_app_id_and_pushkey(self, app_id, pushkey): + cols = ",".join(PushersTable.fields) sql = ( - "SELECT id, user_name, kind, profile_tag, app_id," - "app_display_name, device_display_name, pushkey, ts, data, " - "last_token, last_success, failing_since " - "FROM pushers " + "SELECT "+cols+" FROM pushers " "WHERE app_id = ? AND pushkey = ?" ) @@ -43,51 +41,26 @@ class PusherStore(SQLBaseStore): ret = [ { - "id": r[0], - "user_name": r[1], - "kind": r[2], - "profile_tag": r[3], - "app_id": r[4], - "app_display_name": r[5], - "device_display_name": r[6], - "pushkey": r[7], - "pushkey_ts": r[8], - "data": r[9], - "last_token": r[10], - "last_success": r[11], - "failing_since": r[12] + k: r[i] for i, k in enumerate(PushersTable.fields) } for r in rows ] + print ret defer.returnValue(ret) @defer.inlineCallbacks def get_all_pushers(self): + cols = ",".join(PushersTable.fields) sql = ( - "SELECT id, user_name, kind, profile_tag, app_id," - "app_display_name, device_display_name, pushkey, ts, data, " - "last_token, last_success, failing_since " - "FROM pushers" + "SELECT "+cols+" FROM pushers" ) rows = yield self._execute("get_all_pushers", None, sql) ret = [ { - "id": r[0], - "user_name": r[1], - "kind": r[2], - "profile_tag": r[3], - "app_id": r[4], - "app_display_name": r[5], - "device_display_name": r[6], - "pushkey": r[7], - "pushkey_ts": r[8], - "data": r[9], - "last_token": r[10], - "last_success": r[11], - "failing_since": r[12] + k: r[i] for i, k in enumerate(PushersTable.fields) } for r in rows ] @@ -166,13 +139,15 @@ class PushersTable(Table): fields = [ "id", "user_name", + "access_token", "kind", "profile_tag", "app_id", "app_display_name", "device_display_name", "pushkey", - "pushkey_ts", + "ts", + "lang", "data", "last_token", "last_success", From 6f4f7e4e22522255aca72dc2fe28a636adef481a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 26 Mar 2015 14:12:06 +0000 Subject: [PATCH 11/47] pep8 --- synapse/push/pusherpool.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 0fdd7ea786..041ce8f22a 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -106,8 +106,10 @@ class PusherPool: user_id, not_access_token_id ) for p in all: - if (p['user_name'] == user_id and - p['access_token'] != not_access_token_id): + if ( + p['user_name'] == user_id and + p['access_token'] != not_access_token_id + ): logger.info( "Removing pusher for app id %s, pushkey %s, user %s", p['app_id'], p['pushkey'], p['user_name'] From 59bf16eddcb793705ee6bc243a2158824f7e05c8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 30 Mar 2015 18:13:10 +0100 Subject: [PATCH 12/47] New registration for C/S API v2. Only ReCAPTCHA working currently. --- synapse/api/constants.py | 2 + synapse/config/captcha.py | 7 +- synapse/handlers/auth.py | 90 ++++++++++++++++++++---- synapse/handlers/register.py | 11 ++- synapse/http/client.py | 2 + synapse/rest/client/v2_alpha/__init__.py | 4 +- synapse/rest/client/v2_alpha/_base.py | 6 ++ synapse/rest/client/v2_alpha/register.py | 86 ++++++++++++++++++++++ 8 files changed, 192 insertions(+), 16 deletions(-) create mode 100644 synapse/rest/client/v2_alpha/register.py diff --git a/synapse/api/constants.py b/synapse/api/constants.py index b16bf4247d..3e0ce170a4 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -62,6 +62,8 @@ class LoginType(object): APPLICATION_SERVICE = u"m.login.application_service" SHARED_SECRET = u"org.matrix.login.shared_secret" + HIDDEN_TYPES = [APPLICATION_SERVICE, SHARED_SECRET] + class EventTypes(object): Member = "m.room.member" diff --git a/synapse/config/captcha.py b/synapse/config/captcha.py index 7e21c7414d..07fbfadc0f 100644 --- a/synapse/config/captcha.py +++ b/synapse/config/captcha.py @@ -20,6 +20,7 @@ class CaptchaConfig(Config): def __init__(self, args): super(CaptchaConfig, self).__init__(args) self.recaptcha_private_key = args.recaptcha_private_key + self.recaptcha_public_key = args.recaptcha_public_key self.enable_registration_captcha = args.enable_registration_captcha self.captcha_ip_origin_is_x_forwarded = ( args.captcha_ip_origin_is_x_forwarded @@ -30,9 +31,13 @@ class CaptchaConfig(Config): def add_arguments(cls, parser): super(CaptchaConfig, cls).add_arguments(parser) group = parser.add_argument_group("recaptcha") + group.add_argument( + "--recaptcha-public-key", type=str, default="YOUR_PUBLIC_KEY", + help="This Home Server's ReCAPTCHA public key." + ) group.add_argument( "--recaptcha-private-key", type=str, default="YOUR_PRIVATE_KEY", - help="The matching private key for the web client's public key." + help="This Home Server's ReCAPTCHA private key." ) group.add_argument( "--enable-registration-captcha", type=bool, default=False, diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index e4a73da9a7..ec625f4ea8 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -19,9 +19,12 @@ from ._base import BaseHandler from synapse.api.constants import LoginType from synapse.types import UserID from synapse.api.errors import LoginError, Codes +from synapse.http.client import SimpleHttpClient +from twisted.web.client import PartialDownloadError import logging import bcrypt +import simplejson logger = logging.getLogger(__name__) @@ -33,7 +36,7 @@ class AuthHandler(BaseHandler): super(AuthHandler, self).__init__(hs) @defer.inlineCallbacks - def check_auth(self, flows, clientdict): + def check_auth(self, flows, clientdict, clientip=None): """ Takes a dictionary sent by the client in the login / registration protocol and handles the login flow. @@ -50,11 +53,12 @@ class AuthHandler(BaseHandler): login request and should be passed back to the client. """ types = { - LoginType.PASSWORD: self.check_password_auth + LoginType.PASSWORD: self.check_password_auth, + LoginType.RECAPTCHA: self.check_recaptcha, } - if 'auth' not in clientdict: - defer.returnValue((False, auth_dict_for_flows(flows))) + if not clientdict or 'auth' not in clientdict: + defer.returnValue((False, self.auth_dict_for_flows(flows))) authdict = clientdict['auth'] @@ -67,7 +71,7 @@ class AuthHandler(BaseHandler): raise LoginError(400, "", Codes.MISSING_PARAM) if authdict['type'] not in types: raise LoginError(400, "", Codes.UNRECOGNIZED) - result = yield types[authdict['type']](authdict) + result = yield types[authdict['type']](authdict, clientip) if result: creds[authdict['type']] = result @@ -76,12 +80,12 @@ class AuthHandler(BaseHandler): logger.info("Auth completed with creds: %r", creds) defer.returnValue((True, creds)) - ret = auth_dict_for_flows(flows) + ret = self.auth_dict_for_flows(flows) ret['completed'] = creds.keys() defer.returnValue((False, ret)) @defer.inlineCallbacks - def check_password_auth(self, authdict): + def check_password_auth(self, authdict, _): if "user" not in authdict or "password" not in authdict: raise LoginError(400, "", Codes.MISSING_PARAM) @@ -93,17 +97,77 @@ class AuthHandler(BaseHandler): user_info = yield self.store.get_user_by_id(user_id=user) if not user_info: logger.warn("Attempted to login as %s but they do not exist", user) - raise LoginError(403, "", errcode=Codes.FORBIDDEN) + raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) stored_hash = user_info[0]["password_hash"] if bcrypt.checkpw(password, stored_hash): defer.returnValue(user) else: logger.warn("Failed password login for user %s", user) - raise LoginError(403, "", errcode=Codes.FORBIDDEN) + raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) + @defer.inlineCallbacks + def check_recaptcha(self, authdict, clientip): + try: + user_response = authdict["response"] + except KeyError: + # Client tried to provide captcha but didn't give the parameter: + # bad request. + raise LoginError( + 400, "Captcha response is required", + errcode=Codes.CAPTCHA_NEEDED + ) -def auth_dict_for_flows(flows): - return { - "flows": {"stages": f for f in flows} - } + logger.info( + "Submitting recaptcha response %s with remoteip %s", + user_response, clientip + ) + + # TODO: get this from the homeserver rather than creating a new one for + # each request + try: + client = SimpleHttpClient(self.hs) + data = yield client.post_urlencoded_get_json( + "https://www.google.com/recaptcha/api/siteverify", + args={ + 'secret': self.hs.config.recaptcha_private_key, + 'response': user_response, + 'remoteip': clientip, + } + ) + except PartialDownloadError as pde: + # Twisted is silly + data = pde.response + resp_body = simplejson.loads(data) + if 'success' in resp_body and resp_body['success']: + defer.returnValue(True) + raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) + + def get_params_recaptcha(self): + return {"public_key": self.hs.config.recaptcha_public_key} + + def auth_dict_for_flows(self, flows): + public_flows = [] + for f in flows: + hidden = False + for stagetype in f: + if stagetype in LoginType.HIDDEN_TYPES: + hidden = True + if not hidden: + public_flows.append(f) + + get_params = { + LoginType.RECAPTCHA: self.get_params_recaptcha, + } + + params = {} + + for f in public_flows: + for stage in f: + if stage in get_params and stage not in params: + params[stage] = get_params[stage]() + + return { + "flows": [{"stages": f} for f in public_flows], + "params": params + } \ No newline at end of file diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index c25e321099..542759a827 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -157,7 +157,11 @@ class RegistrationHandler(BaseHandler): @defer.inlineCallbacks def check_recaptcha(self, ip, private_key, challenge, response): - """Checks a recaptcha is correct.""" + """ + Checks a recaptcha is correct. + + Used only by c/s api v1 + """ captcha_response = yield self._validate_captcha( ip, @@ -282,6 +286,8 @@ class RegistrationHandler(BaseHandler): def _validate_captcha(self, ip_addr, private_key, challenge, response): """Validates the captcha provided. + Used only by c/s api v1 + Returns: dict: Containing 'valid'(bool) and 'error_url'(str) if invalid. @@ -299,6 +305,9 @@ class RegistrationHandler(BaseHandler): @defer.inlineCallbacks def _submit_captcha(self, ip_addr, private_key, challenge, response): + """ + Used only by c/s api v1 + """ # TODO: get this from the homeserver rather than creating a new one for # each request client = CaptchaServerHttpClient(self.hs) diff --git a/synapse/http/client.py b/synapse/http/client.py index 2ae1c4d3a4..e8a5dedab4 100644 --- a/synapse/http/client.py +++ b/synapse/http/client.py @@ -200,6 +200,8 @@ class CaptchaServerHttpClient(SimpleHttpClient): """ Separate HTTP client for talking to google's captcha servers Only slightly special because accepts partial download responses + + used only by c/s api v1 """ @defer.inlineCallbacks diff --git a/synapse/rest/client/v2_alpha/__init__.py b/synapse/rest/client/v2_alpha/__init__.py index 041f538e20..98189ead26 100644 --- a/synapse/rest/client/v2_alpha/__init__.py +++ b/synapse/rest/client/v2_alpha/__init__.py @@ -16,7 +16,8 @@ from . import ( sync, filter, - password + password, + register ) from synapse.http.server import JsonResource @@ -34,3 +35,4 @@ class ClientV2AlphaRestResource(JsonResource): sync.register_servlets(hs, client_resource) filter.register_servlets(hs, client_resource) password.register_servlets(hs, client_resource) + register.register_servlets(hs, client_resource) diff --git a/synapse/rest/client/v2_alpha/_base.py b/synapse/rest/client/v2_alpha/_base.py index c772cc986f..db2c9b244a 100644 --- a/synapse/rest/client/v2_alpha/_base.py +++ b/synapse/rest/client/v2_alpha/_base.py @@ -40,6 +40,12 @@ def client_v2_pattern(path_regex): return re.compile("^" + CLIENT_V2_ALPHA_PREFIX + path_regex) +def parse_request_allow_empty(request): + content = request.content.read() + if content == None or content == '': + return None + return simplejson.loads(content) + def parse_json_dict_from_request(request): try: content = simplejson.loads(request.content.read()) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py new file mode 100644 index 0000000000..84da010c29 --- /dev/null +++ b/synapse/rest/client/v2_alpha/register.py @@ -0,0 +1,86 @@ +# -*- 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.constants import LoginType +from synapse.api.errors import LoginError, SynapseError, Codes +from synapse.http.servlet import RestServlet + +from ._base import client_v2_pattern, parse_request_allow_empty + +import logging + + +logger = logging.getLogger(__name__) + + +class RegisterRestServlet(RestServlet): + PATTERN = client_v2_pattern("/register") + + def __init__(self, hs): + super(RegisterRestServlet, self).__init__() + self.hs = hs + self.auth = hs.get_auth() + self.auth_handler = hs.get_handlers().auth_handler + self.registration_handler = hs.get_handlers().registration_handler + + @defer.inlineCallbacks + def on_POST(self, request): + body = parse_request_allow_empty(request) + + authed, result = yield self.auth_handler.check_auth([ + [LoginType.RECAPTCHA], + [LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA], + [LoginType.APPLICATION_SERVICE] + ], body) + + if not authed: + defer.returnValue((401, result)) + + is_application_server = LoginType.APPLICATION_SERVICE in result + is_using_shared_secret = LoginType.SHARED_SECRET in result + + can_register = ( + not self.hs.config.disable_registration + or is_application_server + or is_using_shared_secret + ) + if not can_register: + raise SynapseError(403, "Registration has been disabled") + + if 'username' not in body or 'password' not in body: + raise SynapseError(400, "", Codes.MISSING_PARAM) + desired_username = body['username'] + new_password = body['password'] + + (user_id, token) = yield self.registration_handler.register( + localpart=desired_username, + password=new_password + ) + result = { + "user_id": user_id, + "access_token": token, + "home_server": self.hs.hostname, + } + + defer.returnValue((200, result)) + + def on_OPTIONS(self, _): + return 200, {} + + +def register_servlets(hs, http_server): + RegisterRestServlet(hs).register(http_server) \ No newline at end of file From 46183cc69ff66af286c29c6f20a28086f93618a5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 30 Mar 2015 18:18:19 +0100 Subject: [PATCH 13/47] Add original, unmodified CAPTCHA-SETUP from the webclient repo before modifying (captcha setup is now purely on the HS). --- CAPTCHA_SETUP | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 CAPTCHA_SETUP diff --git a/CAPTCHA_SETUP b/CAPTCHA_SETUP new file mode 100644 index 0000000000..38089ce093 --- /dev/null +++ b/CAPTCHA_SETUP @@ -0,0 +1,46 @@ +Captcha can be enabled for this web client / home server. This file explains how to do that. +The captcha mechanism used is Google's ReCaptcha. This requires API keys from Google. + +Getting keys +------------ +Requires a public/private key pair from: + +https://developers.google.com/recaptcha/ + + +Setting Private ReCaptcha Key +----------------------------- +The private key is a config option on the home server config. If it is not +visible, you can generate it via --generate-config. Set the following value: + + recaptcha_private_key: YOUR_PRIVATE_KEY + +In addition, you MUST enable captchas via: + + enable_registration_captcha: true + +Setting Public ReCaptcha Key +---------------------------- +The web client will look for the global variable webClientConfig for config +options. You should put your ReCaptcha public key there like so: + +webClientConfig = { + useCaptcha: true, + recaptcha_public_key: "YOUR_PUBLIC_KEY" +}; + +This should be put in webclient/config.js which is already .gitignored, rather +than in the web client source files. You MUST set useCaptcha to true else a +ReCaptcha widget will not be generated. + +Configuring IP used for auth +---------------------------- +The ReCaptcha API requires that the IP address of the user who solved the +captcha is sent. If the client is connecting through a proxy or load balancer, +it may be required to use the X-Forwarded-For (XFF) header instead of the origin +IP address. This can be configured as an option on the home server like so: + + captcha_ip_origin_is_x_forwarded: true + + + From 4891c4ff72af2733627c4bfb29cc0fa5362ab617 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 30 Mar 2015 18:27:42 +0100 Subject: [PATCH 14/47] Update CAPTCHA_SETUP (it continues to ignore fallback, but I guess I should fix it so that doesn't need the key in two different places) --- CAPTCHA_SETUP | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/CAPTCHA_SETUP b/CAPTCHA_SETUP index 38089ce093..75ff80981b 100644 --- a/CAPTCHA_SETUP +++ b/CAPTCHA_SETUP @@ -1,4 +1,4 @@ -Captcha can be enabled for this web client / home server. This file explains how to do that. +Captcha can be enabled for this home server. This file explains how to do that. The captcha mechanism used is Google's ReCaptcha. This requires API keys from Google. Getting keys @@ -8,31 +8,18 @@ Requires a public/private key pair from: https://developers.google.com/recaptcha/ -Setting Private ReCaptcha Key ------------------------------ -The private key is a config option on the home server config. If it is not -visible, you can generate it via --generate-config. Set the following value: +Setting ReCaptcha Keys +---------------------- +The keys are a config option on the home server config. If they are not +visible, you can generate them via --generate-config. Set the following value: + recaptcha_public_key: YOUR_PUBLIC_KEY recaptcha_private_key: YOUR_PRIVATE_KEY In addition, you MUST enable captchas via: enable_registration_captcha: true -Setting Public ReCaptcha Key ----------------------------- -The web client will look for the global variable webClientConfig for config -options. You should put your ReCaptcha public key there like so: - -webClientConfig = { - useCaptcha: true, - recaptcha_public_key: "YOUR_PUBLIC_KEY" -}; - -This should be put in webclient/config.js which is already .gitignored, rather -than in the web client source files. You MUST set useCaptcha to true else a -ReCaptcha widget will not be generated. - Configuring IP used for auth ---------------------------- The ReCaptcha API requires that the IP address of the user who solved the @@ -42,5 +29,3 @@ IP address. This can be configured as an option on the home server like so: captcha_ip_origin_is_x_forwarded: true - - From 9f642a93ecab62fb56776ad4b7d7b062b869b66a Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 31 Mar 2015 09:50:44 +0100 Subject: [PATCH 15/47] pep8 --- synapse/handlers/auth.py | 2 +- synapse/rest/client/v2_alpha/_base.py | 3 ++- synapse/rest/client/v2_alpha/register.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index ec625f4ea8..26df9fcd86 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -170,4 +170,4 @@ class AuthHandler(BaseHandler): return { "flows": [{"stages": f} for f in public_flows], "params": params - } \ No newline at end of file + } diff --git a/synapse/rest/client/v2_alpha/_base.py b/synapse/rest/client/v2_alpha/_base.py index db2c9b244a..8adcc9dd95 100644 --- a/synapse/rest/client/v2_alpha/_base.py +++ b/synapse/rest/client/v2_alpha/_base.py @@ -42,10 +42,11 @@ def client_v2_pattern(path_regex): def parse_request_allow_empty(request): content = request.content.read() - if content == None or content == '': + if content is None or content == '': return None return simplejson.loads(content) + def parse_json_dict_from_request(request): try: content = simplejson.loads(request.content.read()) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 84da010c29..4a53e03743 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -16,7 +16,7 @@ from twisted.internet import defer from synapse.api.constants import LoginType -from synapse.api.errors import LoginError, SynapseError, Codes +from synapse.api.errors import SynapseError, Codes from synapse.http.servlet import RestServlet from ._base import client_v2_pattern, parse_request_allow_empty @@ -83,4 +83,4 @@ class RegisterRestServlet(RestServlet): def register_servlets(hs, http_server): - RegisterRestServlet(hs).register(http_server) \ No newline at end of file + RegisterRestServlet(hs).register(http_server) From d18e7779cae4610d9e1425e4f01681359a20d374 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 31 Mar 2015 14:40:02 +0100 Subject: [PATCH 16/47] Grammar and deduplication --- synapse/http/server.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/synapse/http/server.py b/synapse/http/server.py index dee49b9e18..b5c1a3cee2 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -57,10 +57,10 @@ class HttpServer(object): """ def register_path(self, method, path_pattern, callback): - """ Register a callback that get's fired if we receive a http request + """ Register a callback that gets fired if we receive a http request with the given method for a path that matches the given regex. - If the regex contains groups these get's passed to the calback via + If the regex contains groups these gets passed to the calback via an unpacked tuple. Args: @@ -111,9 +111,8 @@ class JsonResource(HttpServer, resource.Resource): interface=self.hs.config.bind_host ) - # Gets called by twisted def render(self, request): - """ This get's called by twisted every time someone sends us a request. + """ This gets called by twisted every time someone sends us a request. """ self._async_render_with_logging_context(request) return server.NOT_DONE_YET @@ -130,7 +129,7 @@ class JsonResource(HttpServer, resource.Resource): @defer.inlineCallbacks def _async_render(self, request): - """ This get's called by twisted every time someone sends us a request. + """ This gets called by twisted every time someone sends us a request. This checks if anyone has registered a callback for that method and path. """ From f129ee1e18e25c3392af2e9633988ae5b87bc6d5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 31 Mar 2015 18:25:10 +0100 Subject: [PATCH 17/47] Make docs a bit more true --- synapse/http/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/http/server.py b/synapse/http/server.py index b5c1a3cee2..30c3aa5cac 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -129,7 +129,7 @@ class JsonResource(HttpServer, resource.Resource): @defer.inlineCallbacks def _async_render(self, request): - """ This gets called by twisted every time someone sends us a request. + """ This gets called from render() every time someone sends us a request. This checks if anyone has registered a callback for that method and path. """ From e9c908ebc09ccc050bd09692c5413124a8c3c06e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 1 Apr 2015 15:05:30 +0100 Subject: [PATCH 18/47] Completely replace fallback auth for C/S V2: * Now only the auth part goes to fallback, not the whole operation * Auth fallback is a normal API endpoint, not a static page * Params like the recaptcha pubkey can just live in the config Involves a little engineering on JsonResource so its servlets aren't always forced to return JSON. I should document this more, in fact I'll do that now. --- static/client/register/style.css | 6 +- synapse/handlers/auth.py | 98 +++++++++--- synapse/http/server.py | 7 +- synapse/rest/client/v2_alpha/__init__.py | 4 +- synapse/rest/client/v2_alpha/auth.py | 189 +++++++++++++++++++++++ synapse/rest/client/v2_alpha/register.py | 2 +- 6 files changed, 280 insertions(+), 26 deletions(-) create mode 100644 synapse/rest/client/v2_alpha/auth.py diff --git a/static/client/register/style.css b/static/client/register/style.css index a3398852b9..5a7b6eebf2 100644 --- a/static/client/register/style.css +++ b/static/client/register/style.css @@ -37,9 +37,13 @@ textarea, input { margin: auto } +.g-recaptcha div { + margin: auto; +} + #registrationForm { text-align: left; - padding: 1em; + padding: 5px; margin-bottom: 40px; display: inline-block; diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 26df9fcd86..3d2461dd7d 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -20,12 +20,15 @@ from synapse.api.constants import LoginType from synapse.types import UserID from synapse.api.errors import LoginError, Codes from synapse.http.client import SimpleHttpClient + from twisted.web.client import PartialDownloadError import logging import bcrypt import simplejson +import synapse.util.stringutils as stringutils + logger = logging.getLogger(__name__) @@ -34,6 +37,11 @@ class AuthHandler(BaseHandler): def __init__(self, hs): super(AuthHandler, self).__init__(hs) + self.checkers = { + LoginType.PASSWORD: self._check_password_auth, + LoginType.RECAPTCHA: self._check_recaptcha, + } + self.sessions = {} @defer.inlineCallbacks def check_auth(self, flows, clientdict, clientip=None): @@ -52,40 +60,64 @@ class AuthHandler(BaseHandler): If authed is false, the dictionary is the server response to the login request and should be passed back to the client. """ - types = { - LoginType.PASSWORD: self.check_password_auth, - LoginType.RECAPTCHA: self.check_recaptcha, - } if not clientdict or 'auth' not in clientdict: - defer.returnValue((False, self.auth_dict_for_flows(flows))) + sess = self._get_session_info(None) + defer.returnValue( + (False, self._auth_dict_for_flows(flows, sess)) + ) authdict = clientdict['auth'] - # In future: support sessions & retrieve previously succeeded - # login types - creds = {} + sess = self._get_session_info( + authdict['session'] if 'session' in authdict else None + ) + if 'creds' not in sess: + sess['creds'] = {} + creds = sess['creds'] # check auth type currently being presented - if 'type' not in authdict: - raise LoginError(400, "", Codes.MISSING_PARAM) - if authdict['type'] not in types: - raise LoginError(400, "", Codes.UNRECOGNIZED) - result = yield types[authdict['type']](authdict, clientip) - if result: - creds[authdict['type']] = result + if 'type' in authdict: + if authdict['type'] not in self.checkers: + raise LoginError(400, "", Codes.UNRECOGNIZED) + result = yield self.checkers[authdict['type']](authdict, clientip) + if result: + creds[authdict['type']] = result + self._save_session(sess) for f in flows: if len(set(f) - set(creds.keys())) == 0: logger.info("Auth completed with creds: %r", creds) + self._remove_session(sess) defer.returnValue((True, creds)) - ret = self.auth_dict_for_flows(flows) + ret = self._auth_dict_for_flows(flows, sess) ret['completed'] = creds.keys() defer.returnValue((False, ret)) @defer.inlineCallbacks - def check_password_auth(self, authdict, _): + def add_oob_auth(self, stagetype, authdict, clientip): + if stagetype not in self.checkers: + raise LoginError(400, "", Codes.MISSING_PARAM) + if 'session' not in authdict: + raise LoginError(400, "", Codes.MISSING_PARAM) + + sess = self._get_session_info( + authdict['session'] + ) + if 'creds' not in sess: + sess['creds'] = {} + creds = sess['creds'] + + result = yield self.checkers[stagetype](authdict, clientip) + if result: + creds[stagetype] = result + self._save_session(sess) + defer.returnValue(True) + defer.returnValue(False) + + @defer.inlineCallbacks + def _check_password_auth(self, authdict, _): if "user" not in authdict or "password" not in authdict: raise LoginError(400, "", Codes.MISSING_PARAM) @@ -107,7 +139,7 @@ class AuthHandler(BaseHandler): raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) @defer.inlineCallbacks - def check_recaptcha(self, authdict, clientip): + def _check_recaptcha(self, authdict, clientip): try: user_response = authdict["response"] except KeyError: @@ -143,10 +175,10 @@ class AuthHandler(BaseHandler): defer.returnValue(True) raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) - def get_params_recaptcha(self): + def _get_params_recaptcha(self): return {"public_key": self.hs.config.recaptcha_public_key} - def auth_dict_for_flows(self, flows): + def _auth_dict_for_flows(self, flows, session): public_flows = [] for f in flows: hidden = False @@ -157,7 +189,7 @@ class AuthHandler(BaseHandler): public_flows.append(f) get_params = { - LoginType.RECAPTCHA: self.get_params_recaptcha, + LoginType.RECAPTCHA: self._get_params_recaptcha, } params = {} @@ -168,6 +200,30 @@ class AuthHandler(BaseHandler): params[stage] = get_params[stage]() return { + "session": session['id'], "flows": [{"stages": f} for f in public_flows], "params": params } + + def _get_session_info(self, session_id): + if session_id not in self.sessions: + session_id = None + + if not session_id: + # create a new session + while session_id is None or session_id in self.sessions: + session_id = stringutils.random_string(24) + self.sessions[session_id] = { + "id": session_id, + } + + return self.sessions[session_id] + + def _save_session(self, session): + # TODO: Persistent storage + logger.debug("Saving session %s", session) + self.sessions[session["id"]] = session + + def _remove_session(self, session): + logger.debug("Removing session %s", session) + del self.sessions[session["id"]] diff --git a/synapse/http/server.py b/synapse/http/server.py index 30c3aa5cac..76c561d105 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -170,9 +170,12 @@ class JsonResource(HttpServer, resource.Resource): request.method, request.path ) - code, response = yield callback(request, *args) + callback_return = yield callback(request, *args) + if callback_return is not None: + code, response = callback_return + + self._send_response(request, code, response) - self._send_response(request, code, response) response_timer.inc_by( self.clock.time_msec() - start, request.method, servlet_classname ) diff --git a/synapse/rest/client/v2_alpha/__init__.py b/synapse/rest/client/v2_alpha/__init__.py index 98189ead26..86e4bc729e 100644 --- a/synapse/rest/client/v2_alpha/__init__.py +++ b/synapse/rest/client/v2_alpha/__init__.py @@ -17,7 +17,8 @@ from . import ( sync, filter, password, - register + register, + auth ) from synapse.http.server import JsonResource @@ -36,3 +37,4 @@ class ClientV2AlphaRestResource(JsonResource): filter.register_servlets(hs, client_resource) password.register_servlets(hs, client_resource) register.register_servlets(hs, client_resource) + auth.register_servlets(hs, client_resource) diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py new file mode 100644 index 0000000000..7a518e226f --- /dev/null +++ b/synapse/rest/client/v2_alpha/auth.py @@ -0,0 +1,189 @@ +# -*- 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.constants import LoginType +from synapse.api.errors import SynapseError +from synapse.api.urls import CLIENT_V2_ALPHA_PREFIX +from synapse.http.servlet import RestServlet + +from ._base import client_v2_pattern + +import logging + + +logger = logging.getLogger(__name__) + +RECAPTCHA_TEMPLATE = """ + + +Authentication + + + + + + + +
+
+

+ Hello! We need to prevent computer programs and other automated + things from creating accounts on this server. +

+

+ Please verify that you're not a robot. +

+ +
+
+ +
+ +
+ + +""" + +SUCCESS_TEMPLATE = """ + + +Success! + + + + + +
+

Thank you

+

You may now close this window and return to the application

+
+ + +""" + +class AuthRestServlet(RestServlet): + """ + Handles Client / Server API authentication in any situations where it + cannot be handled in the normal flow (with requests to the same endpoint). + Current use is for web fallback auth. + """ + PATTERN = client_v2_pattern("/auth/(?P[\w\.]*)/fallback/web") + + def __init__(self, hs): + super(AuthRestServlet, self).__init__() + self.hs = hs + self.auth = hs.get_auth() + self.auth_handler = hs.get_handlers().auth_handler + self.registration_handler = hs.get_handlers().registration_handler + + @defer.inlineCallbacks + def on_GET(self, request, stagetype): + yield + if stagetype == LoginType.RECAPTCHA: + if ('session' not in request.args or + len(request.args['session']) == 0): + raise SynapseError(400, "No session supplied") + + session = request.args["session"][0] + + html = RECAPTCHA_TEMPLATE % { + 'session': session, + 'myurl': "%s/auth/%s/fallback/web" % ( + CLIENT_V2_ALPHA_PREFIX, LoginType.RECAPTCHA + ), + 'sitekey': self.hs.config.recaptcha_public_key, + } + html_bytes = html.encode("utf8") + request.setResponseCode(200) + request.setHeader(b"Content-Type", b"text/html; charset=utf-8") + request.setHeader(b"Server", self.hs.version_string) + request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) + + request.write(html_bytes) + request.finish() + defer.returnValue(None) + else: + raise SynapseError(404, "Unknown auth stage type") + + @defer.inlineCallbacks + def on_POST(self, request, stagetype): + yield + if stagetype == "m.login.recaptcha": + if ('g-recaptcha-response' not in request.args or + len(request.args['g-recaptcha-response'])) == 0: + raise SynapseError(400, "No captcha response supplied") + if ('session' not in request.args or + len(request.args['session'])) == 0: + raise SynapseError(400, "No session supplied") + + session = request.args['session'][0] + + authdict = { + 'response': request.args['g-recaptcha-response'][0], + 'session': session, + } + + success = yield self.auth_handler.add_oob_auth( + LoginType.RECAPTCHA, + authdict, + self.hs.get_ip_from_request(request) + ) + + if success: + html = SUCCESS_TEMPLATE + else: + html = RECAPTCHA_TEMPLATE % { + 'session': session, + 'myurl': "%s/auth/%s/fallback/web" % ( + CLIENT_V2_ALPHA_PREFIX, LoginType.RECAPTCHA + ), + 'sitekey': self.hs.config.recaptcha_public_key, + } + html_bytes = html.encode("utf8") + request.setResponseCode(200) + request.setHeader(b"Content-Type", b"text/html; charset=utf-8") + request.setHeader(b"Server", self.hs.version_string) + request.setHeader(b"Content-Length", b"%d" % (len(html_bytes),)) + + request.write(html_bytes) + request.finish() + + defer.returnValue(None) + else: + raise SynapseError(404, "Unknown auth stage type") + + def on_OPTIONS(self, _): + return 200, {} + + +def register_servlets(hs, http_server): + AuthRestServlet(hs).register(http_server) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 4a53e03743..537918ea27 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -45,7 +45,7 @@ class RegisterRestServlet(RestServlet): [LoginType.RECAPTCHA], [LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA], [LoginType.APPLICATION_SERVICE] - ], body) + ], body, self.hs.get_ip_from_request(request)) if not authed: defer.returnValue((401, result)) From c5bf0343e8c9dc5a21a546bfc93f24f9fc4d9737 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 1 Apr 2015 15:13:14 +0100 Subject: [PATCH 19/47] Explain how I justified to myself making JsonResource not always send JSON. --- synapse/http/server.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/synapse/http/server.py b/synapse/http/server.py index 76c561d105..0dbdce2839 100644 --- a/synapse/http/server.py +++ b/synapse/http/server.py @@ -79,6 +79,13 @@ class JsonResource(HttpServer, resource.Resource): Resources. Register callbacks via register_path() + + Callbacks can return a tuple of status code and a dict in which case the + the dict will automatically be sent to the client as a JSON object. + + The JsonResource is primarily intended for returning JSON, but callbacks + may send something other than JSON, they may do so by using the methods + on the request object and instead returning None. """ isLeaf = True From 70a84f17f39bbc5c8a68541874ca4767871f2b79 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Apr 2015 17:01:29 +0100 Subject: [PATCH 20/47] Add shared secret auth into register v2 and switch the script over. --- register_new_matrix_user | 5 +- synapse/api/constants.py | 4 +- synapse/rest/client/v2_alpha/register.py | 65 ++++++++++++++++++++---- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/register_new_matrix_user b/register_new_matrix_user index daddadc302..f833d2a4db 100755 --- a/register_new_matrix_user +++ b/register_new_matrix_user @@ -33,10 +33,9 @@ def request_registration(user, password, server_location, shared_secret): ).hexdigest() data = { - "user": user, + "username": user, "password": password, "mac": mac, - "type": "org.matrix.login.shared_secret", } server_location = server_location.rstrip("/") @@ -44,7 +43,7 @@ def request_registration(user, password, server_location, shared_secret): print "Sending registration request..." req = urllib2.Request( - "%s/_matrix/client/api/v1/register" % (server_location,), + "%s/_matrix/client/v2_alpha/register" % (server_location,), data=json.dumps(data), headers={'Content-Type': 'application/json'} ) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index 3e0ce170a4..f825c1a58b 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -60,9 +60,11 @@ class LoginType(object): EMAIL_IDENTITY = u"m.login.email.identity" RECAPTCHA = u"m.login.recaptcha" APPLICATION_SERVICE = u"m.login.application_service" + + # Only for C/S API v1 SHARED_SECRET = u"org.matrix.login.shared_secret" - HIDDEN_TYPES = [APPLICATION_SERVICE, SHARED_SECRET] + HIDDEN_TYPES = [APPLICATION_SERVICE] class EventTypes(object): diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 537918ea27..a69b45f362 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -22,6 +22,19 @@ from synapse.http.servlet import RestServlet from ._base import client_v2_pattern, parse_request_allow_empty import logging +import hmac +from hashlib import sha1 +from synapse.util.async import run_on_reactor + + +# We ought to be using hmac.compare_digest() but on older pythons it doesn't +# exist. It's a _really minor_ security flaw to use plain string comparison +# because the timing attack is so obscured by all the other code here it's +# unlikely to make much difference +if hasattr(hmac, "compare_digest"): + compare_digest = hmac.compare_digest +else: + compare_digest = lambda a, b: a == b logger = logging.getLogger(__name__) @@ -39,19 +52,30 @@ class RegisterRestServlet(RestServlet): @defer.inlineCallbacks def on_POST(self, request): + yield run_on_reactor() + body = parse_request_allow_empty(request) - authed, result = yield self.auth_handler.check_auth([ - [LoginType.RECAPTCHA], - [LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA], - [LoginType.APPLICATION_SERVICE] - ], body, self.hs.get_ip_from_request(request)) + is_using_shared_secret = False + is_application_server = False - if not authed: - defer.returnValue((401, result)) + if 'mac' in body: + # Check registration-specific shared secret auth + if 'username' not in body: + raise SynapseError(400, "", Codes.MISSING_PARAM) + self._check_shared_secret_auth( + body['username'], body['mac'] + ) + is_using_shared_secret = True + else: + authed, result = yield self.auth_handler.check_auth([ + [LoginType.RECAPTCHA], + [LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA], + [LoginType.APPLICATION_SERVICE] + ], body, self.hs.get_ip_from_request(request)) - is_application_server = LoginType.APPLICATION_SERVICE in result - is_using_shared_secret = LoginType.SHARED_SECRET in result + if not authed: + defer.returnValue((401, result)) can_register = ( not self.hs.config.disable_registration @@ -81,6 +105,29 @@ class RegisterRestServlet(RestServlet): def on_OPTIONS(self, _): return 200, {} + def _check_shared_secret_auth(self, username, mac): + if not self.hs.config.registration_shared_secret: + raise SynapseError(400, "Shared secret registration is not enabled") + + user = username.encode("utf-8") + + # str() because otherwise hmac complains that 'unicode' does not + # have the buffer interface + got_mac = str(mac) + + want_mac = hmac.new( + key=self.hs.config.registration_shared_secret, + msg=user, + digestmod=sha1, + ).hexdigest() + + if compare_digest(want_mac, got_mac): + return True + else: + raise SynapseError( + 403, "HMAC incorrect", + ) + def register_servlets(hs, http_server): RegisterRestServlet(hs).register(http_server) From 41cd778d6672101c7f18fe9acbca74e5d2dccc04 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Apr 2015 17:06:17 +0100 Subject: [PATCH 21/47] pep8 --- synapse/rest/client/v2_alpha/auth.py | 1 + 1 file changed, 1 insertion(+) diff --git a/synapse/rest/client/v2_alpha/auth.py b/synapse/rest/client/v2_alpha/auth.py index 7a518e226f..4c726f05f5 100644 --- a/synapse/rest/client/v2_alpha/auth.py +++ b/synapse/rest/client/v2_alpha/auth.py @@ -90,6 +90,7 @@ if (window.onAuthDone != undefined) { """ + class AuthRestServlet(RestServlet): """ Handles Client / Server API authentication in any situations where it From 6b59650753a516404d3bb038f4a55cad5d7ddcd1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Apr 2015 17:45:16 +0100 Subject: [PATCH 22/47] Throw sensible errors on not-json when allowing empty body --- synapse/rest/client/v2_alpha/_base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/synapse/rest/client/v2_alpha/_base.py b/synapse/rest/client/v2_alpha/_base.py index 8adcc9dd95..4540e8dcf7 100644 --- a/synapse/rest/client/v2_alpha/_base.py +++ b/synapse/rest/client/v2_alpha/_base.py @@ -44,7 +44,10 @@ def parse_request_allow_empty(request): content = request.content.read() if content is None or content == '': return None - return simplejson.loads(content) + try: + return simplejson.loads(content) + except simplejson.JSONDecodeError: + raise SynapseError(400, "Content not JSON.") def parse_json_dict_from_request(request): From 4eb6d66b45356efcc87089cb52ca6f51c98cd798 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Apr 2015 17:51:19 +0100 Subject: [PATCH 23/47] Add app service auth back in to v2 register --- synapse/api/constants.py | 4 +--- synapse/rest/client/v2_alpha/register.py | 9 +++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index f825c1a58b..d29c2dde01 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -59,13 +59,11 @@ class LoginType(object): EMAIL_URL = u"m.login.email.url" EMAIL_IDENTITY = u"m.login.email.identity" RECAPTCHA = u"m.login.recaptcha" - APPLICATION_SERVICE = u"m.login.application_service" # Only for C/S API v1 + APPLICATION_SERVICE = u"m.login.application_service" SHARED_SECRET = u"org.matrix.login.shared_secret" - HIDDEN_TYPES = [APPLICATION_SERVICE] - class EventTypes(object): Member = "m.room.member" diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index a69b45f362..72319a3bb2 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -59,7 +59,13 @@ class RegisterRestServlet(RestServlet): is_using_shared_secret = False is_application_server = False - if 'mac' in body: + service = None + if 'access_token' in request.args: + service = yield self.auth.get_appservice_by_req(request) + + if service: + is_application_server = True + elif 'mac' in body: # Check registration-specific shared secret auth if 'username' not in body: raise SynapseError(400, "", Codes.MISSING_PARAM) @@ -71,7 +77,6 @@ class RegisterRestServlet(RestServlet): authed, result = yield self.auth_handler.check_auth([ [LoginType.RECAPTCHA], [LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA], - [LoginType.APPLICATION_SERVICE] ], body, self.hs.get_ip_from_request(request)) if not authed: From a19b73990962ff3bfe8b2cae59446bbe7f93ec5c Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Apr 2015 15:50:38 +0100 Subject: [PATCH 24/47] Regstration with email in v2 --- synapse/handlers/__init__.py | 2 + synapse/handlers/auth.py | 64 +++++++++++++++-------- synapse/handlers/identity.py | 66 ++++++++++++++++++++++++ synapse/handlers/register.py | 6 ++- synapse/rest/client/v2_alpha/password.py | 6 +-- synapse/rest/client/v2_alpha/register.py | 8 +-- 6 files changed, 123 insertions(+), 29 deletions(-) create mode 100644 synapse/handlers/identity.py diff --git a/synapse/handlers/__init__.py b/synapse/handlers/__init__.py index 336ce15701..d1b0e032a3 100644 --- a/synapse/handlers/__init__.py +++ b/synapse/handlers/__init__.py @@ -30,6 +30,7 @@ from .admin import AdminHandler from .appservice import ApplicationServicesHandler from .sync import SyncHandler from .auth import AuthHandler +from .identity import IdentityHandler class Handlers(object): @@ -60,3 +61,4 @@ class Handlers(object): ) self.sync_handler = SyncHandler(hs) self.auth_handler = AuthHandler(hs) + self.identity_handler = IdentityHandler(hs) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 3d2461dd7d..2cc54707a2 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -20,6 +20,7 @@ from synapse.api.constants import LoginType from synapse.types import UserID from synapse.api.errors import LoginError, Codes from synapse.http.client import SimpleHttpClient +from synapse.util.async import run_on_reactor from twisted.web.client import PartialDownloadError @@ -40,6 +41,7 @@ class AuthHandler(BaseHandler): self.checkers = { LoginType.PASSWORD: self._check_password_auth, LoginType.RECAPTCHA: self._check_recaptcha, + LoginType.EMAIL_IDENTITY: self._check_email_identity, } self.sessions = {} @@ -54,24 +56,37 @@ class AuthHandler(BaseHandler): authdict: The dictionary from the client root level, not the 'auth' key: this method prompts for auth if none is sent. Returns: - A tuple of authed, dict where authed is true if the client - has successfully completed an auth flow. If it is true, the dict - contains the authenticated credentials of each stage. - If authed is false, the dictionary is the server response to the - login request and should be passed back to the client. + A tuple of authed, dict, dict where authed is true if the client + has successfully completed an auth flow. If it is true, the first + dict contains the authenticated credentials of each stage. + + If authed is false, the first dictionary is the server response to + the login request and should be passed back to the client. + + In either case, the second dict contains the parameters for this + request (which may have been given only in a previous call). """ - if not clientdict or 'auth' not in clientdict: - sess = self._get_session_info(None) + authdict = None + sid = None + if clientdict and 'auth' in clientdict: + authdict = clientdict['auth'] + del clientdict['auth'] + if 'session' in authdict: + sid = authdict['session'] + sess = self._get_session_info(sid) + + if len(clientdict) > 0: + sess['clientdict'] = clientdict + self._save_session(sess) + elif 'clientdict' in sess: + clientdict = sess['clientdict'] + + if not authdict: defer.returnValue( - (False, self._auth_dict_for_flows(flows, sess)) + (False, self._auth_dict_for_flows(flows, sess), clientdict) ) - authdict = clientdict['auth'] - - sess = self._get_session_info( - authdict['session'] if 'session' in authdict else None - ) if 'creds' not in sess: sess['creds'] = {} creds = sess['creds'] @@ -89,11 +104,11 @@ class AuthHandler(BaseHandler): if len(set(f) - set(creds.keys())) == 0: logger.info("Auth completed with creds: %r", creds) self._remove_session(sess) - defer.returnValue((True, creds)) + defer.returnValue((True, creds, clientdict)) ret = self._auth_dict_for_flows(flows, sess) ret['completed'] = creds.keys() - defer.returnValue((False, ret)) + defer.returnValue((False, ret, clientdict)) @defer.inlineCallbacks def add_oob_auth(self, stagetype, authdict, clientip): @@ -175,18 +190,25 @@ class AuthHandler(BaseHandler): defer.returnValue(True) raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) + @defer.inlineCallbacks + def _check_email_identity(self, authdict, _): + yield run_on_reactor() + + threepidCreds = authdict['threepidCreds'] + identity_handler = self.hs.get_handlers().identity_handler + + logger.debug("Getting validated threepid. threepidcreds: %r" % (threepidCreds,)) + threepid = yield identity_handler.threepid_from_creds(threepidCreds) + + defer.returnValue(threepid) + def _get_params_recaptcha(self): return {"public_key": self.hs.config.recaptcha_public_key} def _auth_dict_for_flows(self, flows, session): public_flows = [] for f in flows: - hidden = False - for stagetype in f: - if stagetype in LoginType.HIDDEN_TYPES: - hidden = True - if not hidden: - public_flows.append(f) + public_flows.append(f) get_params = { LoginType.RECAPTCHA: self._get_params_recaptcha, diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py new file mode 100644 index 0000000000..671d366e40 --- /dev/null +++ b/synapse/handlers/identity.py @@ -0,0 +1,66 @@ +# -*- 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. + +"""Utilities for interacting with Identity Servers""" +from twisted.internet import defer + +from synapse.api.errors import ( + CodeMessageException +) +from ._base import BaseHandler +from synapse.http.client import SimpleHttpClient +from synapse.util.async import run_on_reactor + +import json +import logging + +logger = logging.getLogger(__name__) + + +class IdentityHandler(BaseHandler): + + def __init__(self, hs): + super(IdentityHandler, self).__init__(hs) + + @defer.inlineCallbacks + def threepid_from_creds(self, creds): + yield run_on_reactor() + + # TODO: get this from the homeserver rather than creating a new one for + # each request + http_client = SimpleHttpClient(self.hs) + # XXX: make this configurable! + #trustedIdServers = ['matrix.org', 'localhost:8090'] + trustedIdServers = ['matrix.org'] + if not creds['idServer'] in trustedIdServers: + logger.warn('%s is not a trusted ID server: rejecting 3pid ' + + 'credentials', creds['idServer']) + defer.returnValue(None) + + data = {} + try: + data = yield http_client.get_json( + "https://%s%s" % ( + creds['idServer'], + "/_matrix/identity/api/v1/3pid/getValidated3pid" + ), + {'sid': creds['sid'], 'clientSecret': creds['clientSecret']} + ) + except CodeMessageException as e: + data = json.loads(e.msg) + + if 'medium' in data: + defer.returnValue(data) + defer.returnValue(None) \ No newline at end of file diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 542759a827..6759a8c582 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -180,7 +180,11 @@ class RegistrationHandler(BaseHandler): @defer.inlineCallbacks def register_email(self, threepidCreds): - """Registers emails with an identity server.""" + """ + Registers emails with an identity server. + + Used only by c/s api v1 + """ for c in threepidCreds: logger.info("validating theeepidcred sid %s on id server %s", diff --git a/synapse/rest/client/v2_alpha/password.py b/synapse/rest/client/v2_alpha/password.py index 85954c71cd..cb0c8cfb55 100644 --- a/synapse/rest/client/v2_alpha/password.py +++ b/synapse/rest/client/v2_alpha/password.py @@ -41,7 +41,7 @@ class PasswordRestServlet(RestServlet): def on_POST(self, request): body = parse_json_dict_from_request(request) - authed, result = yield self.auth_handler.check_auth([ + authed, result, params = yield self.auth_handler.check_auth([ [LoginType.PASSWORD] ], body) @@ -61,9 +61,9 @@ class PasswordRestServlet(RestServlet): user_id = auth_user.to_string() - if 'new_password' not in body: + if 'new_password' not in params: raise SynapseError(400, "", Codes.MISSING_PARAM) - new_password = body['new_password'] + new_password = params['new_password'] yield self.login_handler.set_password( user_id, new_password, client.token_id diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 72319a3bb2..d7a20fc964 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -74,7 +74,7 @@ class RegisterRestServlet(RestServlet): ) is_using_shared_secret = True else: - authed, result = yield self.auth_handler.check_auth([ + authed, result, params = yield self.auth_handler.check_auth([ [LoginType.RECAPTCHA], [LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA], ], body, self.hs.get_ip_from_request(request)) @@ -90,10 +90,10 @@ class RegisterRestServlet(RestServlet): if not can_register: raise SynapseError(403, "Registration has been disabled") - if 'username' not in body or 'password' not in body: + if 'username' not in params or 'password' not in params: raise SynapseError(400, "", Codes.MISSING_PARAM) - desired_username = body['username'] - new_password = body['password'] + desired_username = params['username'] + new_password = params['password'] (user_id, token) = yield self.registration_handler.register( localpart=desired_username, From 766bd8e88077cbeabffc353d9735a3af190abe61 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 15 Apr 2015 17:14:25 +0100 Subject: [PATCH 25/47] Dummy login so we can do the first POST request to get login flows without it just succeeding --- synapse/api/constants.py | 1 + synapse/handlers/auth.py | 6 ++++++ synapse/handlers/identity.py | 6 +++--- synapse/rest/client/v2_alpha/register.py | 18 ++++++++++++++---- 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/synapse/api/constants.py b/synapse/api/constants.py index d29c2dde01..d8a18ee87b 100644 --- a/synapse/api/constants.py +++ b/synapse/api/constants.py @@ -59,6 +59,7 @@ class LoginType(object): EMAIL_URL = u"m.login.email.url" EMAIL_IDENTITY = u"m.login.email.identity" RECAPTCHA = u"m.login.recaptcha" + DUMMY = u"m.login.dummy" # Only for C/S API v1 APPLICATION_SERVICE = u"m.login.application_service" diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 2cc54707a2..87866f298d 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -42,6 +42,7 @@ class AuthHandler(BaseHandler): LoginType.PASSWORD: self._check_password_auth, LoginType.RECAPTCHA: self._check_recaptcha, LoginType.EMAIL_IDENTITY: self._check_email_identity, + LoginType.DUMMY: self._check_dummy_auth, } self.sessions = {} @@ -202,6 +203,11 @@ class AuthHandler(BaseHandler): defer.returnValue(threepid) + @defer.inlineCallbacks + def _check_dummy_auth(self, authdict, _): + yield run_on_reactor() + defer.returnValue(True) + def _get_params_recaptcha(self): return {"public_key": self.hs.config.recaptcha_public_key} diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index 671d366e40..19896ce90d 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -42,8 +42,8 @@ class IdentityHandler(BaseHandler): # each request http_client = SimpleHttpClient(self.hs) # XXX: make this configurable! - #trustedIdServers = ['matrix.org', 'localhost:8090'] - trustedIdServers = ['matrix.org'] + trustedIdServers = ['matrix.org', 'localhost:8090'] + #trustedIdServers = ['matrix.org'] if not creds['idServer'] in trustedIdServers: logger.warn('%s is not a trusted ID server: rejecting 3pid ' + 'credentials', creds['idServer']) @@ -52,7 +52,7 @@ class IdentityHandler(BaseHandler): data = {} try: data = yield http_client.get_json( - "https://%s%s" % ( + "http://%s%s" % ( creds['idServer'], "/_matrix/identity/api/v1/3pid/getValidated3pid" ), diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index d7a20fc964..ee99b74fd6 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -63,6 +63,17 @@ class RegisterRestServlet(RestServlet): if 'access_token' in request.args: service = yield self.auth.get_appservice_by_req(request) + if self.hs.config.enable_registration_captcha: + flows = [ + [LoginType.RECAPTCHA], + [LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA] + ] + else: + flows = [ + [LoginType.DUMMY], + [LoginType.EMAIL_IDENTITY] + ] + if service: is_application_server = True elif 'mac' in body: @@ -74,10 +85,9 @@ class RegisterRestServlet(RestServlet): ) is_using_shared_secret = True else: - authed, result, params = yield self.auth_handler.check_auth([ - [LoginType.RECAPTCHA], - [LoginType.EMAIL_IDENTITY, LoginType.RECAPTCHA], - ], body, self.hs.get_ip_from_request(request)) + authed, result, params = yield self.auth_handler.check_auth( + flows, body, self.hs.get_ip_from_request(request) + ) if not authed: defer.returnValue((401, result)) From ea1776f556edaf6ca483bc5faed5e9d244aa1a15 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Apr 2015 19:56:44 +0100 Subject: [PATCH 26/47] Return user ID in use error straight away --- synapse/handlers/auth.py | 2 + synapse/handlers/identity.py | 25 +++++- synapse/handlers/register.py | 102 +++++++++-------------- synapse/rest/client/v2_alpha/register.py | 25 +++++- 4 files changed, 88 insertions(+), 66 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 87866f298d..1f927e67ad 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -201,6 +201,8 @@ class AuthHandler(BaseHandler): logger.debug("Getting validated threepid. threepidcreds: %r" % (threepidCreds,)) threepid = yield identity_handler.threepid_from_creds(threepidCreds) + threepid['threepidCreds'] = authdict['threepidCreds'] + defer.returnValue(threepid) @defer.inlineCallbacks diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index 19896ce90d..cb5e1e80ac 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -63,4 +63,27 @@ class IdentityHandler(BaseHandler): if 'medium' in data: defer.returnValue(data) - defer.returnValue(None) \ No newline at end of file + defer.returnValue(None) + + @defer.inlineCallbacks + def bind_threepid(self, creds, mxid): + yield run_on_reactor() + logger.debug("binding threepid %r to %s", creds, mxid) + http_client = SimpleHttpClient(self.hs) + data = None + try: + data = yield http_client.post_urlencoded_get_json( + # XXX: Change when ID servers are all HTTPS + "http://%s%s" % ( + creds['idServer'], "/_matrix/identity/api/v1/3pid/bind" + ), + { + 'sid': creds['sid'], + 'clientSecret': creds['clientSecret'], + 'mxid': mxid, + } + ) + logger.debug("bound threepid %r to %s", creds, mxid) + except CodeMessageException as e: + data = json.loads(e.msg) + defer.returnValue(data) \ No newline at end of file diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 6759a8c582..541b1019da 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -44,6 +44,36 @@ class RegistrationHandler(BaseHandler): self.distributor = hs.get_distributor() self.distributor.declare("registered_user") + @defer.inlineCallbacks + def check_username(self, localpart): + yield run_on_reactor() + + print "checking username %s" % (localpart) + + if urllib.quote(localpart) != localpart: + raise SynapseError( + 400, + "User ID must only contain characters which do not" + " require URL encoding." + ) + + user = UserID(localpart, self.hs.hostname) + user_id = user.to_string() + + yield self.check_user_id_is_valid(user_id) + + print "is valid" + + u = yield self.store.get_user_by_id(user_id) + print "user is: " + print u + if u: + raise SynapseError( + 400, + "User ID already taken.", + errcode=Codes.USER_IN_USE, + ) + @defer.inlineCallbacks def register(self, localpart=None, password=None): """Registers a new client on the server. @@ -64,18 +94,11 @@ class RegistrationHandler(BaseHandler): password_hash = bcrypt.hashpw(password, bcrypt.gensalt()) if localpart: - if localpart and urllib.quote(localpart) != localpart: - raise SynapseError( - 400, - "User ID must only contain characters which do not" - " require URL encoding." - ) + self.check_username(localpart) user = UserID(localpart, self.hs.hostname) user_id = user.to_string() - yield self.check_user_id_is_valid(user_id) - token = self._generate_token(user_id) yield self.store.register( user_id=user_id, @@ -190,7 +213,8 @@ class RegistrationHandler(BaseHandler): logger.info("validating theeepidcred sid %s on id server %s", c['sid'], c['idServer']) try: - threepid = yield self._threepid_from_creds(c) + identity_handler = self.hs.get_handlers().identity_handler + threepid = yield identity_handler.threepid_from_creds(c) except: logger.exception("Couldn't validate 3pid") raise RegistrationError(400, "Couldn't validate 3pid") @@ -202,12 +226,16 @@ class RegistrationHandler(BaseHandler): @defer.inlineCallbacks def bind_emails(self, user_id, threepidCreds): - """Links emails with a user ID and informs an identity server.""" + """Links emails with a user ID and informs an identity server. + + Used only by c/s api v1 + """ # Now we have a matrix ID, bind it to the threepids we were given for c in threepidCreds: + identity_handler = self.hs.get_handlers().identity_handler # XXX: This should be a deferred list, shouldn't it? - yield self._bind_threepid(c, user_id) + yield identity_handler.bind_threepid(c, user_id) @defer.inlineCallbacks def check_user_id_is_valid(self, user_id): @@ -234,58 +262,6 @@ class RegistrationHandler(BaseHandler): def _generate_user_id(self): return "-" + stringutils.random_string(18) - @defer.inlineCallbacks - def _threepid_from_creds(self, creds): - # TODO: get this from the homeserver rather than creating a new one for - # each request - http_client = SimpleHttpClient(self.hs) - # XXX: make this configurable! - trustedIdServers = ['matrix.org:8090', 'matrix.org'] - if not creds['idServer'] in trustedIdServers: - logger.warn('%s is not a trusted ID server: rejecting 3pid ' + - 'credentials', creds['idServer']) - defer.returnValue(None) - - data = {} - try: - data = yield http_client.get_json( - # XXX: This should be HTTPS - "http://%s%s" % ( - creds['idServer'], - "/_matrix/identity/api/v1/3pid/getValidated3pid" - ), - {'sid': creds['sid'], 'clientSecret': creds['clientSecret']} - ) - except CodeMessageException as e: - data = json.loads(e.msg) - - if 'medium' in data: - defer.returnValue(data) - defer.returnValue(None) - - @defer.inlineCallbacks - def _bind_threepid(self, creds, mxid): - yield - logger.debug("binding threepid") - http_client = SimpleHttpClient(self.hs) - data = None - try: - data = yield http_client.post_urlencoded_get_json( - # XXX: Change when ID servers are all HTTPS - "http://%s%s" % ( - creds['idServer'], "/_matrix/identity/api/v1/3pid/bind" - ), - { - 'sid': creds['sid'], - 'clientSecret': creds['clientSecret'], - 'mxid': mxid, - } - ) - logger.debug("bound threepid") - except CodeMessageException as e: - data = json.loads(e.msg) - defer.returnValue(data) - @defer.inlineCallbacks def _validate_captcha(self, ip_addr, private_key, challenge, response): """Validates the captcha provided. diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index ee99b74fd6..a5fec45dce 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -49,12 +49,20 @@ class RegisterRestServlet(RestServlet): self.auth = hs.get_auth() self.auth_handler = hs.get_handlers().auth_handler self.registration_handler = hs.get_handlers().registration_handler + self.identity_handler = hs.get_handlers().identity_handler @defer.inlineCallbacks def on_POST(self, request): yield run_on_reactor() body = parse_request_allow_empty(request) + if 'password' not in body: + raise SynapseError(400, "", Codes.MISSING_PARAM) + + if 'username' in body: + desired_username = body['username'] + print "username in body" + yield self.registration_handler.check_username(desired_username) is_using_shared_secret = False is_application_server = False @@ -100,15 +108,28 @@ class RegisterRestServlet(RestServlet): if not can_register: raise SynapseError(403, "Registration has been disabled") - if 'username' not in params or 'password' not in params: + if 'password' not in params: raise SynapseError(400, "", Codes.MISSING_PARAM) - desired_username = params['username'] + desired_username = params['username'] if 'username' in params else None new_password = params['password'] (user_id, token) = yield self.registration_handler.register( localpart=desired_username, password=new_password ) + + if 'bind_email' in params and params['bind_email']: + logger.info("bind_email specified: binding") + + emailThreepid = result[LoginType.EMAIL_IDENTITY] + threepidCreds = emailThreepid['threepidCreds'] + logger.debug("Binding emails %s to %s" % ( + emailThreepid, user_id + )) + yield self.identity_handler.bind_threepid(threepidCreds, user_id) + else: + logger.info("bind_email not specified: not binding email") + result = { "user_id": user_id, "access_token": token, From 4cd5fb13a31a4da1d7d8feb06d211a2f7842f5ad Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 16 Apr 2015 20:03:13 +0100 Subject: [PATCH 27/47] Oops, left debugging in. --- synapse/handlers/register.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 541b1019da..25b1db62ea 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -48,8 +48,6 @@ class RegistrationHandler(BaseHandler): def check_username(self, localpart): yield run_on_reactor() - print "checking username %s" % (localpart) - if urllib.quote(localpart) != localpart: raise SynapseError( 400, @@ -62,11 +60,7 @@ class RegistrationHandler(BaseHandler): yield self.check_user_id_is_valid(user_id) - print "is valid" - u = yield self.store.get_user_by_id(user_id) - print "user is: " - print u if u: raise SynapseError( 400, From 83b554437ec9810dd09de992c728c2a2f01aa0e1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Apr 2015 12:57:25 +0100 Subject: [PATCH 28/47] Need to yield the username check, otherwise very very weird things happen. --- synapse/handlers/register.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index 25b1db62ea..d4483c3a1d 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -88,7 +88,7 @@ class RegistrationHandler(BaseHandler): password_hash = bcrypt.hashpw(password, bcrypt.gensalt()) if localpart: - self.check_username(localpart) + yield self.check_username(localpart) user = UserID(localpart, self.hs.hostname) user_id = user.to_string() From 94e1e58b4de00ea92fa60d6fc7970dafa9ebce2a Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Apr 2015 13:44:12 +0100 Subject: [PATCH 29/47] password -> account servlet and add start of an 'add 3pid' endpoint --- synapse/rest/client/v2_alpha/__init__.py | 4 ++-- .../client/v2_alpha/{password.py => account.py} | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) rename synapse/rest/client/v2_alpha/{password.py => account.py} (86%) diff --git a/synapse/rest/client/v2_alpha/__init__.py b/synapse/rest/client/v2_alpha/__init__.py index 86e4bc729e..28d95b2729 100644 --- a/synapse/rest/client/v2_alpha/__init__.py +++ b/synapse/rest/client/v2_alpha/__init__.py @@ -16,7 +16,7 @@ from . import ( sync, filter, - password, + account, register, auth ) @@ -35,6 +35,6 @@ class ClientV2AlphaRestResource(JsonResource): def register_servlets(client_resource, hs): sync.register_servlets(hs, client_resource) filter.register_servlets(hs, client_resource) - password.register_servlets(hs, client_resource) + account.register_servlets(hs, client_resource) register.register_servlets(hs, client_resource) auth.register_servlets(hs, client_resource) diff --git a/synapse/rest/client/v2_alpha/password.py b/synapse/rest/client/v2_alpha/account.py similarity index 86% rename from synapse/rest/client/v2_alpha/password.py rename to synapse/rest/client/v2_alpha/account.py index cb0c8cfb55..0f3b12880c 100644 --- a/synapse/rest/client/v2_alpha/password.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -75,5 +75,20 @@ class PasswordRestServlet(RestServlet): return 200, {} +class AddThreepidRestServlet(RestServlet): + PATTERN = client_v2_pattern("/account/3pid") + + @defer.inlineCallbacks + def on_POST(self, request): + body = parse_json_dict_from_request(request) + + if 'threePidCreds' not in body: + raise SynapseError(400, "Missing param", Codes.MISSING_PARAM) + + auth_user, client = yield self.auth.get_user_by_req(request) + + + + def register_servlets(hs, http_server): PasswordRestServlet(hs).register(http_server) From bf5e54f25537e293b78b26d0b8109c1ee43eee06 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Apr 2015 13:44:55 +0100 Subject: [PATCH 30/47] Register the 3pid servlet --- synapse/rest/client/v2_alpha/account.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 0f3b12880c..ca92a6a4cf 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -75,7 +75,7 @@ class PasswordRestServlet(RestServlet): return 200, {} -class AddThreepidRestServlet(RestServlet): +class ThreepidRestServlet(RestServlet): PATTERN = client_v2_pattern("/account/3pid") @defer.inlineCallbacks @@ -92,3 +92,5 @@ class AddThreepidRestServlet(RestServlet): def register_servlets(hs, http_server): PasswordRestServlet(hs).register(http_server) + ThreepidRestServlet(hs).register(http_server) + ThreepidRestServlet(hs).register(http_server) From 0b1a8500a297ea0f988c3fd04403163d265239a1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Apr 2015 13:53:54 +0100 Subject: [PATCH 31/47] just the once would probably be fine --- synapse/rest/client/v2_alpha/account.py | 1 - 1 file changed, 1 deletion(-) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index ca92a6a4cf..750d826f91 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -93,4 +93,3 @@ class ThreepidRestServlet(RestServlet): def register_servlets(hs, http_server): PasswordRestServlet(hs).register(http_server) ThreepidRestServlet(hs).register(http_server) - ThreepidRestServlet(hs).register(http_server) From f96ab9d18dcebf995700f096792101a490b3a9b8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Apr 2015 16:44:49 +0100 Subject: [PATCH 32/47] make add3pid servlet work --- synapse/handlers/login.py | 7 +++++ synapse/rest/client/v2_alpha/account.py | 38 ++++++++++++++++++++++++ synapse/rest/client/v2_alpha/register.py | 36 +++++++++++++++------- synapse/storage/registration.py | 11 +++++++ 4 files changed, 81 insertions(+), 11 deletions(-) diff --git a/synapse/handlers/login.py b/synapse/handlers/login.py index 04f6dbb95e..5c39356d71 100644 --- a/synapse/handlers/login.py +++ b/synapse/handlers/login.py @@ -74,3 +74,10 @@ class LoginHandler(BaseHandler): user_id, token_id ) yield self.store.flush_user(user_id) + + @defer.inlineCallbacks + def add_threepid(self, user_id, medium, address, validated_at): + yield self.store.user_add_threepid( + user_id, medium, address, validated_at, + self.hs.get_clock().time_msec() + ) \ No newline at end of file diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 750d826f91..6045b016ef 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -18,6 +18,7 @@ from twisted.internet import defer from synapse.api.constants import LoginType from synapse.api.errors import LoginError, SynapseError, Codes from synapse.http.servlet import RestServlet +from synapse.util.async import run_on_reactor from ._base import client_v2_pattern, parse_json_dict_from_request @@ -39,6 +40,8 @@ class PasswordRestServlet(RestServlet): @defer.inlineCallbacks def on_POST(self, request): + yield run_on_reactor() + body = parse_json_dict_from_request(request) authed, result, params = yield self.auth_handler.check_auth([ @@ -78,16 +81,51 @@ class PasswordRestServlet(RestServlet): class ThreepidRestServlet(RestServlet): PATTERN = client_v2_pattern("/account/3pid") + def __init__(self, hs): + super(ThreepidRestServlet, self).__init__() + self.hs = hs + self.login_handler = hs.get_handlers().login_handler + self.identity_handler = hs.get_handlers().identity_handler + self.auth = hs.get_auth() + @defer.inlineCallbacks def on_POST(self, request): + yield run_on_reactor() + body = parse_json_dict_from_request(request) if 'threePidCreds' not in body: raise SynapseError(400, "Missing param", Codes.MISSING_PARAM) + threePidCreds = body['threePidCreds'] auth_user, client = yield self.auth.get_user_by_req(request) + threepid = yield self.identity_handler.threepid_from_creds(threePidCreds) + if not threepid: + raise SynapseError(400, "Failed to auth 3pid") + + for reqd in ['medium', 'address', 'validatedAt']: + if reqd not in threepid: + logger.warn("Couldn't add 3pid: invalid response from ID sevrer") + raise SynapseError(500, "Invalid response from ID Server") + + yield self.login_handler.add_threepid( + auth_user.to_string(), + threepid['medium'], + threepid['address'], + threepid['validatedAt'], + ) + + if 'bind' in body and body['bind']: + logger.debug("Binding emails %s to %s" % ( + threepid, auth_user.to_string() + )) + yield self.identity_handler.bind_threepid( + threePidCreds, auth_user.to_string() + ) + + defer.returnValue((200, {})) def register_servlets(hs, http_server): diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index a5fec45dce..e93897e285 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -50,6 +50,7 @@ class RegisterRestServlet(RestServlet): self.auth_handler = hs.get_handlers().auth_handler self.registration_handler = hs.get_handlers().registration_handler self.identity_handler = hs.get_handlers().identity_handler + self.login_handler = hs.get_handlers().login_handler @defer.inlineCallbacks def on_POST(self, request): @@ -61,7 +62,6 @@ class RegisterRestServlet(RestServlet): if 'username' in body: desired_username = body['username'] - print "username in body" yield self.registration_handler.check_username(desired_username) is_using_shared_secret = False @@ -118,17 +118,31 @@ class RegisterRestServlet(RestServlet): password=new_password ) - if 'bind_email' in params and params['bind_email']: - logger.info("bind_email specified: binding") + if LoginType.EMAIL_IDENTITY in result: + threepid = result[LoginType.EMAIL_IDENTITY] - emailThreepid = result[LoginType.EMAIL_IDENTITY] - threepidCreds = emailThreepid['threepidCreds'] - logger.debug("Binding emails %s to %s" % ( - emailThreepid, user_id - )) - yield self.identity_handler.bind_threepid(threepidCreds, user_id) - else: - logger.info("bind_email not specified: not binding email") + for reqd in ['medium', 'address', 'validatedAt']: + if reqd not in threepid: + logger.info("Can't add incomplete 3pid") + else: + yield self.login_handler.add_threepid( + user_id, + threepid['medium'], + threepid['address'], + threepid['validatedAt'], + ) + + if 'bind_email' in params and params['bind_email']: + logger.info("bind_email specified: binding") + + emailThreepid = result[LoginType.EMAIL_IDENTITY] + threepidCreds = emailThreepid['threepidCreds'] + logger.debug("Binding emails %s to %s" % ( + emailThreepid, user_id + )) + yield self.identity_handler.bind_threepid(threepidCreds, user_id) + else: + logger.info("bind_email not specified: not binding email") result = { "user_id": user_id, diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index f61d8fdb6a..4bc01f3cc2 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -175,3 +175,14 @@ class RegistrationStore(SQLBaseStore): return rows[0] return None + + @defer.inlineCallbacks + def user_add_threepid(self, user_id, medium, address, validated_at, added_at): + yield self._simple_upsert("user_threepids", { + "user": user_id, + "medium": medium, + "address": address, + }, { + "validated_at": validated_at, + "added_at": added_at, + }) \ No newline at end of file From 4eea5cf6c2a301938466828b02557d8500197bb3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Apr 2015 16:46:45 +0100 Subject: [PATCH 33/47] pep8 --- synapse/handlers/identity.py | 6 +++--- synapse/handlers/login.py | 2 +- synapse/handlers/register.py | 5 +---- synapse/storage/registration.py | 2 +- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index cb5e1e80ac..5c72635915 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -42,8 +42,8 @@ class IdentityHandler(BaseHandler): # each request http_client = SimpleHttpClient(self.hs) # XXX: make this configurable! - trustedIdServers = ['matrix.org', 'localhost:8090'] - #trustedIdServers = ['matrix.org'] + # trustedIdServers = ['matrix.org', 'localhost:8090'] + trustedIdServers = ['matrix.org'] if not creds['idServer'] in trustedIdServers: logger.warn('%s is not a trusted ID server: rejecting 3pid ' + 'credentials', creds['idServer']) @@ -86,4 +86,4 @@ class IdentityHandler(BaseHandler): logger.debug("bound threepid %r to %s", creds, mxid) except CodeMessageException as e: data = json.loads(e.msg) - defer.returnValue(data) \ No newline at end of file + defer.returnValue(data) diff --git a/synapse/handlers/login.py b/synapse/handlers/login.py index 5c39356d71..f7f3698340 100644 --- a/synapse/handlers/login.py +++ b/synapse/handlers/login.py @@ -80,4 +80,4 @@ class LoginHandler(BaseHandler): yield self.store.user_add_threepid( user_id, medium, address, validated_at, self.hs.get_clock().time_msec() - ) \ No newline at end of file + ) diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index d4483c3a1d..7b68585a17 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -18,18 +18,15 @@ from twisted.internet import defer from synapse.types import UserID from synapse.api.errors import ( - AuthError, Codes, SynapseError, RegistrationError, InvalidCaptchaError, - CodeMessageException + AuthError, Codes, SynapseError, RegistrationError, InvalidCaptchaError ) from ._base import BaseHandler import synapse.util.stringutils as stringutils from synapse.util.async import run_on_reactor -from synapse.http.client import SimpleHttpClient from synapse.http.client import CaptchaServerHttpClient import base64 import bcrypt -import json import logging import urllib diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index 4bc01f3cc2..8f62e5c6f2 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -185,4 +185,4 @@ class RegistrationStore(SQLBaseStore): }, { "validated_at": validated_at, "added_at": added_at, - }) \ No newline at end of file + }) From 117f35ac4ac4f8d344ae1efbc629a3f8bc25f459 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Apr 2015 17:20:18 +0100 Subject: [PATCH 34/47] Add endpoint to get threepids from server --- synapse/rest/client/v2_alpha/account.py | 12 ++++++++++++ synapse/storage/registration.py | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 6045b016ef..5ac3ac0f71 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -88,6 +88,18 @@ class ThreepidRestServlet(RestServlet): self.identity_handler = hs.get_handlers().identity_handler self.auth = hs.get_auth() + @defer.inlineCallbacks + def on_GET(self, request): + yield run_on_reactor() + + auth_user, _ = yield self.auth.get_user_by_req(request) + + threepids = yield self.hs.get_datastore().user_get_threepids( + auth_user.to_string() + ) + + defer.returnValue((200, {'threepids': threepids})) + @defer.inlineCallbacks def on_POST(self, request): yield run_on_reactor() diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index 8f62e5c6f2..08d60f0817 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -186,3 +186,14 @@ class RegistrationStore(SQLBaseStore): "validated_at": validated_at, "added_at": added_at, }) + + @defer.inlineCallbacks + def user_get_threepids(self, user_id): + ret = yield self._simple_select_list( + "user_threepids", { + "user": user_id + }, + ['medium', 'address', 'validated_at', 'added_at'], + 'user_get_threepids' + ) + defer.returnValue(ret) \ No newline at end of file From 8db6832db8a8ad1a68ff6781b90f3e2cb1a72fc0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Apr 2015 19:53:47 +0100 Subject: [PATCH 35/47] Password reset, finally. --- synapse/handlers/auth.py | 8 +++++++- synapse/rest/client/v2_alpha/account.py | 21 ++++++++++++++++----- synapse/storage/registration.py | 16 +++++++++++++++- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 1f927e67ad..7b0ab4829b 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -195,12 +195,18 @@ class AuthHandler(BaseHandler): def _check_email_identity(self, authdict, _): yield run_on_reactor() + if 'threepidCreds' not in authdict: + raise LoginError(400, "Missing threepidCreds", Codes.MISSING_PARAM) + threepidCreds = authdict['threepidCreds'] identity_handler = self.hs.get_handlers().identity_handler - logger.debug("Getting validated threepid. threepidcreds: %r" % (threepidCreds,)) + logger.info("Getting validated threepid. threepidcreds: %r" % (threepidCreds,)) threepid = yield identity_handler.threepid_from_creds(threepidCreds) + if not threepid: + raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) + threepid['threepidCreds'] = authdict['threepidCreds'] defer.returnValue(threepid) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 5ac3ac0f71..e33607b799 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -45,31 +45,42 @@ class PasswordRestServlet(RestServlet): body = parse_json_dict_from_request(request) authed, result, params = yield self.auth_handler.check_auth([ - [LoginType.PASSWORD] + [LoginType.PASSWORD], + [LoginType.EMAIL_IDENTITY] ], body) if not authed: defer.returnValue((401, result)) - auth_user = None + user_id = None if LoginType.PASSWORD in result: # if using password, they should also be logged in auth_user, client = yield self.auth.get_user_by_req(request) if auth_user.to_string() != result[LoginType.PASSWORD]: raise LoginError(400, "", Codes.UNKNOWN) + user_id = auth_user.to_string() + elif LoginType.EMAIL_IDENTITY in result: + threepid = result[LoginType.EMAIL_IDENTITY] + if 'medium' not in threepid or 'address' not in threepid: + raise SynapseError(500, "Malformed threepid") + # if using email, we must know about the email they're authing with! + threepid_user = yield self.hs.get_datastore().get_user_by_threepid( + threepid['medium'], threepid['address'] + ) + if not threepid_user: + raise SynapseError(404, "Email address not found", Codes.NOT_FOUND) + user_id = threepid_user else: logger.error("Auth succeeded but no known type!", result.keys()) raise SynapseError(500, "", Codes.UNKNOWN) - user_id = auth_user.to_string() - if 'new_password' not in params: raise SynapseError(400, "", Codes.MISSING_PARAM) new_password = params['new_password'] yield self.login_handler.set_password( - user_id, new_password, client.token_id + user_id, new_password, None ) defer.returnValue((200, {})) diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index 08d60f0817..ab43856023 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -196,4 +196,18 @@ class RegistrationStore(SQLBaseStore): ['medium', 'address', 'validated_at', 'added_at'], 'user_get_threepids' ) - defer.returnValue(ret) \ No newline at end of file + defer.returnValue(ret) + + @defer.inlineCallbacks + def get_user_by_threepid(self, medium, address): + ret = yield self._simple_select_one( + "user_threepids", + { + "medium": medium, + "address": address + }, + ['user'], True, 'get_user_by_threepid' + ) + if ret: + defer.returnValue(ret['user']) + defer.returnValue(None) \ No newline at end of file From 91c8f828e13c71fb4d5124191740b0c9ac8a4bc2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 17 Apr 2015 19:56:04 +0100 Subject: [PATCH 36/47] pep8 --- synapse/storage/registration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index ab43856023..f85cbb0d9d 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -210,4 +210,4 @@ class RegistrationStore(SQLBaseStore): ) if ret: defer.returnValue(ret['user']) - defer.returnValue(None) \ No newline at end of file + defer.returnValue(None) From 2e0d9219b9585801ac8a8b5f3911107643ea3519 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 23 Apr 2015 11:45:29 +0100 Subject: [PATCH 37/47] Remove now-redundant email config --- synapse/config/email.py | 42 ------------------------------------ synapse/config/homeserver.py | 3 +-- 2 files changed, 1 insertion(+), 44 deletions(-) delete mode 100644 synapse/config/email.py diff --git a/synapse/config/email.py b/synapse/config/email.py deleted file mode 100644 index f0854f8c37..0000000000 --- a/synapse/config/email.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2014, 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 ._base import Config - - -class EmailConfig(Config): - - def __init__(self, args): - super(EmailConfig, self).__init__(args) - self.email_from_address = args.email_from_address - self.email_smtp_server = args.email_smtp_server - - @classmethod - def add_arguments(cls, parser): - super(EmailConfig, cls).add_arguments(parser) - email_group = parser.add_argument_group("email") - email_group.add_argument( - "--email-from-address", - default="FROM@EXAMPLE.COM", - help="The address to send emails from (e.g. for password resets)." - ) - email_group.add_argument( - "--email-smtp-server", - default="", - help=( - "The SMTP server to send emails from (e.g. for password" - " resets)." - ) - ) diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 3edfadb98b..efbdd93c25 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -20,7 +20,6 @@ from .database import DatabaseConfig from .ratelimiting import RatelimitConfig from .repository import ContentRepositoryConfig from .captcha import CaptchaConfig -from .email import EmailConfig from .voip import VoipConfig from .registration import RegistrationConfig from .metrics import MetricsConfig @@ -29,7 +28,7 @@ from .appservice import AppServiceConfig class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig, RatelimitConfig, ContentRepositoryConfig, CaptchaConfig, - EmailConfig, VoipConfig, RegistrationConfig, + VoipConfig, RegistrationConfig, MetricsConfig, AppServiceConfig,): pass From a2c10d37d7052a1ab6cf7188c3b4d763850e1561 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 23 Apr 2015 13:23:44 +0100 Subject: [PATCH 38/47] Add an error code to 'missing token' response. --- synapse/api/auth.py | 3 ++- synapse/api/errors.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 3d2b45d217..11f76c06f7 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -372,7 +372,8 @@ class Auth(object): defer.returnValue((user, ClientInfo(device_id, token_id))) except KeyError: raise AuthError( - self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token." + self.TOKEN_NOT_FOUND_HTTP_STATUS, "Missing access token.", + errcode=Codes.MISSING_TOKEN ) @defer.inlineCallbacks diff --git a/synapse/api/errors.py b/synapse/api/errors.py index eddd889778..109547b3ce 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -31,6 +31,7 @@ class Codes(object): BAD_PAGINATION = "M_BAD_PAGINATION" UNKNOWN = "M_UNKNOWN" NOT_FOUND = "M_NOT_FOUND" + MISSING_TOKEN = "M_MISSING_TOKEN" UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN" LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED" From 0eb61a3d16bffa83b0963418fa17a8cf6c760631 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 23 Apr 2015 14:44:12 +0100 Subject: [PATCH 39/47] Remove ultimately unused feature of saving params from the first call in the session: it's probably too open to abuse. --- synapse/handlers/auth.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 7b0ab4829b..ac07add2f7 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -78,8 +78,16 @@ class AuthHandler(BaseHandler): sess = self._get_session_info(sid) if len(clientdict) > 0: - sess['clientdict'] = clientdict - self._save_session(sess) + # This was designed to allow the client to omit the parameters + # and just supply the session in subsequent calls so it split + # auth between devices by just sharing the session, (eg. so you + # could continue registration from your phone having clicked the + # email auth link on there). It's probably too open to abuse + # because it lets unauthenticated clients store arbitrary objects + # on a home server. + #sess['clientdict'] = clientdict + #self._save_session(sess) + pass elif 'clientdict' in sess: clientdict = sess['clientdict'] From 03eb4adc6ead31b69af6a87b8d05ae7e0e965fd0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 23 Apr 2015 18:20:17 +0100 Subject: [PATCH 40/47] Dedicated error code for failed 3pid auth verification --- synapse/api/errors.py | 1 + synapse/rest/client/v2_alpha/account.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 109547b3ce..e8b9ee533d 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -39,6 +39,7 @@ class Codes(object): MISSING_PARAM = "M_MISSING_PARAM", TOO_LARGE = "M_TOO_LARGE", EXCLUSIVE = "M_EXCLUSIVE" + THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED" class CodeMessageException(RuntimeError): diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index e33607b799..4d199bbbb8 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -126,7 +126,9 @@ class ThreepidRestServlet(RestServlet): threepid = yield self.identity_handler.threepid_from_creds(threePidCreds) if not threepid: - raise SynapseError(400, "Failed to auth 3pid") + raise SynapseError( + 400, "Failed to auth 3pid", Codes.THREEPID_AUTH_FAILED + ) for reqd in ['medium', 'address', 'validatedAt']: if reqd not in threepid: From f7a79a37beb6bbb217b53a1d8d93a33cf577e6ac Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 24 Apr 2015 09:42:37 +0100 Subject: [PATCH 41/47] pep8 --- synapse/handlers/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index ac07add2f7..34d7080fab 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -85,8 +85,8 @@ class AuthHandler(BaseHandler): # email auth link on there). It's probably too open to abuse # because it lets unauthenticated clients store arbitrary objects # on a home server. - #sess['clientdict'] = clientdict - #self._save_session(sess) + # sess['clientdict'] = clientdict + # self._save_session(sess) pass elif 'clientdict' in sess: clientdict = sess['clientdict'] From a21861962608726a5fe443762421c80119517778 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 24 Apr 2015 11:27:38 +0100 Subject: [PATCH 42/47] Use underscores instead of camelcase for id server stuff --- synapse/handlers/auth.py | 12 ++++++------ synapse/handlers/identity.py | 12 ++++++------ synapse/rest/client/v2_alpha/register.py | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 34d7080fab..ef3219b38e 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -203,19 +203,19 @@ class AuthHandler(BaseHandler): def _check_email_identity(self, authdict, _): yield run_on_reactor() - if 'threepidCreds' not in authdict: - raise LoginError(400, "Missing threepidCreds", Codes.MISSING_PARAM) + if 'threepid_creds' not in authdict: + raise LoginError(400, "Missing threepid_creds", Codes.MISSING_PARAM) - threepidCreds = authdict['threepidCreds'] + threepid_creds = authdict['threepid_creds'] identity_handler = self.hs.get_handlers().identity_handler - logger.info("Getting validated threepid. threepidcreds: %r" % (threepidCreds,)) - threepid = yield identity_handler.threepid_from_creds(threepidCreds) + logger.info("Getting validated threepid. threepidcreds: %r" % (threepid_creds,)) + threepid = yield identity_handler.threepid_from_creds(threepid_creds) if not threepid: raise LoginError(401, "", errcode=Codes.UNAUTHORIZED) - threepid['threepidCreds'] = authdict['threepidCreds'] + threepid['threepid_creds'] = authdict['threepid_creds'] defer.returnValue(threepid) diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index 5c72635915..3ddd834c61 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -44,19 +44,19 @@ class IdentityHandler(BaseHandler): # XXX: make this configurable! # trustedIdServers = ['matrix.org', 'localhost:8090'] trustedIdServers = ['matrix.org'] - if not creds['idServer'] in trustedIdServers: + if not creds['id_server'] in trustedIdServers: logger.warn('%s is not a trusted ID server: rejecting 3pid ' + - 'credentials', creds['idServer']) + 'credentials', creds['id_server']) defer.returnValue(None) data = {} try: data = yield http_client.get_json( "http://%s%s" % ( - creds['idServer'], + creds['id_server'], "/_matrix/identity/api/v1/3pid/getValidated3pid" ), - {'sid': creds['sid'], 'clientSecret': creds['clientSecret']} + {'sid': creds['sid'], 'client_secret': creds['client_secret']} ) except CodeMessageException as e: data = json.loads(e.msg) @@ -75,11 +75,11 @@ class IdentityHandler(BaseHandler): data = yield http_client.post_urlencoded_get_json( # XXX: Change when ID servers are all HTTPS "http://%s%s" % ( - creds['idServer'], "/_matrix/identity/api/v1/3pid/bind" + creds['id_server'], "/_matrix/identity/api/v1/3pid/bind" ), { 'sid': creds['sid'], - 'clientSecret': creds['clientSecret'], + 'client_secret': creds['client_secret'], 'mxid': mxid, } ) diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index e93897e285..dd176c7e77 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -136,11 +136,11 @@ class RegisterRestServlet(RestServlet): logger.info("bind_email specified: binding") emailThreepid = result[LoginType.EMAIL_IDENTITY] - threepidCreds = emailThreepid['threepidCreds'] + threepid_creds = emailThreepid['threepid_creds'] logger.debug("Binding emails %s to %s" % ( emailThreepid, user_id )) - yield self.identity_handler.bind_threepid(threepidCreds, user_id) + yield self.identity_handler.bind_threepid(threepid_creds, user_id) else: logger.info("bind_email not specified: not binding email") From 7ac8a60c6fdd1555cc86203d7c78415d9d236661 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 24 Apr 2015 11:44:27 +0100 Subject: [PATCH 43/47] More underscores --- synapse/rest/client/v2_alpha/account.py | 4 ++-- synapse/rest/client/v2_alpha/register.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 4d199bbbb8..394e6b3809 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -130,7 +130,7 @@ class ThreepidRestServlet(RestServlet): 400, "Failed to auth 3pid", Codes.THREEPID_AUTH_FAILED ) - for reqd in ['medium', 'address', 'validatedAt']: + for reqd in ['medium', 'address', 'validated_at']: if reqd not in threepid: logger.warn("Couldn't add 3pid: invalid response from ID sevrer") raise SynapseError(500, "Invalid response from ID Server") @@ -139,7 +139,7 @@ class ThreepidRestServlet(RestServlet): auth_user.to_string(), threepid['medium'], threepid['address'], - threepid['validatedAt'], + threepid['validated_at'], ) if 'bind' in body and body['bind']: diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index dd176c7e77..3640fb4a29 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -121,7 +121,7 @@ class RegisterRestServlet(RestServlet): if LoginType.EMAIL_IDENTITY in result: threepid = result[LoginType.EMAIL_IDENTITY] - for reqd in ['medium', 'address', 'validatedAt']: + for reqd in ['medium', 'address', 'validated_at']: if reqd not in threepid: logger.info("Can't add incomplete 3pid") else: @@ -129,7 +129,7 @@ class RegisterRestServlet(RestServlet): user_id, threepid['medium'], threepid['address'], - threepid['validatedAt'], + threepid['validated_at'], ) if 'bind_email' in params and params['bind_email']: From 1bac74b9aea46f9e46152955ecf06d8cc7eacdd3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 24 Apr 2015 14:48:49 +0100 Subject: [PATCH 44/47] Change to https for ID server communication --- synapse/handlers/identity.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py index 3ddd834c61..ad8246b58c 100644 --- a/synapse/handlers/identity.py +++ b/synapse/handlers/identity.py @@ -52,7 +52,7 @@ class IdentityHandler(BaseHandler): data = {} try: data = yield http_client.get_json( - "http://%s%s" % ( + "https://%s%s" % ( creds['id_server'], "/_matrix/identity/api/v1/3pid/getValidated3pid" ), @@ -73,8 +73,7 @@ class IdentityHandler(BaseHandler): data = None try: data = yield http_client.post_urlencoded_get_json( - # XXX: Change when ID servers are all HTTPS - "http://%s%s" % ( + "https://%s%s" % ( creds['id_server'], "/_matrix/identity/api/v1/3pid/bind" ), { From f1acb9fd40475563acbb01758b25099d2095a524 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 27 Apr 2015 11:56:34 +0100 Subject: [PATCH 45/47] logging args --- synapse/rest/client/v2_alpha/account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index 394e6b3809..bd57d11dc0 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -143,9 +143,9 @@ class ThreepidRestServlet(RestServlet): ) if 'bind' in body and body['bind']: - logger.debug("Binding emails %s to %s" % ( + logger.debug("Binding emails %s to %s", threepid, auth_user.to_string() - )) + ) yield self.identity_handler.bind_threepid( threePidCreds, auth_user.to_string() ) From 03c4f0ed67fe9e2aab78fc55ff10d554c0cd7317 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 27 Apr 2015 12:36:59 +0100 Subject: [PATCH 46/47] pep8 --- synapse/rest/client/v2_alpha/account.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/rest/client/v2_alpha/account.py b/synapse/rest/client/v2_alpha/account.py index bd57d11dc0..3e522ad39b 100644 --- a/synapse/rest/client/v2_alpha/account.py +++ b/synapse/rest/client/v2_alpha/account.py @@ -143,7 +143,8 @@ class ThreepidRestServlet(RestServlet): ) if 'bind' in body and body['bind']: - logger.debug("Binding emails %s to %s", + logger.debug( + "Binding emails %s to %s", threepid, auth_user.to_string() ) yield self.identity_handler.bind_threepid( From 412ece18e7edb87053a3684e49d5dd485f88a65d Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 27 Apr 2015 14:08:45 +0100 Subject: [PATCH 47/47] Add commentage. --- synapse/handlers/auth.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index ef3219b38e..2e8009d3c3 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -121,6 +121,10 @@ class AuthHandler(BaseHandler): @defer.inlineCallbacks def add_oob_auth(self, stagetype, authdict, clientip): + """ + Adds the result of out-of-band authentication into an existing auth + session. Currently used for adding the result of fallback auth. + """ if stagetype not in self.checkers: raise LoginError(400, "", Codes.MISSING_PARAM) if 'session' not in authdict: