SYN-48: Implement WHOIS rest servlet

This commit is contained in:
Erik Johnston 2014-09-29 14:59:52 +01:00
parent c65306f877
commit 3ccb17ce59
9 changed files with 190 additions and 25 deletions

View file

@ -220,7 +220,8 @@ class Auth(object):
# Can optionally look elsewhere in the request (e.g. headers) # Can optionally look elsewhere in the request (e.g. headers)
try: try:
access_token = request.args["access_token"][0] 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) ip_addr = self.hs.get_ip_from_request(request)
user_agent = request.requestHeaders.getRawHeaders( user_agent = request.requestHeaders.getRawHeaders(
@ -229,10 +230,11 @@ class Auth(object):
)[0] )[0]
if user and access_token and ip_addr: if user and access_token and ip_addr:
self.store.insert_client_ip( self.store.insert_client_ip(
user, user=user,
access_token, access_token=access_token,
ip_addr, device_id=user_info["device_id"],
user_agent ip=ip_addr,
user_agent=user_agent
) )
defer.returnValue(user) defer.returnValue(user)
@ -246,15 +248,23 @@ class Auth(object):
Args: Args:
token (str)- The access token to get the user by. token (str)- The access token to get the user by.
Returns: 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: Raises:
AuthError if no user by that token exists or the token is invalid. AuthError if no user by that token exists or the token is invalid.
""" """
try: try:
user_id = yield self.store.get_user_by_token(token=token) ret = yield self.store.get_user_by_token(token=token)
if not user_id: if not ret:
raise StoreError() 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: except StoreError:
raise AuthError(403, "Unrecognised access token.", raise AuthError(403, "Unrecognised access token.",
errcode=Codes.UNKNOWN_TOKEN) errcode=Codes.UNKNOWN_TOKEN)

View file

@ -25,6 +25,7 @@ from .profile import ProfileHandler
from .presence import PresenceHandler from .presence import PresenceHandler
from .directory import DirectoryHandler from .directory import DirectoryHandler
from .typing import TypingNotificationHandler from .typing import TypingNotificationHandler
from .admin import AdminHandler
class Handlers(object): class Handlers(object):
@ -49,3 +50,4 @@ class Handlers(object):
self.login_handler = LoginHandler(hs) self.login_handler = LoginHandler(hs)
self.directory_handler = DirectoryHandler(hs) self.directory_handler = DirectoryHandler(hs)
self.typing_notification_handler = TypingNotificationHandler(hs) self.typing_notification_handler = TypingNotificationHandler(hs)
self.admin_handler = AdminHandler(hs)

62
synapse/handlers/admin.py Normal file
View file

@ -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)

View file

@ -15,7 +15,8 @@
from . import ( 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) initial_sync.register_servlets(hs, client_resource)
directory.register_servlets(hs, client_resource) directory.register_servlets(hs, client_resource)
voip.register_servlets(hs, client_resource) voip.register_servlets(hs, client_resource)
admin.register_servlets(hs, client_resource)

47
synapse/rest/admin.py Normal file
View file

@ -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<user_id>[^/]*)")
@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)

View file

@ -294,18 +294,54 @@ class DataStore(RoomMemberStore, RoomStore,
defer.returnValue(self.min_token) 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( return self._simple_insert(
"user_ips", "user_ips",
{ {
"user": user.to_string(), "user": user.to_string(),
"access_token": access_token, "access_token": access_token,
"device_id": device_id,
"ip": ip, "ip": ip,
"user_agent": user_agent, "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): def snapshot_room(self, room_id, user_id, state_type=None, state_key=None):
"""Snapshot the room for an update by a user """Snapshot the room for an update by a user
Args: Args:

View file

@ -88,7 +88,6 @@ class RegistrationStore(SQLBaseStore):
query, user_id query, user_id
) )
@defer.inlineCallbacks
def get_user_by_token(self, token): def get_user_by_token(self, token):
"""Get a user from the given access token. """Get a user from the given access token.
@ -99,11 +98,11 @@ class RegistrationStore(SQLBaseStore):
Raises: Raises:
StoreError if no user was found. StoreError if no user was found.
""" """
user_id = yield self.runInteraction(self._query_for_auth, return self.runInteraction(
token) self._query_for_auth,
defer.returnValue(user_id) token
)
@defer.inlineCallbacks
def is_server_admin(self, user): def is_server_admin(self, user):
return self._simple_select_one_onecol( return self._simple_select_one_onecol(
table="users", table="users",
@ -112,11 +111,16 @@ class RegistrationStore(SQLBaseStore):
) )
def _query_for_auth(self, txn, token): def _query_for_auth(self, txn, token):
txn.execute("SELECT users.name FROM access_tokens LEFT JOIN users" + sql = (
" ON users.id = access_tokens.user_id WHERE token = ?", "SELECT users.name, users.admin, access_tokens.device_id "
[token]) "FROM users "
row = txn.fetchone() "INNER JOIN access_tokens on users.id = access_tokens.user_id "
if row: "WHERE token = ?"
return row[0] )
cursor = txn.execute(sql, (token,))
rows = self.cursor_to_dict(cursor)
if rows:
return rows[0]
raise StoreError(404, "Token not found.") raise StoreError(404, "Token not found.")

View file

@ -2,9 +2,10 @@
CREATE TABLE IF NOT EXISTS user_ips ( CREATE TABLE IF NOT EXISTS user_ips (
user TEXT NOT NULL, user TEXT NOT NULL,
access_token TEXT NOT NULL, access_token TEXT NOT NULL,
device_id TEXT,
ip TEXT NOT NULL, ip TEXT NOT NULL,
user_agent 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 CONSTRAINT user_ip UNIQUE (user, access_token, ip, user_agent) ON CONFLICT REPLACE
); );

View file

@ -34,9 +34,10 @@ CREATE TABLE IF NOT EXISTS access_tokens(
CREATE TABLE IF NOT EXISTS user_ips ( CREATE TABLE IF NOT EXISTS user_ips (
user TEXT NOT NULL, user TEXT NOT NULL,
access_token TEXT NOT NULL, access_token TEXT NOT NULL,
device_id TEXT,
ip TEXT NOT NULL, ip TEXT NOT NULL,
user_agent 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 CONSTRAINT user_ip UNIQUE (user, access_token, ip, user_agent) ON CONFLICT REPLACE
); );