2017-05-25 13:33:50 +01:00
// Copyright 2017 Vector Creations 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.
2020-02-13 17:27:33 +00:00
package postgres
2017-05-25 13:33:50 +01:00
import (
2017-09-18 15:51:26 +01:00
"context"
2017-05-25 13:33:50 +01:00
"database/sql"
2022-11-11 16:41:37 +00:00
"fmt"
2017-05-25 13:33:50 +01:00
"time"
2020-02-11 12:13:38 +00:00
"github.com/lib/pq"
2018-05-31 15:21:13 +01:00
"github.com/matrix-org/dendrite/clientapi/userutil"
2020-05-21 14:40:13 +01:00
"github.com/matrix-org/dendrite/internal"
2020-06-12 14:55:57 +01:00
"github.com/matrix-org/dendrite/internal/sqlutil"
2020-06-16 14:10:55 +01:00
"github.com/matrix-org/dendrite/userapi/api"
2022-07-25 10:39:22 +01:00
"github.com/matrix-org/dendrite/userapi/storage/postgres/deltas"
2022-02-18 13:51:59 +00:00
"github.com/matrix-org/dendrite/userapi/storage/tables"
2017-05-25 13:33:50 +01:00
"github.com/matrix-org/gomatrixserverlib"
)
const devicesSchema = `
2019-08-23 17:55:40 +01:00
-- This sequence is used for automatic allocation of session_id .
2022-10-18 15:59:08 +01:00
CREATE SEQUENCE IF NOT EXISTS userapi_device_session_id_seq START 1 ;
2019-08-23 17:55:40 +01:00
2017-05-25 13:33:50 +01:00
-- Stores data about devices .
2022-10-18 15:59:08 +01:00
CREATE TABLE IF NOT EXISTS userapi_devices (
2017-05-25 13:33:50 +01:00
-- The access token granted to this device . This has to be the primary key
-- so we can distinguish which device is making a given request .
access_token TEXT NOT NULL PRIMARY KEY ,
2019-08-23 17:55:40 +01:00
-- The auto - allocated unique ID of the session identified by the access token .
-- This can be used as a secure substitution of the access token in situations
-- where data is associated with access tokens ( e . g . transaction storage ) ,
-- so we don ' t have to store users ' access tokens everywhere .
2022-10-18 15:59:08 +01:00
session_id BIGINT NOT NULL DEFAULT nextval ( ' userapi_device_session_id_seq ' ) ,
2017-05-25 13:33:50 +01:00
-- The device identifier . This only needs to uniquely identify a device for a given user , not globally .
-- access_tokens will be clobbered based on the device ID for a user .
device_id TEXT NOT NULL ,
-- The Matrix user ID localpart for this device . This is preferable to storing the full user_id
-- as it is smaller , makes it clearer that we only manage devices for our own users , and may make
-- migration to different domain names easier .
localpart TEXT NOT NULL ,
2022-11-11 16:41:37 +00:00
server_name TEXT NOT NULL ,
2017-05-25 13:33:50 +01:00
-- When this devices was first recognised on the network , as a unix timestamp ( ms resolution ) .
2017-11-14 09:59:02 +00:00
created_ts BIGINT NOT NULL ,
-- The display name , human friendlier than device_id and updatable
2020-10-09 09:17:23 +01:00
display_name TEXT ,
-- The time the device was last used , as a unix timestamp ( ms resolution ) .
last_seen_ts BIGINT NOT NULL ,
-- The last seen IP address of this device
ip TEXT ,
-- User agent of this device
user_agent TEXT
-- TODO : device keys , device display names , token restrictions ( if 3 rd - party OAuth app )
2017-05-25 13:33:50 +01:00
) ;
-- Device IDs must be unique for a given user .
2022-11-11 16:41:37 +00:00
CREATE UNIQUE INDEX IF NOT EXISTS userapi_device_localpart_id_idx ON userapi_devices ( localpart , server_name , device_id ) ;
2017-05-25 13:33:50 +01:00
`
const insertDeviceSQL = "" +
2022-11-11 16:41:37 +00:00
"INSERT INTO userapi_devices(device_id, localpart, server_name, access_token, created_ts, display_name, last_seen_ts, ip, user_agent) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)" +
2019-08-23 17:55:40 +01:00
" RETURNING session_id"
2017-05-25 13:33:50 +01:00
const selectDeviceByTokenSQL = "" +
2022-11-11 16:41:37 +00:00
"SELECT session_id, device_id, localpart, server_name FROM userapi_devices WHERE access_token = $1"
2017-05-25 13:33:50 +01:00
2017-10-17 19:12:54 +01:00
const selectDeviceByIDSQL = "" +
2022-11-11 16:41:37 +00:00
"SELECT display_name, last_seen_ts, ip FROM userapi_devices WHERE localpart = $1 AND server_name = $2 AND device_id = $3"
2017-10-17 19:12:54 +01:00
const selectDevicesByLocalpartSQL = "" +
2023-02-17 10:39:46 +00:00
"SELECT device_id, display_name, last_seen_ts, ip, user_agent, session_id FROM userapi_devices WHERE localpart = $1 AND server_name = $2 AND device_id != $3 ORDER BY last_seen_ts DESC"
2017-11-14 09:59:02 +00:00
const updateDeviceNameSQL = "" +
2022-11-11 16:41:37 +00:00
"UPDATE userapi_devices SET display_name = $1 WHERE localpart = $2 AND server_name = $3 AND device_id = $4"
2017-10-17 19:12:54 +01:00
2017-05-25 13:33:50 +01:00
const deleteDeviceSQL = "" +
2022-11-11 16:41:37 +00:00
"DELETE FROM userapi_devices WHERE device_id = $1 AND localpart = $2 AND server_name = $3"
2017-05-25 13:33:50 +01:00
2017-10-15 11:29:47 +01:00
const deleteDevicesByLocalpartSQL = "" +
2022-11-11 16:41:37 +00:00
"DELETE FROM userapi_devices WHERE localpart = $1 AND server_name = $2 AND device_id != $3"
2017-10-15 11:29:47 +01:00
2020-02-11 12:13:38 +00:00
const deleteDevicesSQL = "" +
2022-11-11 16:41:37 +00:00
"DELETE FROM userapi_devices WHERE localpart = $1 AND server_name = $2 AND device_id = ANY($3)"
2020-02-11 12:13:38 +00:00
2020-07-22 17:04:57 +01:00
const selectDevicesByIDSQL = "" +
2023-02-17 10:39:46 +00:00
"SELECT device_id, localpart, server_name, display_name, last_seen_ts, session_id FROM userapi_devices WHERE device_id = ANY($1) ORDER BY last_seen_ts DESC"
2020-07-22 17:04:57 +01:00
2020-10-09 09:17:23 +01:00
const updateDeviceLastSeen = "" +
2022-11-11 16:41:37 +00:00
"UPDATE userapi_devices SET last_seen_ts = $1, ip = $2, user_agent = $3 WHERE localpart = $4 AND server_name = $5 AND device_id = $6"
2020-10-09 09:17:23 +01:00
2017-05-25 13:33:50 +01:00
type devicesStatements struct {
2017-10-15 11:29:47 +01:00
insertDeviceStmt * sql . Stmt
selectDeviceByTokenStmt * sql . Stmt
2017-10-17 19:12:54 +01:00
selectDeviceByIDStmt * sql . Stmt
selectDevicesByLocalpartStmt * sql . Stmt
2020-07-22 17:04:57 +01:00
selectDevicesByIDStmt * sql . Stmt
2017-11-14 09:59:02 +00:00
updateDeviceNameStmt * sql . Stmt
2020-10-09 09:17:23 +01:00
updateDeviceLastSeenStmt * sql . Stmt
2017-10-15 11:29:47 +01:00
deleteDeviceStmt * sql . Stmt
deleteDevicesByLocalpartStmt * sql . Stmt
2020-02-11 12:13:38 +00:00
deleteDevicesStmt * sql . Stmt
2017-10-17 19:12:54 +01:00
serverName gomatrixserverlib . ServerName
2017-05-25 13:33:50 +01:00
}
2022-02-18 13:51:59 +00:00
func NewPostgresDevicesTable ( db * sql . DB , serverName gomatrixserverlib . ServerName ) ( tables . DevicesTable , error ) {
s := & devicesStatements {
serverName : serverName ,
2017-10-17 19:12:54 +01:00
}
2022-02-18 13:51:59 +00:00
_ , err := db . Exec ( devicesSchema )
if err != nil {
return nil , err
2020-10-09 09:17:23 +01:00
}
2022-07-25 10:39:22 +01:00
m := sqlutil . NewMigrator ( db )
m . AddMigrations ( sqlutil . Migration {
Version : "userapi: add last_seen_ts" ,
Up : deltas . UpLastSeenTSIP ,
} )
err = m . Up ( context . Background ( ) )
if err != nil {
return nil , err
}
2022-02-18 13:51:59 +00:00
return s , sqlutil . StatementList {
{ & s . insertDeviceStmt , insertDeviceSQL } ,
{ & s . selectDeviceByTokenStmt , selectDeviceByTokenSQL } ,
{ & s . selectDeviceByIDStmt , selectDeviceByIDSQL } ,
{ & s . selectDevicesByLocalpartStmt , selectDevicesByLocalpartSQL } ,
{ & s . updateDeviceNameStmt , updateDeviceNameSQL } ,
{ & s . deleteDeviceStmt , deleteDeviceSQL } ,
{ & s . deleteDevicesByLocalpartStmt , deleteDevicesByLocalpartSQL } ,
{ & s . deleteDevicesStmt , deleteDevicesSQL } ,
{ & s . selectDevicesByIDStmt , selectDevicesByIDSQL } ,
{ & s . updateDeviceLastSeenStmt , updateDeviceLastSeen } ,
} . Prepare ( db )
2017-05-25 13:33:50 +01:00
}
// insertDevice creates a new device. Returns an error if any device with the same access token already exists.
// Returns an error if the user already has a device with the given device ID.
// Returns the device on success.
2022-02-18 13:51:59 +00:00
func ( s * devicesStatements ) InsertDevice (
2022-11-11 16:41:37 +00:00
ctx context . Context , txn * sql . Tx , id string ,
localpart string , serverName gomatrixserverlib . ServerName ,
accessToken string , displayName * string , ipAddr , userAgent string ,
2020-06-16 14:10:55 +01:00
) ( * api . Device , error ) {
2017-05-25 13:33:50 +01:00
createdTimeMS := time . Now ( ) . UnixNano ( ) / 1000000
2019-08-23 17:55:40 +01:00
var sessionID int64
2020-06-12 14:55:57 +01:00
stmt := sqlutil . TxStmt ( txn , s . insertDeviceStmt )
2022-11-11 16:41:37 +00:00
if err := stmt . QueryRowContext ( ctx , id , localpart , serverName , accessToken , createdTimeMS , displayName , createdTimeMS , ipAddr , userAgent ) . Scan ( & sessionID ) ; err != nil {
return nil , fmt . Errorf ( "insertDeviceStmt: %w" , err )
2017-05-25 13:33:50 +01:00
}
2023-02-20 13:58:03 +00:00
dev := & api . Device {
2017-09-18 15:51:26 +01:00
ID : id ,
2022-11-11 16:41:37 +00:00
UserID : userutil . MakeUserID ( localpart , serverName ) ,
2017-09-18 15:51:26 +01:00
AccessToken : accessToken ,
2019-08-23 17:55:40 +01:00
SessionID : sessionID ,
2020-10-09 09:17:23 +01:00
LastSeenTS : createdTimeMS ,
LastSeenIP : ipAddr ,
UserAgent : userAgent ,
2023-02-20 13:58:03 +00:00
}
if displayName != nil {
dev . DisplayName = * displayName
}
return dev , nil
2017-05-25 13:33:50 +01:00
}
2023-02-17 10:39:46 +00:00
func ( s * devicesStatements ) InsertDeviceWithSessionID ( ctx context . Context , txn * sql . Tx , id ,
localpart string , serverName gomatrixserverlib . ServerName ,
accessToken string , displayName * string , ipAddr , userAgent string ,
sessionID int64 ,
) ( * api . Device , error ) {
return s . InsertDevice ( ctx , txn , id , localpart , serverName , accessToken , displayName , ipAddr , userAgent )
}
2020-02-11 12:13:38 +00:00
// deleteDevice removes a single device by id and user localpart.
2022-02-18 13:51:59 +00:00
func ( s * devicesStatements ) DeleteDevice (
2022-11-11 16:41:37 +00:00
ctx context . Context , txn * sql . Tx , id string ,
localpart string , serverName gomatrixserverlib . ServerName ,
2017-09-18 15:51:26 +01:00
) error {
2020-06-12 14:55:57 +01:00
stmt := sqlutil . TxStmt ( txn , s . deleteDeviceStmt )
2022-11-11 16:41:37 +00:00
_ , err := stmt . ExecContext ( ctx , id , localpart , serverName )
2017-05-25 13:33:50 +01:00
return err
}
2020-02-11 12:13:38 +00:00
// deleteDevices removes a single or multiple devices by ids and user localpart.
// Returns an error if the execution failed.
2022-02-18 13:51:59 +00:00
func ( s * devicesStatements ) DeleteDevices (
2022-11-11 16:41:37 +00:00
ctx context . Context , txn * sql . Tx ,
localpart string , serverName gomatrixserverlib . ServerName ,
devices [ ] string ,
2020-02-11 12:13:38 +00:00
) error {
2020-06-12 14:55:57 +01:00
stmt := sqlutil . TxStmt ( txn , s . deleteDevicesStmt )
2022-11-11 16:41:37 +00:00
_ , err := stmt . ExecContext ( ctx , localpart , serverName , pq . Array ( devices ) )
2020-02-11 12:13:38 +00:00
return err
}
// deleteDevicesByLocalpart removes all devices for the
// given user localpart.
2022-02-18 13:51:59 +00:00
func ( s * devicesStatements ) DeleteDevicesByLocalpart (
2022-11-11 16:41:37 +00:00
ctx context . Context , txn * sql . Tx ,
localpart string , serverName gomatrixserverlib . ServerName ,
exceptDeviceID string ,
2017-10-15 11:29:47 +01:00
) error {
2020-06-12 14:55:57 +01:00
stmt := sqlutil . TxStmt ( txn , s . deleteDevicesByLocalpartStmt )
2022-11-11 16:41:37 +00:00
_ , err := stmt . ExecContext ( ctx , localpart , serverName , exceptDeviceID )
2017-10-15 11:29:47 +01:00
return err
}
2022-02-18 13:51:59 +00:00
func ( s * devicesStatements ) UpdateDeviceName (
2022-11-11 16:41:37 +00:00
ctx context . Context , txn * sql . Tx ,
localpart string , serverName gomatrixserverlib . ServerName ,
deviceID string , displayName * string ,
2017-11-14 09:59:02 +00:00
) error {
2020-06-12 14:55:57 +01:00
stmt := sqlutil . TxStmt ( txn , s . updateDeviceNameStmt )
2022-11-11 16:41:37 +00:00
_ , err := stmt . ExecContext ( ctx , displayName , localpart , serverName , deviceID )
2017-11-14 09:59:02 +00:00
return err
}
2022-02-18 13:51:59 +00:00
func ( s * devicesStatements ) SelectDeviceByToken (
2017-09-18 15:51:26 +01:00
ctx context . Context , accessToken string ,
2020-06-16 14:10:55 +01:00
) ( * api . Device , error ) {
var dev api . Device
2017-05-25 13:33:50 +01:00
var localpart string
2022-11-11 16:41:37 +00:00
var serverName gomatrixserverlib . ServerName
2017-09-18 15:51:26 +01:00
stmt := s . selectDeviceByTokenStmt
2022-11-11 16:41:37 +00:00
err := stmt . QueryRowContext ( ctx , accessToken ) . Scan ( & dev . SessionID , & dev . ID , & localpart , & serverName )
2017-05-25 13:33:50 +01:00
if err == nil {
2022-11-11 16:41:37 +00:00
dev . UserID = userutil . MakeUserID ( localpart , serverName )
2017-05-25 13:33:50 +01:00
dev . AccessToken = accessToken
}
return & dev , err
}
2019-07-22 15:05:38 +01:00
// selectDeviceByID retrieves a device from the database with the given user
// localpart and deviceID
2022-02-18 13:51:59 +00:00
func ( s * devicesStatements ) SelectDeviceByID (
2022-11-11 16:41:37 +00:00
ctx context . Context ,
localpart string , serverName gomatrixserverlib . ServerName ,
deviceID string ,
2020-06-16 14:10:55 +01:00
) ( * api . Device , error ) {
var dev api . Device
2022-04-28 14:06:34 +01:00
var displayName , ip sql . NullString
var lastseenTS sql . NullInt64
2017-10-17 19:12:54 +01:00
stmt := s . selectDeviceByIDStmt
2022-11-11 16:41:37 +00:00
err := stmt . QueryRowContext ( ctx , localpart , serverName , deviceID ) . Scan ( & displayName , & lastseenTS , & ip )
2017-10-17 19:12:54 +01:00
if err == nil {
dev . ID = deviceID
2022-11-11 16:41:37 +00:00
dev . UserID = userutil . MakeUserID ( localpart , serverName )
2020-07-22 17:04:57 +01:00
if displayName . Valid {
dev . DisplayName = displayName . String
}
2022-04-28 14:06:34 +01:00
if lastseenTS . Valid {
dev . LastSeenTS = lastseenTS . Int64
}
if ip . Valid {
dev . LastSeenIP = ip . String
}
2017-10-17 19:12:54 +01:00
}
return & dev , err
}
2022-02-18 13:51:59 +00:00
func ( s * devicesStatements ) SelectDevicesByID ( ctx context . Context , deviceIDs [ ] string ) ( [ ] api . Device , error ) {
2020-07-22 17:04:57 +01:00
rows , err := s . selectDevicesByIDStmt . QueryContext ( ctx , pq . StringArray ( deviceIDs ) )
if err != nil {
return nil , err
}
defer internal . CloseAndLogIfError ( ctx , rows , "selectDevicesByID: rows.close() failed" )
var devices [ ] api . Device
2022-04-27 14:05:49 +01:00
var dev api . Device
var localpart string
2022-11-11 16:41:37 +00:00
var serverName gomatrixserverlib . ServerName
2022-04-27 14:05:49 +01:00
var lastseents sql . NullInt64
var displayName sql . NullString
2020-07-22 17:04:57 +01:00
for rows . Next ( ) {
2023-02-17 10:39:46 +00:00
if err := rows . Scan ( & dev . ID , & localpart , & serverName , & displayName , & lastseents , & dev . SessionID ) ; err != nil {
2020-07-22 17:04:57 +01:00
return nil , err
}
if displayName . Valid {
dev . DisplayName = displayName . String
}
2022-04-27 14:05:49 +01:00
if lastseents . Valid {
dev . LastSeenTS = lastseents . Int64
}
2022-11-11 16:41:37 +00:00
dev . UserID = userutil . MakeUserID ( localpart , serverName )
2020-07-22 17:04:57 +01:00
devices = append ( devices , dev )
}
return devices , rows . Err ( )
}
2022-02-18 13:51:59 +00:00
func ( s * devicesStatements ) SelectDevicesByLocalpart (
2022-11-11 16:41:37 +00:00
ctx context . Context , txn * sql . Tx ,
localpart string , serverName gomatrixserverlib . ServerName ,
exceptDeviceID string ,
2020-06-16 14:10:55 +01:00
) ( [ ] api . Device , error ) {
devices := [ ] api . Device { }
2022-11-11 16:41:37 +00:00
rows , err := sqlutil . TxStmt ( txn , s . selectDevicesByLocalpartStmt ) . QueryContext ( ctx , localpart , serverName , exceptDeviceID )
2017-10-17 19:12:54 +01:00
if err != nil {
return devices , err
}
2020-05-21 14:40:13 +01:00
defer internal . CloseAndLogIfError ( ctx , rows , "selectDevicesByLocalpart: rows.close() failed" )
2017-10-17 19:12:54 +01:00
2022-04-27 14:05:49 +01:00
var dev api . Device
var lastseents sql . NullInt64
var id , displayname , ip , useragent sql . NullString
2017-10-17 19:12:54 +01:00
for rows . Next ( ) {
2023-02-17 10:39:46 +00:00
err = rows . Scan ( & id , & displayname , & lastseents , & ip , & useragent , & dev . SessionID )
2017-10-17 19:12:54 +01:00
if err != nil {
return devices , err
}
2020-05-26 14:41:16 +01:00
if id . Valid {
dev . ID = id . String
}
if displayname . Valid {
dev . DisplayName = displayname . String
}
2020-11-17 10:07:03 +00:00
if lastseents . Valid {
dev . LastSeenTS = lastseents . Int64
}
if ip . Valid {
dev . LastSeenIP = ip . String
}
if useragent . Valid {
dev . UserAgent = useragent . String
}
2022-11-11 16:41:37 +00:00
dev . UserID = userutil . MakeUserID ( localpart , serverName )
2017-10-17 19:12:54 +01:00
devices = append ( devices , dev )
}
2020-02-11 14:12:21 +00:00
return devices , rows . Err ( )
2017-10-17 19:12:54 +01:00
}
2020-10-09 09:17:23 +01:00
2022-11-11 16:41:37 +00:00
func ( s * devicesStatements ) UpdateDeviceLastSeen ( ctx context . Context , txn * sql . Tx , localpart string , serverName gomatrixserverlib . ServerName , deviceID , ipAddr , userAgent string ) error {
2020-10-09 09:17:23 +01:00
lastSeenTs := time . Now ( ) . UnixNano ( ) / 1000000
stmt := sqlutil . TxStmt ( txn , s . updateDeviceLastSeenStmt )
2022-11-11 16:41:37 +00:00
_ , err := stmt . ExecContext ( ctx , lastSeenTs , ipAddr , userAgent , localpart , serverName , deviceID )
2020-10-09 09:17:23 +01:00
return err
}