2017-05-25 07:33:50 -05: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 11:27:33 -06:00
package postgres
2017-05-25 07:33:50 -05:00
import (
2017-09-18 09:51:26 -05:00
"context"
2017-05-25 07:33:50 -05:00
"database/sql"
2022-11-04 10:11:28 -05:00
"fmt"
2017-05-25 07:33:50 -05:00
"time"
2020-02-11 06:13:38 -06:00
"github.com/lib/pq"
2018-05-31 09:21:13 -05:00
"github.com/matrix-org/dendrite/clientapi/userutil"
2020-05-21 08:40:13 -05:00
"github.com/matrix-org/dendrite/internal"
2020-06-12 08:55:57 -05:00
"github.com/matrix-org/dendrite/internal/sqlutil"
2020-06-16 08:10:55 -05:00
"github.com/matrix-org/dendrite/userapi/api"
2022-07-25 04:39:22 -05:00
"github.com/matrix-org/dendrite/userapi/storage/postgres/deltas"
2022-02-18 07:51:59 -06:00
"github.com/matrix-org/dendrite/userapi/storage/tables"
2017-05-25 07:33:50 -05:00
"github.com/matrix-org/gomatrixserverlib"
)
const devicesSchema = `
2019-08-23 11:55:40 -05:00
-- This sequence is used for automatic allocation of session_id .
2022-10-18 09:59:08 -05:00
CREATE SEQUENCE IF NOT EXISTS userapi_device_session_id_seq START 1 ;
2019-08-23 11:55:40 -05:00
2017-05-25 07:33:50 -05:00
-- Stores data about devices .
2022-10-18 09:59:08 -05:00
CREATE TABLE IF NOT EXISTS userapi_devices (
2017-05-25 07:33:50 -05: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 11:55:40 -05: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 09:59:08 -05:00
session_id BIGINT NOT NULL DEFAULT nextval ( ' userapi_device_session_id_seq ' ) ,
2017-05-25 07:33:50 -05: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-04 06:09:43 -05:00
server_name TEXT NOT NULL ,
2017-05-25 07:33:50 -05:00
-- When this devices was first recognised on the network , as a unix timestamp ( ms resolution ) .
2017-11-14 03:59:02 -06:00
created_ts BIGINT NOT NULL ,
-- The display name , human friendlier than device_id and updatable
2020-10-09 03:17:23 -05: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 07:33:50 -05:00
) ;
-- Device IDs must be unique for a given user .
2022-10-18 09:59:08 -05:00
CREATE UNIQUE INDEX IF NOT EXISTS userapi_device_localpart_id_idx ON userapi_devices ( localpart , device_id ) ;
2017-05-25 07:33:50 -05:00
`
const insertDeviceSQL = "" +
2022-11-04 10:11:28 -05: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 11:55:40 -05:00
" RETURNING session_id"
2017-05-25 07:33:50 -05:00
const selectDeviceByTokenSQL = "" +
2022-10-18 09:59:08 -05:00
"SELECT session_id, device_id, localpart FROM userapi_devices WHERE access_token = $1"
2017-05-25 07:33:50 -05:00
2017-10-17 13:12:54 -05:00
const selectDeviceByIDSQL = "" +
2022-10-18 09:59:08 -05:00
"SELECT display_name, last_seen_ts, ip FROM userapi_devices WHERE localpart = $1 and device_id = $2"
2017-10-17 13:12:54 -05:00
const selectDevicesByLocalpartSQL = "" +
2022-10-18 09:59:08 -05:00
"SELECT device_id, display_name, last_seen_ts, ip, user_agent FROM userapi_devices WHERE localpart = $1 AND device_id != $2 ORDER BY last_seen_ts DESC"
2017-11-14 03:59:02 -06:00
const updateDeviceNameSQL = "" +
2022-10-18 09:59:08 -05:00
"UPDATE userapi_devices SET display_name = $1 WHERE localpart = $2 AND device_id = $3"
2017-10-17 13:12:54 -05:00
2017-05-25 07:33:50 -05:00
const deleteDeviceSQL = "" +
2022-11-04 10:11:28 -05:00
"DELETE FROM userapi_devices WHERE device_id = $1 AND localpart = $2 AND server_name = $3"
2017-05-25 07:33:50 -05:00
2017-10-15 05:29:47 -05:00
const deleteDevicesByLocalpartSQL = "" +
2022-10-18 09:59:08 -05:00
"DELETE FROM userapi_devices WHERE localpart = $1 AND device_id != $2"
2017-10-15 05:29:47 -05:00
2020-02-11 06:13:38 -06:00
const deleteDevicesSQL = "" +
2022-10-18 09:59:08 -05:00
"DELETE FROM userapi_devices WHERE localpart = $1 AND device_id = ANY($2)"
2020-02-11 06:13:38 -06:00
2020-07-22 11:04:57 -05:00
const selectDevicesByIDSQL = "" +
2022-10-18 09:59:08 -05:00
"SELECT device_id, localpart, display_name, last_seen_ts FROM userapi_devices WHERE device_id = ANY($1) ORDER BY last_seen_ts DESC"
2020-07-22 11:04:57 -05:00
2020-10-09 03:17:23 -05:00
const updateDeviceLastSeen = "" +
2022-10-18 09:59:08 -05:00
"UPDATE userapi_devices SET last_seen_ts = $1, ip = $2, user_agent = $3 WHERE localpart = $4 AND device_id = $5"
2020-10-09 03:17:23 -05:00
2017-05-25 07:33:50 -05:00
type devicesStatements struct {
2017-10-15 05:29:47 -05:00
insertDeviceStmt * sql . Stmt
selectDeviceByTokenStmt * sql . Stmt
2017-10-17 13:12:54 -05:00
selectDeviceByIDStmt * sql . Stmt
selectDevicesByLocalpartStmt * sql . Stmt
2020-07-22 11:04:57 -05:00
selectDevicesByIDStmt * sql . Stmt
2017-11-14 03:59:02 -06:00
updateDeviceNameStmt * sql . Stmt
2020-10-09 03:17:23 -05:00
updateDeviceLastSeenStmt * sql . Stmt
2017-10-15 05:29:47 -05:00
deleteDeviceStmt * sql . Stmt
deleteDevicesByLocalpartStmt * sql . Stmt
2020-02-11 06:13:38 -06:00
deleteDevicesStmt * sql . Stmt
2017-10-17 13:12:54 -05:00
serverName gomatrixserverlib . ServerName
2017-05-25 07:33:50 -05:00
}
2022-02-18 07:51:59 -06:00
func NewPostgresDevicesTable ( db * sql . DB , serverName gomatrixserverlib . ServerName ) ( tables . DevicesTable , error ) {
s := & devicesStatements {
serverName : serverName ,
2017-10-17 13:12:54 -05:00
}
2022-02-18 07:51:59 -06:00
_ , err := db . Exec ( devicesSchema )
if err != nil {
return nil , err
2020-10-09 03:17:23 -05:00
}
2022-07-25 04:39:22 -05: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 07:51:59 -06: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 07:33:50 -05: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 07:51:59 -06:00
func ( s * devicesStatements ) InsertDevice (
2022-11-04 10:11:28 -05:00
ctx context . Context , txn * sql . Tx , id string ,
localpart string , serverName gomatrixserverlib . ServerName ,
accessToken string , displayName * string , ipAddr , userAgent string ,
2020-06-16 08:10:55 -05:00
) ( * api . Device , error ) {
2017-05-25 07:33:50 -05:00
createdTimeMS := time . Now ( ) . UnixNano ( ) / 1000000
2019-08-23 11:55:40 -05:00
var sessionID int64
2020-06-12 08:55:57 -05:00
stmt := sqlutil . TxStmt ( txn , s . insertDeviceStmt )
2022-11-04 10:11:28 -05: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 07:33:50 -05:00
}
2020-06-16 08:10:55 -05:00
return & api . Device {
2017-09-18 09:51:26 -05:00
ID : id ,
2022-11-04 10:11:28 -05:00
UserID : userutil . MakeUserID ( localpart , serverName ) ,
2017-09-18 09:51:26 -05:00
AccessToken : accessToken ,
2019-08-23 11:55:40 -05:00
SessionID : sessionID ,
2020-10-09 03:17:23 -05:00
LastSeenTS : createdTimeMS ,
LastSeenIP : ipAddr ,
UserAgent : userAgent ,
2017-09-18 09:51:26 -05:00
} , nil
2017-05-25 07:33:50 -05:00
}
2020-02-11 06:13:38 -06:00
// deleteDevice removes a single device by id and user localpart.
2022-02-18 07:51:59 -06:00
func ( s * devicesStatements ) DeleteDevice (
2022-11-04 10:11:28 -05:00
ctx context . Context , txn * sql . Tx , id string ,
localpart string , serverName gomatrixserverlib . ServerName ,
2017-09-18 09:51:26 -05:00
) error {
2020-06-12 08:55:57 -05:00
stmt := sqlutil . TxStmt ( txn , s . deleteDeviceStmt )
2022-11-04 10:11:28 -05:00
_ , err := stmt . ExecContext ( ctx , id , localpart , serverName )
2017-05-25 07:33:50 -05:00
return err
}
2020-02-11 06:13:38 -06:00
// deleteDevices removes a single or multiple devices by ids and user localpart.
// Returns an error if the execution failed.
2022-02-18 07:51:59 -06:00
func ( s * devicesStatements ) DeleteDevices (
2020-02-11 06:13:38 -06:00
ctx context . Context , txn * sql . Tx , localpart string , devices [ ] string ,
) error {
2020-06-12 08:55:57 -05:00
stmt := sqlutil . TxStmt ( txn , s . deleteDevicesStmt )
2020-02-11 06:13:38 -06:00
_ , err := stmt . ExecContext ( ctx , localpart , pq . Array ( devices ) )
return err
}
// deleteDevicesByLocalpart removes all devices for the
// given user localpart.
2022-02-18 07:51:59 -06:00
func ( s * devicesStatements ) DeleteDevicesByLocalpart (
2020-09-04 09:16:13 -05:00
ctx context . Context , txn * sql . Tx , localpart , exceptDeviceID string ,
2017-10-15 05:29:47 -05:00
) error {
2020-06-12 08:55:57 -05:00
stmt := sqlutil . TxStmt ( txn , s . deleteDevicesByLocalpartStmt )
2020-09-04 09:16:13 -05:00
_ , err := stmt . ExecContext ( ctx , localpart , exceptDeviceID )
2017-10-15 05:29:47 -05:00
return err
}
2022-02-18 07:51:59 -06:00
func ( s * devicesStatements ) UpdateDeviceName (
2017-11-14 03:59:02 -06:00
ctx context . Context , txn * sql . Tx , localpart , deviceID string , displayName * string ,
) error {
2020-06-12 08:55:57 -05:00
stmt := sqlutil . TxStmt ( txn , s . updateDeviceNameStmt )
2017-11-14 03:59:02 -06:00
_ , err := stmt . ExecContext ( ctx , displayName , localpart , deviceID )
return err
}
2022-02-18 07:51:59 -06:00
func ( s * devicesStatements ) SelectDeviceByToken (
2017-09-18 09:51:26 -05:00
ctx context . Context , accessToken string ,
2020-06-16 08:10:55 -05:00
) ( * api . Device , error ) {
var dev api . Device
2017-05-25 07:33:50 -05:00
var localpart string
2017-09-18 09:51:26 -05:00
stmt := s . selectDeviceByTokenStmt
2019-08-23 11:55:40 -05:00
err := stmt . QueryRowContext ( ctx , accessToken ) . Scan ( & dev . SessionID , & dev . ID , & localpart )
2017-05-25 07:33:50 -05:00
if err == nil {
2018-05-31 09:21:13 -05:00
dev . UserID = userutil . MakeUserID ( localpart , s . serverName )
2017-05-25 07:33:50 -05:00
dev . AccessToken = accessToken
}
return & dev , err
}
2019-07-22 09:05:38 -05:00
// selectDeviceByID retrieves a device from the database with the given user
// localpart and deviceID
2022-02-18 07:51:59 -06:00
func ( s * devicesStatements ) SelectDeviceByID (
2017-10-17 13:12:54 -05:00
ctx context . Context , localpart , deviceID string ,
2020-06-16 08:10:55 -05:00
) ( * api . Device , error ) {
var dev api . Device
2022-04-28 08:06:34 -05:00
var displayName , ip sql . NullString
var lastseenTS sql . NullInt64
2017-10-17 13:12:54 -05:00
stmt := s . selectDeviceByIDStmt
2022-04-28 08:06:34 -05:00
err := stmt . QueryRowContext ( ctx , localpart , deviceID ) . Scan ( & displayName , & lastseenTS , & ip )
2017-10-17 13:12:54 -05:00
if err == nil {
dev . ID = deviceID
2018-05-31 09:21:13 -05:00
dev . UserID = userutil . MakeUserID ( localpart , s . serverName )
2020-07-22 11:04:57 -05:00
if displayName . Valid {
dev . DisplayName = displayName . String
}
2022-04-28 08:06:34 -05:00
if lastseenTS . Valid {
dev . LastSeenTS = lastseenTS . Int64
}
if ip . Valid {
dev . LastSeenIP = ip . String
}
2017-10-17 13:12:54 -05:00
}
return & dev , err
}
2022-02-18 07:51:59 -06:00
func ( s * devicesStatements ) SelectDevicesByID ( ctx context . Context , deviceIDs [ ] string ) ( [ ] api . Device , error ) {
2020-07-22 11:04:57 -05: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 08:05:49 -05:00
var dev api . Device
var localpart string
var lastseents sql . NullInt64
var displayName sql . NullString
2020-07-22 11:04:57 -05:00
for rows . Next ( ) {
2022-04-27 08:05:49 -05:00
if err := rows . Scan ( & dev . ID , & localpart , & displayName , & lastseents ) ; err != nil {
2020-07-22 11:04:57 -05:00
return nil , err
}
if displayName . Valid {
dev . DisplayName = displayName . String
}
2022-04-27 08:05:49 -05:00
if lastseents . Valid {
dev . LastSeenTS = lastseents . Int64
}
2020-07-22 11:04:57 -05:00
dev . UserID = userutil . MakeUserID ( localpart , s . serverName )
devices = append ( devices , dev )
}
return devices , rows . Err ( )
}
2022-02-18 07:51:59 -06:00
func ( s * devicesStatements ) SelectDevicesByLocalpart (
2020-09-04 09:16:13 -05:00
ctx context . Context , txn * sql . Tx , localpart , exceptDeviceID string ,
2020-06-16 08:10:55 -05:00
) ( [ ] api . Device , error ) {
devices := [ ] api . Device { }
2020-09-04 09:16:13 -05:00
rows , err := sqlutil . TxStmt ( txn , s . selectDevicesByLocalpartStmt ) . QueryContext ( ctx , localpart , exceptDeviceID )
2017-10-17 13:12:54 -05:00
if err != nil {
return devices , err
}
2020-05-21 08:40:13 -05:00
defer internal . CloseAndLogIfError ( ctx , rows , "selectDevicesByLocalpart: rows.close() failed" )
2017-10-17 13:12:54 -05:00
2022-04-27 08:05:49 -05:00
var dev api . Device
var lastseents sql . NullInt64
var id , displayname , ip , useragent sql . NullString
2017-10-17 13:12:54 -05:00
for rows . Next ( ) {
2020-11-17 04:07:03 -06:00
err = rows . Scan ( & id , & displayname , & lastseents , & ip , & useragent )
2017-10-17 13:12:54 -05:00
if err != nil {
return devices , err
}
2020-05-26 08:41:16 -05:00
if id . Valid {
dev . ID = id . String
}
if displayname . Valid {
dev . DisplayName = displayname . String
}
2020-11-17 04:07:03 -06:00
if lastseents . Valid {
dev . LastSeenTS = lastseents . Int64
}
if ip . Valid {
dev . LastSeenIP = ip . String
}
if useragent . Valid {
dev . UserAgent = useragent . String
}
2018-05-31 09:21:13 -05:00
dev . UserID = userutil . MakeUserID ( localpart , s . serverName )
2017-10-17 13:12:54 -05:00
devices = append ( devices , dev )
}
2020-02-11 08:12:21 -06:00
return devices , rows . Err ( )
2017-10-17 13:12:54 -05:00
}
2020-10-09 03:17:23 -05:00
2022-05-04 12:04:28 -05:00
func ( s * devicesStatements ) UpdateDeviceLastSeen ( ctx context . Context , txn * sql . Tx , localpart , deviceID , ipAddr , userAgent string ) error {
2020-10-09 03:17:23 -05:00
lastSeenTs := time . Now ( ) . UnixNano ( ) / 1000000
stmt := sqlutil . TxStmt ( txn , s . updateDeviceLastSeenStmt )
2022-05-04 12:04:28 -05:00
_ , err := stmt . ExecContext ( ctx , lastSeenTs , ipAddr , userAgent , localpart , deviceID )
2020-10-09 03:17:23 -05:00
return err
}