diff --git a/clientapi/routing/admin_whois.go b/clientapi/routing/admin_whois.go new file mode 100644 index 000000000..b448791c3 --- /dev/null +++ b/clientapi/routing/admin_whois.go @@ -0,0 +1,88 @@ +// Copyright 2020 David Spenler +// +// 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. + +package routing + +import ( + "net/http" + + "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/userapi/api" + + "github.com/matrix-org/util" +) + +type adminWhoisResponse struct { + UserID string `json:"user_id"` + Devices map[string]deviceInfo `json:"devices"` +} + +type deviceInfo struct { + Sessions []sessionInfo `json:"sessions"` +} + +type sessionInfo struct { + Connections []connectionInfo `json:"connections"` +} + +type connectionInfo struct { + IP string `json:"ip"` + LastSeen int64 `json:"last_seen"` + UserAgent string `json:"user_agent"` +} + +// GetAdminWhois implements GET /admin/whois/{userId} +func GetAdminWhois( + req *http.Request, userAPI api.UserInternalAPI, device *api.Device, + userID string, +) util.JSONResponse { + if userID != device.UserID { + // TODO: Still allow if user is admin + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("userID does not match the current user"), + } + } + + var queryRes api.QueryDevicesResponse + err := userAPI.QueryDevices(req.Context(), &api.QueryDevicesRequest{ + UserID: userID, + }, &queryRes) + if err != nil { + util.GetLogger(req.Context()).WithError(err).Error("GetAdminWhois failed to query user devices") + return jsonerror.InternalServerError() + } + + devices := make(map[string]deviceInfo) + for _, device := range queryRes.Devices { + connInfo := connectionInfo{ + IP: device.LastSeenIP, + LastSeen: device.LastSeenTS, + UserAgent: device.UserAgent, + } + dev, ok := devices[device.ID] + if !ok { + dev.Sessions = []sessionInfo{{}} + } + dev.Sessions[0].Connections = append(dev.Sessions[0].Connections, connInfo) + devices[device.ID] = dev + } + return util.JSONResponse{ + Code: http.StatusOK, + JSON: adminWhoisResponse{ + UserID: userID, + Devices: devices, + }, + } +} diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 99d1bd099..65b622b3a 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -651,6 +651,16 @@ func Setup( }), ).Methods(http.MethodGet) + r0mux.Handle("/admin/whois/{userID}", + httputil.MakeAuthAPI("admin_whois", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) + if err != nil { + return util.ErrorResponse(err) + } + return GetAdminWhois(req, userAPI, device, vars["userID"]) + }), + ).Methods(http.MethodGet) + r0mux.Handle("/user_directory/search", httputil.MakeAuthAPI("userdirectory_search", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.rateLimit(req); r != nil { diff --git a/sytest-whitelist b/sytest-whitelist index 096a1f15e..17bf2581d 100644 --- a/sytest-whitelist +++ b/sytest-whitelist @@ -495,3 +495,4 @@ Users cannot set notifications powerlevel higher than their own Forgetting room does not show up in v2 /sync Can forget room you've been kicked from Can re-join room if re-invited +/whois diff --git a/userapi/storage/devices/postgres/devices_table.go b/userapi/storage/devices/postgres/devices_table.go index 379fed794..cc554fe7e 100644 --- a/userapi/storage/devices/postgres/devices_table.go +++ b/userapi/storage/devices/postgres/devices_table.go @@ -77,7 +77,7 @@ const selectDeviceByIDSQL = "" + "SELECT display_name FROM device_devices WHERE localpart = $1 and device_id = $2" const selectDevicesByLocalpartSQL = "" + - "SELECT device_id, display_name FROM device_devices WHERE localpart = $1 AND device_id != $2" + "SELECT device_id, display_name, last_seen_ts, ip, user_agent FROM device_devices WHERE localpart = $1 AND device_id != $2" const updateDeviceNameSQL = "" + "UPDATE device_devices SET display_name = $1 WHERE localpart = $2 AND device_id = $3" @@ -281,8 +281,9 @@ func (s *devicesStatements) selectDevicesByLocalpart( for rows.Next() { var dev api.Device - var id, displayname sql.NullString - err = rows.Scan(&id, &displayname) + var lastseents sql.NullInt64 + var id, displayname, ip, useragent sql.NullString + err = rows.Scan(&id, &displayname, &lastseents, &ip, &useragent) if err != nil { return devices, err } @@ -292,6 +293,16 @@ func (s *devicesStatements) selectDevicesByLocalpart( if displayname.Valid { dev.DisplayName = displayname.String } + if lastseents.Valid { + dev.LastSeenTS = lastseents.Int64 + } + if ip.Valid { + dev.LastSeenIP = ip.String + } + if useragent.Valid { + dev.UserAgent = useragent.String + } + dev.UserID = userutil.MakeUserID(localpart, s.serverName) devices = append(devices, dev) } diff --git a/userapi/storage/devices/sqlite3/devices_table.go b/userapi/storage/devices/sqlite3/devices_table.go index 26c03222a..cdfe2bb91 100644 --- a/userapi/storage/devices/sqlite3/devices_table.go +++ b/userapi/storage/devices/sqlite3/devices_table.go @@ -62,7 +62,7 @@ const selectDeviceByIDSQL = "" + "SELECT display_name FROM device_devices WHERE localpart = $1 and device_id = $2" const selectDevicesByLocalpartSQL = "" + - "SELECT device_id, display_name FROM device_devices WHERE localpart = $1 AND device_id != $2" + "SELECT device_id, display_name, last_seen_ts, ip, user_agent FROM device_devices WHERE localpart = $1 AND device_id != $2" const updateDeviceNameSQL = "" + "UPDATE device_devices SET display_name = $1 WHERE localpart = $2 AND device_id = $3" @@ -256,8 +256,9 @@ func (s *devicesStatements) selectDevicesByLocalpart( for rows.Next() { var dev api.Device - var id, displayname sql.NullString - err = rows.Scan(&id, &displayname) + var lastseents sql.NullInt64 + var id, displayname, ip, useragent sql.NullString + err = rows.Scan(&id, &displayname, &lastseents, &ip, &useragent) if err != nil { return devices, err } @@ -267,6 +268,16 @@ func (s *devicesStatements) selectDevicesByLocalpart( if displayname.Valid { dev.DisplayName = displayname.String } + if lastseents.Valid { + dev.LastSeenTS = lastseents.Int64 + } + if ip.Valid { + dev.LastSeenIP = ip.String + } + if useragent.Valid { + dev.UserAgent = useragent.String + } + dev.UserID = userutil.MakeUserID(localpart, s.serverName) devices = append(devices, dev) }