From 0fdf3088743b25e9fcc39b7b3b4c7f6e8332e26c Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Fri, 26 Sep 2014 16:36:24 +0100 Subject: Track the IP users connect with. Add an admin column to users table. --- synapse/rest/register.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) (limited to 'synapse/rest') diff --git a/synapse/rest/register.py b/synapse/rest/register.py index 4935e323d9..804117ee09 100644 --- a/synapse/rest/register.py +++ b/synapse/rest/register.py @@ -195,13 +195,7 @@ class RegisterRestServlet(RestServlet): raise SynapseError(400, "Captcha response is required", errcode=Codes.CAPTCHA_NEEDED) - # May be an X-Forwarding-For header depending on config - ip_addr = request.getClientIP() - if self.hs.config.captcha_ip_origin_is_x_forwarded: - # use the header - if request.requestHeaders.hasHeader("X-Forwarded-For"): - ip_addr = request.requestHeaders.getRawHeaders( - "X-Forwarded-For")[0] + ip_addr = self.hs.get_ip_from_request(request) handler = self.handlers.registration_handler yield handler.check_recaptcha( -- cgit 1.5.1 From 3ccb17ce592d7e75e0bd0237c347d64f63d5eb10 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 29 Sep 2014 14:59:52 +0100 Subject: SYN-48: Implement WHOIS rest servlet --- synapse/api/auth.py | 28 +++++++++++------ synapse/handlers/__init__.py | 2 ++ synapse/handlers/admin.py | 62 +++++++++++++++++++++++++++++++++++++ synapse/rest/__init__.py | 4 ++- synapse/rest/admin.py | 47 ++++++++++++++++++++++++++++ synapse/storage/__init__.py | 40 ++++++++++++++++++++++-- synapse/storage/registration.py | 26 +++++++++------- synapse/storage/schema/delta/v5.sql | 3 +- synapse/storage/schema/users.sql | 3 +- 9 files changed, 190 insertions(+), 25 deletions(-) create mode 100644 synapse/handlers/admin.py create mode 100644 synapse/rest/admin.py (limited to 'synapse/rest') diff --git a/synapse/api/auth.py b/synapse/api/auth.py index 5e3ea5b8c5..8f7982c7fa 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -220,7 +220,8 @@ class Auth(object): # Can optionally look elsewhere in the request (e.g. headers) try: access_token = request.args["access_token"][0] - user = yield self.get_user_by_token(access_token) + user_info = yield self.get_user_by_token(access_token) + user = user_info["user"] ip_addr = self.hs.get_ip_from_request(request) user_agent = request.requestHeaders.getRawHeaders( @@ -229,10 +230,11 @@ class Auth(object): )[0] if user and access_token and ip_addr: self.store.insert_client_ip( - user, - access_token, - ip_addr, - user_agent + user=user, + access_token=access_token, + device_id=user_info["device_id"], + ip=ip_addr, + user_agent=user_agent ) defer.returnValue(user) @@ -246,15 +248,23 @@ class Auth(object): Args: token (str)- The access token to get the user by. Returns: - UserID : User ID object of the user who has that access token. + dict : dict that includes the user, device_id, and whether the + user is a server admin. Raises: AuthError if no user by that token exists or the token is invalid. """ try: - user_id = yield self.store.get_user_by_token(token=token) - if not user_id: + ret = yield self.store.get_user_by_token(token=token) + if not ret: raise StoreError() - defer.returnValue(self.hs.parse_userid(user_id)) + + user_info = { + "admin": bool(ret.get("admin", False)), + "device_id": ret.get("device_id"), + "user": self.hs.parse_userid(ret.get("name")), + } + + defer.returnValue(user_info) except StoreError: raise AuthError(403, "Unrecognised access token.", errcode=Codes.UNKNOWN_TOKEN) diff --git a/synapse/handlers/__init__.py b/synapse/handlers/__init__.py index 5308e2c8e1..d5df3c630b 100644 --- a/synapse/handlers/__init__.py +++ b/synapse/handlers/__init__.py @@ -25,6 +25,7 @@ from .profile import ProfileHandler from .presence import PresenceHandler from .directory import DirectoryHandler from .typing import TypingNotificationHandler +from .admin import AdminHandler class Handlers(object): @@ -49,3 +50,4 @@ class Handlers(object): self.login_handler = LoginHandler(hs) self.directory_handler = DirectoryHandler(hs) self.typing_notification_handler = TypingNotificationHandler(hs) + self.admin_handler = AdminHandler(hs) diff --git a/synapse/handlers/admin.py b/synapse/handlers/admin.py new file mode 100644 index 0000000000..687b343a1d --- /dev/null +++ b/synapse/handlers/admin.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 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 + +import logging + + +logger = logging.getLogger(__name__) + + +class AdminHandler(BaseHandler): + + def __init__(self, hs): + super(AdminHandler, self).__init__(hs) + + @defer.inlineCallbacks + def get_whois(self, user): + res = yield self.store.get_user_ip_and_agents(user) + + d = {} + for r in res: + device = d.setdefault(r["device_id"], {}) + session = device.setdefault(r["access_token"], []) + session.append({ + "ip": r["ip"], + "user_agent": r["user_agent"], + "last_seen": r["last_seen"], + }) + + ret = { + "user_id": user.to_string(), + "devices": [ + { + "device_id": k, + "sessions": [ + { + # "access_token": x, TODO (erikj) + "connections": y, + } + for x, y in v.items() + ] + } + for k, v in d.items() + ], + } + + defer.returnValue(ret) diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index 3b9aa59733..e391e5678d 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -15,7 +15,8 @@ from . import ( - room, events, register, login, profile, presence, initial_sync, directory, voip + room, events, register, login, profile, presence, initial_sync, directory, + voip, admin, ) @@ -43,3 +44,4 @@ class RestServletFactory(object): initial_sync.register_servlets(hs, client_resource) directory.register_servlets(hs, client_resource) voip.register_servlets(hs, client_resource) + admin.register_servlets(hs, client_resource) diff --git a/synapse/rest/admin.py b/synapse/rest/admin.py new file mode 100644 index 0000000000..97eb1954e0 --- /dev/null +++ b/synapse/rest/admin.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer + +from synapse.api.errors import AuthError, SynapseError +from base import RestServlet, client_path_pattern + +import logging + +logger = logging.getLogger(__name__) + + +class WhoisRestServlet(RestServlet): + PATTERN = client_path_pattern("/admin/whois/(?P[^/]*)") + + @defer.inlineCallbacks + def on_GET(self, request, user_id): + target_user = self.hs.parse_userid(user_id) + auth_user = yield self.auth.get_user_by_req(request) + is_admin = yield self.auth.is_server_admin(auth_user) + + if not is_admin and target_user != auth_user: + raise AuthError(403, "You are not a server admin") + + if not target_user.is_mine: + raise SynapseError(400, "Can only whois a local user") + + ret = yield self.handlers.admin_handler.get_whois(auth_user) + + defer.returnValue((200, ret)) + + +def register_servlets(hs, http_server): + WhoisRestServlet(hs).register(http_server) diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 169a80dce4..749347d5a8 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -294,18 +294,54 @@ class DataStore(RoomMemberStore, RoomStore, defer.returnValue(self.min_token) - def insert_client_ip(self, user, access_token, ip, user_agent): + def insert_client_ip(self, user, access_token, device_id, ip, user_agent): return self._simple_insert( "user_ips", { "user": user.to_string(), "access_token": access_token, + "device_id": device_id, "ip": ip, "user_agent": user_agent, - "last_used": int(self._clock.time()), + "last_seen": int(self._clock.time_msec()), } ) + def get_user_ip_and_agents(self, user): + return self._simple_select_list( + table="user_ips", + keyvalues={"user": user.to_string()}, + retcols=[ + "device_id", "access_token", "ip", "user_agent", "last_seen" + ], + ) + + d = {} + for r in res: + device = d.setdefault(r["device_id"], {}) + session = device.setdefault(r["access_token"], []) + session.append({ + "ip": r["ip"], + "user_agent": r["user_agent"], + "last_seen": r["last_seen"], + }) + + defer.returnValue( + [ + { + "device_id": k, + "sessions": [ + { + "access_token": x, + "connections": y, + } + for x, y in v.items() + ] + } + for k, v in d.items() + ] + ) + def snapshot_room(self, room_id, user_id, state_type=None, state_key=None): """Snapshot the room for an update by a user Args: diff --git a/synapse/storage/registration.py b/synapse/storage/registration.py index f32b000cb6..cf43209367 100644 --- a/synapse/storage/registration.py +++ b/synapse/storage/registration.py @@ -88,7 +88,6 @@ class RegistrationStore(SQLBaseStore): query, user_id ) - @defer.inlineCallbacks def get_user_by_token(self, token): """Get a user from the given access token. @@ -99,11 +98,11 @@ class RegistrationStore(SQLBaseStore): Raises: StoreError if no user was found. """ - user_id = yield self.runInteraction(self._query_for_auth, - token) - defer.returnValue(user_id) + return self.runInteraction( + self._query_for_auth, + token + ) - @defer.inlineCallbacks def is_server_admin(self, user): return self._simple_select_one_onecol( table="users", @@ -112,11 +111,16 @@ class RegistrationStore(SQLBaseStore): ) def _query_for_auth(self, txn, token): - txn.execute("SELECT users.name FROM access_tokens LEFT JOIN users" + - " ON users.id = access_tokens.user_id WHERE token = ?", - [token]) - row = txn.fetchone() - if row: - return row[0] + sql = ( + "SELECT users.name, users.admin, access_tokens.device_id " + "FROM users " + "INNER JOIN access_tokens on users.id = access_tokens.user_id " + "WHERE token = ?" + ) + + cursor = txn.execute(sql, (token,)) + rows = self.cursor_to_dict(cursor) + if rows: + return rows[0] raise StoreError(404, "Token not found.") diff --git a/synapse/storage/schema/delta/v5.sql b/synapse/storage/schema/delta/v5.sql index f5a667a250..af9df11aa9 100644 --- a/synapse/storage/schema/delta/v5.sql +++ b/synapse/storage/schema/delta/v5.sql @@ -2,9 +2,10 @@ CREATE TABLE IF NOT EXISTS user_ips ( user TEXT NOT NULL, access_token TEXT NOT NULL, + device_id TEXT, ip TEXT NOT NULL, user_agent TEXT NOT NULL, - last_used INTEGER NOT NULL, + last_seen INTEGER NOT NULL, CONSTRAINT user_ip UNIQUE (user, access_token, ip, user_agent) ON CONFLICT REPLACE ); diff --git a/synapse/storage/schema/users.sql b/synapse/storage/schema/users.sql index d96dd9f075..8244f733bd 100644 --- a/synapse/storage/schema/users.sql +++ b/synapse/storage/schema/users.sql @@ -34,9 +34,10 @@ CREATE TABLE IF NOT EXISTS access_tokens( CREATE TABLE IF NOT EXISTS user_ips ( user TEXT NOT NULL, access_token TEXT NOT NULL, + device_id TEXT, ip TEXT NOT NULL, user_agent TEXT NOT NULL, - last_used INTEGER NOT NULL, + last_seen INTEGER NOT NULL, CONSTRAINT user_ip UNIQUE (user, access_token, ip, user_agent) ON CONFLICT REPLACE ); -- cgit 1.5.1 From 1132663cc7d2b168b32b0b48f65bbdf161d090f4 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Mon, 29 Sep 2014 15:04:04 +0100 Subject: SYN-48: Fix typo. Get the whois for requested user rather tahan the requester --- synapse/rest/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/rest') diff --git a/synapse/rest/admin.py b/synapse/rest/admin.py index 97eb1954e0..ed9b484623 100644 --- a/synapse/rest/admin.py +++ b/synapse/rest/admin.py @@ -38,7 +38,7 @@ class WhoisRestServlet(RestServlet): if not target_user.is_mine: raise SynapseError(400, "Can only whois a local user") - ret = yield self.handlers.admin_handler.get_whois(auth_user) + ret = yield self.handlers.admin_handler.get_whois(target_user) defer.returnValue((200, ret)) -- cgit 1.5.1 From e06adc6d7e645e5116c28d2b5756a70bb8f4f240 Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Tue, 30 Sep 2014 11:31:22 +0100 Subject: SYN-2: Allow server admins to delete room aliases --- synapse/handlers/directory.py | 45 ++++++++++++++++++++++++++--------------- synapse/rest/directory.py | 20 +++++++++++++++++- synapse/storage/directory.py | 30 +++++++++++++++++++++++++++ tests/storage/test_directory.py | 25 +++++++++++++++++++---- 4 files changed, 99 insertions(+), 21 deletions(-) (limited to 'synapse/rest') diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 4ab00a761a..84c3a1d56f 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -57,7 +57,6 @@ class DirectoryHandler(BaseHandler): if not servers: raise SynapseError(400, "Failed to get server list") - try: yield self.store.create_room_alias_association( room_alias, @@ -68,25 +67,19 @@ class DirectoryHandler(BaseHandler): defer.returnValue("Already exists") # TODO: Send the room event. + yield self._update_room_alias_events(user_id, room_id) - aliases = yield self.store.get_aliases_for_room(room_id) - - event = self.event_factory.create_event( - etype=RoomAliasesEvent.TYPE, - state_key=self.hs.hostname, - room_id=room_id, - user_id=user_id, - content={"aliases": aliases}, - ) + @defer.inlineCallbacks + def delete_association(self, user_id, room_alias): + # TODO Check if server admin - snapshot = yield self.store.snapshot_room( - room_id=room_id, - user_id=user_id, - ) + if not room_alias.is_mine: + raise SynapseError(400, "Room alias must be local") - yield self.state_handler.handle_new_event(event, snapshot) - yield self._on_new_room_event(event, snapshot, extra_users=[user_id]) + room_id = yield self.store.delete_room_alias(room_alias) + if room_id: + yield self._update_room_alias_events(user_id, room_id) @defer.inlineCallbacks def get_association(self, room_alias): @@ -142,3 +135,23 @@ class DirectoryHandler(BaseHandler): "room_id": result.room_id, "servers": result.servers, }) + + @defer.inlineCallbacks + def _update_room_alias_events(self, user_id, room_id): + aliases = yield self.store.get_aliases_for_room(room_id) + + event = self.event_factory.create_event( + etype=RoomAliasesEvent.TYPE, + state_key=self.hs.hostname, + room_id=room_id, + user_id=user_id, + content={"aliases": aliases}, + ) + + snapshot = yield self.store.snapshot_room( + room_id=room_id, + user_id=user_id, + ) + + yield self.state_handler.handle_new_event(event, snapshot) + yield self._on_new_room_event(event, snapshot, extra_users=[user_id]) diff --git a/synapse/rest/directory.py b/synapse/rest/directory.py index 31849246a1..6c260e7102 100644 --- a/synapse/rest/directory.py +++ b/synapse/rest/directory.py @@ -16,7 +16,7 @@ from twisted.internet import defer -from synapse.api.errors import SynapseError, Codes +from synapse.api.errors import AuthError, SynapseError, Codes from base import RestServlet, client_path_pattern import json @@ -81,6 +81,24 @@ class ClientDirectoryServer(RestServlet): defer.returnValue((200, {})) + @defer.inlineCallbacks + def on_DELETE(self, request, room_alias): + user = yield self.auth.get_user_by_req(request) + + is_admin = yield self.auth.is_server_admin(user) + if not is_admin: + raise AuthError(403, "You need to be a server admin") + + dir_handler = self.handlers.directory_handler + + room_alias = self.hs.parse_roomalias(urllib.unquote(room_alias)) + + yield dir_handler.delete_association( + user.to_string(), room_alias + ) + + defer.returnValue((200, {})) + def _parse_json(request): try: diff --git a/synapse/storage/directory.py b/synapse/storage/directory.py index 540eb4c2c4..52373a28a6 100644 --- a/synapse/storage/directory.py +++ b/synapse/storage/directory.py @@ -93,6 +93,36 @@ class DirectoryStore(SQLBaseStore): } ) + def delete_room_alias(self, room_alias): + return self.runInteraction( + self._delete_room_alias_txn, + room_alias, + ) + + def _delete_room_alias_txn(self, txn, room_alias): + cursor = txn.execute( + "SELECT room_id FROM room_aliases WHERE room_alias = ?", + (room_alias.to_string(),) + ) + + res = cursor.fetchone() + if res: + room_id = res[0] + else: + return None + + txn.execute( + "DELETE FROM room_aliases WHERE room_alias = ?", + (room_alias.to_string(),) + ) + + txn.execute( + "DELETE FROM room_alias_servers WHERE room_alias = ?", + (room_alias.to_string(),) + ) + + return room_id + def get_aliases_for_room(self, room_id): return self._simple_select_onecol( "room_aliases", diff --git a/tests/storage/test_directory.py b/tests/storage/test_directory.py index 7e8e7e1e83..e9c242cc07 100644 --- a/tests/storage/test_directory.py +++ b/tests/storage/test_directory.py @@ -30,7 +30,8 @@ class DirectoryStoreTestCase(unittest.TestCase): db_pool = SQLiteMemoryDbPool() yield db_pool.prepare() - hs = HomeServer("test", + hs = HomeServer( + "test", db_pool=db_pool, ) @@ -60,9 +61,25 @@ class DirectoryStoreTestCase(unittest.TestCase): servers=["test"], ) - self.assertObjectHasAttributes( - {"room_id": self.room.to_string(), - "servers": ["test"]}, + { + "room_id": self.room.to_string(), + "servers": ["test"], + }, + (yield self.store.get_association_from_room_alias(self.alias)) + ) + + @defer.inlineCallbacks + def test_delete_alias(self): + yield self.store.create_room_alias_association( + room_alias=self.alias, + room_id=self.room.to_string(), + servers=["test"], + ) + + room_id = yield self.store.delete_room_alias(self.alias) + self.assertEqual(self.room.to_string(), room_id) + + self.assertIsNone( (yield self.store.get_association_from_room_alias(self.alias)) ) -- cgit 1.5.1 From 13b560971e71a87bf44c35ae9cb4591333dd576c Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Tue, 14 Oct 2014 16:47:08 +0100 Subject: Make sure to return an empty JSON object ({}) from presence PUT/POST requests rather than an empty string ("") because most deserialisers won't like the latter --- synapse/rest/presence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'synapse/rest') diff --git a/synapse/rest/presence.py b/synapse/rest/presence.py index 7fc8ce4404..138cc88a05 100644 --- a/synapse/rest/presence.py +++ b/synapse/rest/presence.py @@ -68,7 +68,7 @@ class PresenceStatusRestServlet(RestServlet): yield self.handlers.presence_handler.set_state( target_user=user, auth_user=auth_user, state=state) - defer.returnValue((200, "")) + defer.returnValue((200, {})) def on_OPTIONS(self, request): return (200, {}) @@ -141,7 +141,7 @@ class PresenceListRestServlet(RestServlet): yield defer.DeferredList(deferreds) - defer.returnValue((200, "")) + defer.returnValue((200, {})) def on_OPTIONS(self, request): return (200, {}) -- cgit 1.5.1