2020-02-13 17:27:33 +00: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.
|
|
|
|
|
|
|
|
package postgres
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-02-18 11:31:05 +00:00
|
|
|
"crypto/rand"
|
2020-02-13 17:27:33 +00:00
|
|
|
"database/sql"
|
2022-02-18 11:31:05 +00:00
|
|
|
"encoding/base64"
|
2020-06-18 18:36:03 +01:00
|
|
|
"encoding/json"
|
2020-02-13 17:27:33 +00:00
|
|
|
"errors"
|
2021-07-27 17:08:53 +01:00
|
|
|
"fmt"
|
2020-03-06 18:00:07 +00:00
|
|
|
"strconv"
|
2021-04-07 13:26:20 +01:00
|
|
|
"time"
|
2020-02-13 17:27:33 +00:00
|
|
|
|
2022-02-16 17:55:38 +00:00
|
|
|
"github.com/matrix-org/gomatrixserverlib"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
|
2020-02-13 17:27:33 +00:00
|
|
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
2020-04-16 10:06:55 +01:00
|
|
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
2020-12-02 17:41:00 +00:00
|
|
|
"github.com/matrix-org/dendrite/setup/config"
|
2020-06-17 11:22:26 +01:00
|
|
|
"github.com/matrix-org/dendrite/userapi/api"
|
2022-02-18 11:31:05 +00:00
|
|
|
"github.com/matrix-org/dendrite/userapi/storage/postgres/deltas"
|
2020-02-13 17:27:33 +00:00
|
|
|
|
|
|
|
// Import the postgres database driver.
|
|
|
|
_ "github.com/lib/pq"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Database represents an account database
|
|
|
|
type Database struct {
|
2020-08-21 10:42:08 +01:00
|
|
|
db *sql.DB
|
|
|
|
writer sqlutil.Writer
|
2020-06-12 14:55:57 +01:00
|
|
|
sqlutil.PartitionOffsetStatements
|
2021-04-07 13:26:20 +01:00
|
|
|
accounts accountsStatements
|
|
|
|
profiles profilesStatements
|
|
|
|
accountDatas accountDataStatements
|
|
|
|
threepids threepidStatements
|
|
|
|
openIDTokens tokenStatements
|
2021-07-27 17:08:53 +01:00
|
|
|
keyBackupVersions keyBackupVersionStatements
|
2022-02-18 11:31:05 +00:00
|
|
|
devices devicesStatements
|
|
|
|
loginTokens loginTokenStatements
|
|
|
|
loginTokenLifetime time.Duration
|
2021-07-27 17:08:53 +01:00
|
|
|
keyBackups keyBackupStatements
|
2021-04-07 13:26:20 +01:00
|
|
|
serverName gomatrixserverlib.ServerName
|
|
|
|
bcryptCost int
|
|
|
|
openIDTokenLifetimeMS int64
|
2020-02-13 17:27:33 +00:00
|
|
|
}
|
|
|
|
|
2022-02-18 11:31:05 +00:00
|
|
|
const (
|
|
|
|
// The length of generated device IDs
|
|
|
|
deviceIDByteLength = 6
|
|
|
|
loginTokenByteLength = 32
|
|
|
|
)
|
|
|
|
|
2020-02-13 17:27:33 +00:00
|
|
|
// NewDatabase creates a new accounts and profiles database
|
2022-02-18 11:31:05 +00:00
|
|
|
func NewDatabase(dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, openIDTokenLifetimeMS int64, loginTokenLifetime time.Duration) (*Database, error) {
|
2020-08-10 14:18:04 +01:00
|
|
|
db, err := sqlutil.Open(dbProperties)
|
|
|
|
if err != nil {
|
2020-02-13 17:27:33 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2020-08-21 10:42:08 +01:00
|
|
|
d := &Database{
|
2021-04-07 13:26:20 +01:00
|
|
|
serverName: serverName,
|
|
|
|
db: db,
|
|
|
|
writer: sqlutil.NewDummyWriter(),
|
2022-02-18 11:31:05 +00:00
|
|
|
loginTokenLifetime: loginTokenLifetime,
|
2021-04-07 13:26:20 +01:00
|
|
|
bcryptCost: bcryptCost,
|
|
|
|
openIDTokenLifetimeMS: openIDTokenLifetimeMS,
|
2020-08-21 10:42:08 +01:00
|
|
|
}
|
2020-10-15 18:09:41 +01:00
|
|
|
|
|
|
|
// Create tables before executing migrations so we don't fail if the table is missing,
|
|
|
|
// and THEN prepare statements so we don't fail due to referencing new columns
|
|
|
|
if err = d.accounts.execSchema(db); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
m := sqlutil.NewMigrations()
|
|
|
|
deltas.LoadIsActive(m)
|
2022-02-18 11:31:05 +00:00
|
|
|
//deltas.LoadLastSeenTSIP(m)
|
2022-02-16 17:55:38 +00:00
|
|
|
deltas.LoadAddAccountType(m)
|
2020-10-15 18:09:41 +01:00
|
|
|
if err = m.RunDeltas(db, dbProperties); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-08-21 10:42:08 +01:00
|
|
|
if err = d.PartitionOffsetStatements.Prepare(db, d.writer, "account"); err != nil {
|
2020-02-13 17:27:33 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2020-08-21 10:42:08 +01:00
|
|
|
if err = d.accounts.prepare(db, serverName); err != nil {
|
2020-02-13 17:27:33 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2020-08-21 10:42:08 +01:00
|
|
|
if err = d.profiles.prepare(db); err != nil {
|
2020-02-13 17:27:33 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2020-08-21 10:42:08 +01:00
|
|
|
if err = d.accountDatas.prepare(db); err != nil {
|
2020-02-13 17:27:33 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2020-08-21 10:42:08 +01:00
|
|
|
if err = d.threepids.prepare(db); err != nil {
|
2020-02-13 17:27:33 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2021-04-07 13:26:20 +01:00
|
|
|
if err = d.openIDTokens.prepare(db, serverName); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-07-27 19:29:32 +01:00
|
|
|
if err = d.keyBackupVersions.prepare(db); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err = d.keyBackups.prepare(db); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-02-18 11:31:05 +00:00
|
|
|
if err = d.devices.prepare(db, serverName); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err = d.loginTokens.prepare(db); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-10-15 18:09:41 +01:00
|
|
|
|
2020-08-21 10:42:08 +01:00
|
|
|
return d, nil
|
2020-02-13 17:27:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetAccountByPassword returns the account associated with the given localpart and password.
|
|
|
|
// Returns sql.ErrNoRows if no account exists which matches the given localpart.
|
|
|
|
func (d *Database) GetAccountByPassword(
|
|
|
|
ctx context.Context, localpart, plaintextPassword string,
|
2020-06-17 11:22:26 +01:00
|
|
|
) (*api.Account, error) {
|
2020-02-13 17:27:33 +00:00
|
|
|
hash, err := d.accounts.selectPasswordHash(ctx, localpart)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(plaintextPassword)); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return d.accounts.selectAccountByLocalpart(ctx, localpart)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetProfileByLocalpart returns the profile associated with the given localpart.
|
|
|
|
// Returns sql.ErrNoRows if no profile exists which matches the given localpart.
|
|
|
|
func (d *Database) GetProfileByLocalpart(
|
|
|
|
ctx context.Context, localpart string,
|
|
|
|
) (*authtypes.Profile, error) {
|
|
|
|
return d.profiles.selectProfileByLocalpart(ctx, localpart)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetAvatarURL updates the avatar URL of the profile associated with the given
|
|
|
|
// localpart. Returns an error if something went wrong with the SQL query
|
|
|
|
func (d *Database) SetAvatarURL(
|
|
|
|
ctx context.Context, localpart string, avatarURL string,
|
|
|
|
) error {
|
|
|
|
return d.profiles.setAvatarURL(ctx, localpart, avatarURL)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetDisplayName updates the display name of the profile associated with the given
|
|
|
|
// localpart. Returns an error if something went wrong with the SQL query
|
|
|
|
func (d *Database) SetDisplayName(
|
|
|
|
ctx context.Context, localpart string, displayName string,
|
|
|
|
) error {
|
|
|
|
return d.profiles.setDisplayName(ctx, localpart, displayName)
|
|
|
|
}
|
|
|
|
|
2020-09-04 15:16:13 +01:00
|
|
|
// SetPassword sets the account password to the given hash.
|
|
|
|
func (d *Database) SetPassword(
|
|
|
|
ctx context.Context, localpart, plaintextPassword string,
|
|
|
|
) error {
|
2021-03-08 13:19:02 +00:00
|
|
|
hash, err := d.hashPassword(plaintextPassword)
|
2020-09-04 15:16:13 +01:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return d.accounts.updatePassword(ctx, localpart, hash)
|
|
|
|
}
|
|
|
|
|
2020-02-13 17:27:33 +00:00
|
|
|
// CreateAccount makes a new account with the given login name and password, and creates an empty profile
|
|
|
|
// for this account. If no password is supplied, the account will be a passwordless account. If the
|
2020-06-12 14:55:57 +01:00
|
|
|
// account already exists, it will return nil, sqlutil.ErrUserExists.
|
2020-02-13 17:27:33 +00:00
|
|
|
func (d *Database) CreateAccount(
|
2022-02-16 17:55:38 +00:00
|
|
|
ctx context.Context, localpart, plaintextPassword, appserviceID string, accountType api.AccountType,
|
2020-06-17 11:22:26 +01:00
|
|
|
) (acc *api.Account, err error) {
|
2020-06-12 14:55:57 +01:00
|
|
|
err = sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
2022-02-16 17:55:38 +00:00
|
|
|
// For guest accounts, we create a new numeric local part
|
|
|
|
if accountType == api.AccountTypeGuest {
|
|
|
|
var numLocalpart int64
|
|
|
|
numLocalpart, err = d.accounts.selectNewNumericLocalpart(ctx, txn)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
localpart = strconv.FormatInt(numLocalpart, 10)
|
|
|
|
plaintextPassword = ""
|
|
|
|
appserviceID = ""
|
|
|
|
}
|
|
|
|
acc, err = d.createAccount(ctx, txn, localpart, plaintextPassword, appserviceID, accountType)
|
2020-03-06 18:00:07 +00:00
|
|
|
return err
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Database) createAccount(
|
2022-02-16 17:55:38 +00:00
|
|
|
ctx context.Context, txn *sql.Tx, localpart, plaintextPassword, appserviceID string, accountType api.AccountType,
|
2020-06-17 11:22:26 +01:00
|
|
|
) (*api.Account, error) {
|
2021-03-02 10:43:25 +00:00
|
|
|
var account *api.Account
|
2020-02-13 17:27:33 +00:00
|
|
|
var err error
|
|
|
|
// Generate a password hash if this is not a password-less user
|
|
|
|
hash := ""
|
|
|
|
if plaintextPassword != "" {
|
2021-03-08 13:19:02 +00:00
|
|
|
hash, err = d.hashPassword(plaintextPassword)
|
2020-02-13 17:27:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
2022-02-16 17:55:38 +00:00
|
|
|
if account, err = d.accounts.insertAccount(ctx, txn, localpart, hash, appserviceID, accountType); err != nil {
|
2020-06-12 14:55:57 +01:00
|
|
|
if sqlutil.IsUniqueConstraintViolationErr(err) {
|
|
|
|
return nil, sqlutil.ErrUserExists
|
2020-02-13 17:27:33 +00:00
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-03-02 10:43:25 +00:00
|
|
|
if err = d.profiles.insertProfile(ctx, txn, localpart); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err = d.accountDatas.insertAccountData(ctx, txn, localpart, "", "m.push_rules", json.RawMessage(`{
|
2020-02-13 17:27:33 +00:00
|
|
|
"global": {
|
|
|
|
"content": [],
|
|
|
|
"override": [],
|
|
|
|
"room": [],
|
|
|
|
"sender": [],
|
|
|
|
"underride": []
|
|
|
|
}
|
2020-06-18 18:36:03 +01:00
|
|
|
}`)); err != nil {
|
2020-02-13 17:27:33 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2021-03-02 10:43:25 +00:00
|
|
|
return account, nil
|
2020-02-13 17:27:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SaveAccountData saves new account data for a given user and a given room.
|
|
|
|
// If the account data is not specific to a room, the room ID should be an empty string
|
|
|
|
// If an account data already exists for a given set (user, room, data type), it will
|
|
|
|
// update the corresponding row with the new content
|
|
|
|
// Returns a SQL error if there was an issue with the insertion/update
|
|
|
|
func (d *Database) SaveAccountData(
|
2020-06-18 18:36:03 +01:00
|
|
|
ctx context.Context, localpart, roomID, dataType string, content json.RawMessage,
|
2020-02-13 17:27:33 +00:00
|
|
|
) error {
|
2020-06-12 14:55:57 +01:00
|
|
|
return sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
2020-03-06 18:00:07 +00:00
|
|
|
return d.accountDatas.insertAccountData(ctx, txn, localpart, roomID, dataType, content)
|
|
|
|
})
|
2020-02-13 17:27:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetAccountData returns account data related to a given localpart
|
|
|
|
// If no account data could be found, returns an empty arrays
|
|
|
|
// Returns an error if there was an issue with the retrieval
|
|
|
|
func (d *Database) GetAccountData(ctx context.Context, localpart string) (
|
2020-06-18 18:36:03 +01:00
|
|
|
global map[string]json.RawMessage,
|
|
|
|
rooms map[string]map[string]json.RawMessage,
|
2020-02-13 17:27:33 +00:00
|
|
|
err error,
|
|
|
|
) {
|
|
|
|
return d.accountDatas.selectAccountData(ctx, localpart)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetAccountDataByType returns account data matching a given
|
|
|
|
// localpart, room ID and type.
|
|
|
|
// If no account data could be found, returns nil
|
|
|
|
// Returns an error if there was an issue with the retrieval
|
|
|
|
func (d *Database) GetAccountDataByType(
|
|
|
|
ctx context.Context, localpart, roomID, dataType string,
|
2020-06-18 18:36:03 +01:00
|
|
|
) (data json.RawMessage, err error) {
|
2020-02-13 17:27:33 +00:00
|
|
|
return d.accountDatas.selectAccountDataByType(
|
|
|
|
ctx, localpart, roomID, dataType,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetNewNumericLocalpart generates and returns a new unused numeric localpart
|
|
|
|
func (d *Database) GetNewNumericLocalpart(
|
|
|
|
ctx context.Context,
|
|
|
|
) (int64, error) {
|
2020-03-06 18:00:07 +00:00
|
|
|
return d.accounts.selectNewNumericLocalpart(ctx, nil)
|
2020-02-13 17:27:33 +00:00
|
|
|
}
|
|
|
|
|
2021-03-08 13:19:02 +00:00
|
|
|
func (d *Database) hashPassword(plaintext string) (hash string, err error) {
|
|
|
|
hashBytes, err := bcrypt.GenerateFromPassword([]byte(plaintext), d.bcryptCost)
|
2020-02-13 17:27:33 +00:00
|
|
|
return string(hashBytes), err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Err3PIDInUse is the error returned when trying to save an association involving
|
|
|
|
// a third-party identifier which is already associated to a local user.
|
2021-09-08 17:31:03 +01:00
|
|
|
var Err3PIDInUse = errors.New("this third-party identifier is already in use")
|
2020-02-13 17:27:33 +00:00
|
|
|
|
|
|
|
// SaveThreePIDAssociation saves the association between a third party identifier
|
|
|
|
// and a local Matrix user (identified by the user's ID's local part).
|
|
|
|
// If the third-party identifier is already part of an association, returns Err3PIDInUse.
|
|
|
|
// Returns an error if there was a problem talking to the database.
|
|
|
|
func (d *Database) SaveThreePIDAssociation(
|
|
|
|
ctx context.Context, threepid, localpart, medium string,
|
|
|
|
) (err error) {
|
2020-06-12 14:55:57 +01:00
|
|
|
return sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
2020-02-13 17:27:33 +00:00
|
|
|
user, err := d.threepids.selectLocalpartForThreePID(
|
|
|
|
ctx, txn, threepid, medium,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(user) > 0 {
|
|
|
|
return Err3PIDInUse
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.threepids.insertThreePID(ctx, txn, threepid, medium, localpart)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveThreePIDAssociation removes the association involving a given third-party
|
|
|
|
// identifier.
|
|
|
|
// If no association exists involving this third-party identifier, returns nothing.
|
|
|
|
// If there was a problem talking to the database, returns an error.
|
|
|
|
func (d *Database) RemoveThreePIDAssociation(
|
|
|
|
ctx context.Context, threepid string, medium string,
|
|
|
|
) (err error) {
|
|
|
|
return d.threepids.deleteThreePID(ctx, threepid, medium)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetLocalpartForThreePID looks up the localpart associated with a given third-party
|
|
|
|
// identifier.
|
|
|
|
// If no association involves the given third-party idenfitier, returns an empty
|
|
|
|
// string.
|
|
|
|
// Returns an error if there was a problem talking to the database.
|
|
|
|
func (d *Database) GetLocalpartForThreePID(
|
|
|
|
ctx context.Context, threepid string, medium string,
|
|
|
|
) (localpart string, err error) {
|
|
|
|
return d.threepids.selectLocalpartForThreePID(ctx, nil, threepid, medium)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetThreePIDsForLocalpart looks up the third-party identifiers associated with
|
|
|
|
// a given local user.
|
|
|
|
// If no association is known for this user, returns an empty slice.
|
|
|
|
// Returns an error if there was an issue talking to the database.
|
|
|
|
func (d *Database) GetThreePIDsForLocalpart(
|
|
|
|
ctx context.Context, localpart string,
|
|
|
|
) (threepids []authtypes.ThreePID, err error) {
|
|
|
|
return d.threepids.selectThreePIDsForLocalpart(ctx, localpart)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CheckAccountAvailability checks if the username/localpart is already present
|
|
|
|
// in the database.
|
|
|
|
// If the DB returns sql.ErrNoRows the Localpart isn't taken.
|
|
|
|
func (d *Database) CheckAccountAvailability(ctx context.Context, localpart string) (bool, error) {
|
|
|
|
_, err := d.accounts.selectAccountByLocalpart(ctx, localpart)
|
|
|
|
if err == sql.ErrNoRows {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetAccountByLocalpart returns the account associated with the given localpart.
|
|
|
|
// This function assumes the request is authenticated or the account data is used only internally.
|
|
|
|
// Returns sql.ErrNoRows if no account exists which matches the given localpart.
|
|
|
|
func (d *Database) GetAccountByLocalpart(ctx context.Context, localpart string,
|
2020-06-17 11:22:26 +01:00
|
|
|
) (*api.Account, error) {
|
2020-02-13 17:27:33 +00:00
|
|
|
return d.accounts.selectAccountByLocalpart(ctx, localpart)
|
|
|
|
}
|
2020-07-28 10:53:17 +01:00
|
|
|
|
|
|
|
// SearchProfiles returns all profiles where the provided localpart or display name
|
|
|
|
// match any part of the profiles in the database.
|
|
|
|
func (d *Database) SearchProfiles(ctx context.Context, searchString string, limit int,
|
|
|
|
) ([]authtypes.Profile, error) {
|
|
|
|
return d.profiles.selectProfilesBySearch(ctx, searchString, limit)
|
|
|
|
}
|
2020-10-02 17:18:20 +01:00
|
|
|
|
|
|
|
// DeactivateAccount deactivates the user's account, removing all ability for the user to login again.
|
|
|
|
func (d *Database) DeactivateAccount(ctx context.Context, localpart string) (err error) {
|
|
|
|
return d.accounts.deactivateAccount(ctx, localpart)
|
|
|
|
}
|
2021-04-07 13:26:20 +01:00
|
|
|
|
|
|
|
// CreateOpenIDToken persists a new token that was issued through OpenID Connect
|
|
|
|
func (d *Database) CreateOpenIDToken(
|
|
|
|
ctx context.Context,
|
|
|
|
token, localpart string,
|
|
|
|
) (int64, error) {
|
|
|
|
expiresAtMS := time.Now().UnixNano()/int64(time.Millisecond) + d.openIDTokenLifetimeMS
|
|
|
|
err := sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
return d.openIDTokens.insertToken(ctx, txn, token, localpart, expiresAtMS)
|
|
|
|
})
|
|
|
|
return expiresAtMS, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetOpenIDTokenAttributes gets the attributes of issued an OIDC auth token
|
|
|
|
func (d *Database) GetOpenIDTokenAttributes(
|
|
|
|
ctx context.Context,
|
|
|
|
token string,
|
|
|
|
) (*api.OpenIDTokenAttributes, error) {
|
|
|
|
return d.openIDTokens.selectOpenIDTokenAtrributes(ctx, token)
|
|
|
|
}
|
2021-07-27 12:47:32 +01:00
|
|
|
|
|
|
|
func (d *Database) CreateKeyBackup(
|
|
|
|
ctx context.Context, userID, algorithm string, authData json.RawMessage,
|
|
|
|
) (version string, err error) {
|
|
|
|
err = sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
2021-07-27 17:08:53 +01:00
|
|
|
version, err = d.keyBackupVersions.insertKeyBackup(ctx, txn, userID, algorithm, authData, "")
|
2021-07-27 12:47:32 +01:00
|
|
|
return err
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Database) UpdateKeyBackupAuthData(
|
|
|
|
ctx context.Context, userID, version string, authData json.RawMessage,
|
|
|
|
) (err error) {
|
|
|
|
err = sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
2021-07-27 17:08:53 +01:00
|
|
|
return d.keyBackupVersions.updateKeyBackupAuthData(ctx, txn, userID, version, authData)
|
2021-07-27 12:47:32 +01:00
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Database) DeleteKeyBackup(
|
|
|
|
ctx context.Context, userID, version string,
|
|
|
|
) (exists bool, err error) {
|
|
|
|
err = sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
2021-07-27 17:08:53 +01:00
|
|
|
exists, err = d.keyBackupVersions.deleteKeyBackup(ctx, txn, userID, version)
|
2021-07-27 12:47:32 +01:00
|
|
|
return err
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Database) GetKeyBackup(
|
|
|
|
ctx context.Context, userID, version string,
|
2021-07-27 17:08:53 +01:00
|
|
|
) (versionResult, algorithm string, authData json.RawMessage, etag string, deleted bool, err error) {
|
2021-07-27 12:47:32 +01:00
|
|
|
err = sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
2021-07-27 17:08:53 +01:00
|
|
|
versionResult, algorithm, authData, etag, deleted, err = d.keyBackupVersions.selectKeyBackup(ctx, txn, userID, version)
|
2021-07-27 12:47:32 +01:00
|
|
|
return err
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2021-07-27 17:08:53 +01:00
|
|
|
|
2021-07-27 19:29:32 +01:00
|
|
|
func (d *Database) GetBackupKeys(
|
|
|
|
ctx context.Context, version, userID, filterRoomID, filterSessionID string,
|
|
|
|
) (result map[string]map[string]api.KeyBackupSession, err error) {
|
|
|
|
err = sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
if filterSessionID != "" {
|
|
|
|
result, err = d.keyBackups.selectKeysByRoomIDAndSessionID(ctx, txn, userID, version, filterRoomID, filterSessionID)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if filterRoomID != "" {
|
|
|
|
result, err = d.keyBackups.selectKeysByRoomID(ctx, txn, userID, version, filterRoomID)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
result, err = d.keyBackups.selectKeys(ctx, txn, userID, version)
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Database) CountBackupKeys(
|
|
|
|
ctx context.Context, version, userID string,
|
|
|
|
) (count int64, err error) {
|
|
|
|
err = sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
count, err = d.keyBackups.countKeys(ctx, txn, userID, version)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-07-27 17:08:53 +01:00
|
|
|
// nolint:nakedret
|
|
|
|
func (d *Database) UpsertBackupKeys(
|
|
|
|
ctx context.Context, version, userID string, uploads []api.InternalKeyBackupSession,
|
|
|
|
) (count int64, etag string, err error) {
|
|
|
|
// wrap the following logic in a txn to ensure we atomically upload keys
|
|
|
|
err = sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
_, _, _, oldETag, deleted, err := d.keyBackupVersions.selectKeyBackup(ctx, txn, userID, version)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if deleted {
|
|
|
|
return fmt.Errorf("backup was deleted")
|
|
|
|
}
|
|
|
|
// pull out all keys for this (user_id, version)
|
|
|
|
existingKeys, err := d.keyBackups.selectKeys(ctx, txn, userID, version)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
changed := false
|
|
|
|
// loop over all the new keys (which should be smaller than the set of backed up keys)
|
|
|
|
for _, newKey := range uploads {
|
|
|
|
// if we have a matching (room_id, session_id), we may need to update the key if it meets some rules, check them.
|
|
|
|
existingRoom := existingKeys[newKey.RoomID]
|
|
|
|
if existingRoom != nil {
|
|
|
|
existingSession, ok := existingRoom[newKey.SessionID]
|
|
|
|
if ok {
|
2021-07-27 19:29:32 +01:00
|
|
|
if existingSession.ShouldReplaceRoomKey(&newKey.KeyBackupSession) {
|
2021-07-27 17:08:53 +01:00
|
|
|
err = d.keyBackups.updateBackupKey(ctx, txn, userID, version, newKey)
|
|
|
|
changed = true
|
|
|
|
if err != nil {
|
2021-07-28 10:25:45 +01:00
|
|
|
return fmt.Errorf("d.keyBackups.updateBackupKey: %w", err)
|
2021-07-27 17:08:53 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// if we shouldn't replace the key we do nothing with it
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// if we're here, either the room or session are new, either way, we insert
|
|
|
|
err = d.keyBackups.insertBackupKey(ctx, txn, userID, version, newKey)
|
|
|
|
changed = true
|
|
|
|
if err != nil {
|
2021-07-28 10:25:45 +01:00
|
|
|
return fmt.Errorf("d.keyBackups.insertBackupKey: %w", err)
|
2021-07-27 17:08:53 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
count, err = d.keyBackups.countKeys(ctx, txn, userID, version)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if changed {
|
|
|
|
// update the etag
|
|
|
|
var newETag string
|
|
|
|
if oldETag == "" {
|
|
|
|
newETag = "1"
|
|
|
|
} else {
|
|
|
|
oldETagInt, err := strconv.ParseInt(oldETag, 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to parse old etag: %s", err)
|
|
|
|
}
|
|
|
|
newETag = strconv.FormatInt(oldETagInt+1, 10)
|
|
|
|
}
|
|
|
|
etag = newETag
|
|
|
|
return d.keyBackupVersions.updateKeyBackupETag(ctx, txn, userID, version, newETag)
|
|
|
|
} else {
|
|
|
|
etag = oldETag
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
2022-02-18 11:31:05 +00:00
|
|
|
|
|
|
|
// GetDeviceByAccessToken returns the device matching the given access token.
|
|
|
|
// Returns sql.ErrNoRows if no matching device was found.
|
|
|
|
func (d *Database) GetDeviceByAccessToken(
|
|
|
|
ctx context.Context, token string,
|
|
|
|
) (*api.Device, error) {
|
|
|
|
return d.devices.selectDeviceByToken(ctx, token)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetDeviceByID returns the device matching the given ID.
|
|
|
|
// Returns sql.ErrNoRows if no matching device was found.
|
|
|
|
func (d *Database) GetDeviceByID(
|
|
|
|
ctx context.Context, localpart, deviceID string,
|
|
|
|
) (*api.Device, error) {
|
|
|
|
return d.devices.selectDeviceByID(ctx, localpart, deviceID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetDevicesByLocalpart returns the devices matching the given localpart.
|
|
|
|
func (d *Database) GetDevicesByLocalpart(
|
|
|
|
ctx context.Context, localpart string,
|
|
|
|
) ([]api.Device, error) {
|
|
|
|
return d.devices.selectDevicesByLocalpart(ctx, nil, localpart, "")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Database) GetDevicesByID(ctx context.Context, deviceIDs []string) ([]api.Device, error) {
|
|
|
|
return d.devices.selectDevicesByID(ctx, deviceIDs)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateDevice makes a new device associated with the given user ID localpart.
|
|
|
|
// If there is already a device with the same device ID for this user, that access token will be revoked
|
|
|
|
// and replaced with the given accessToken. If the given accessToken is already in use for another device,
|
|
|
|
// an error will be returned.
|
|
|
|
// If no device ID is given one is generated.
|
|
|
|
// Returns the device on success.
|
|
|
|
func (d *Database) CreateDevice(
|
|
|
|
ctx context.Context, localpart string, deviceID *string, accessToken string,
|
|
|
|
displayName *string, ipAddr, userAgent string,
|
|
|
|
) (dev *api.Device, returnErr error) {
|
|
|
|
if deviceID != nil {
|
|
|
|
returnErr = sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
var err error
|
|
|
|
// Revoke existing tokens for this device
|
|
|
|
if err = d.devices.deleteDevice(ctx, txn, *deviceID, localpart); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
dev, err = d.devices.insertDevice(ctx, txn, *deviceID, localpart, accessToken, displayName, ipAddr, userAgent)
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
// We generate device IDs in a loop in case its already taken.
|
|
|
|
// We cap this at going round 5 times to ensure we don't spin forever
|
|
|
|
var newDeviceID string
|
|
|
|
for i := 1; i <= 5; i++ {
|
|
|
|
newDeviceID, returnErr = generateDeviceID()
|
|
|
|
if returnErr != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
returnErr = sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
var err error
|
|
|
|
dev, err = d.devices.insertDevice(ctx, txn, newDeviceID, localpart, accessToken, displayName, ipAddr, userAgent)
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
if returnErr == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// generateDeviceID creates a new device id. Returns an error if failed to generate
|
|
|
|
// random bytes.
|
|
|
|
func generateDeviceID() (string, error) {
|
|
|
|
b := make([]byte, deviceIDByteLength)
|
|
|
|
_, err := rand.Read(b)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
// url-safe no padding
|
|
|
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateDevice updates the given device with the display name.
|
|
|
|
// Returns SQL error if there are problems and nil on success.
|
|
|
|
func (d *Database) UpdateDevice(
|
|
|
|
ctx context.Context, localpart, deviceID string, displayName *string,
|
|
|
|
) error {
|
|
|
|
return sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
return d.devices.updateDeviceName(ctx, txn, localpart, deviceID, displayName)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveDevice revokes a device by deleting the entry in the database
|
|
|
|
// matching with the given device ID and user ID localpart.
|
|
|
|
// If the device doesn't exist, it will not return an error
|
|
|
|
// If something went wrong during the deletion, it will return the SQL error.
|
|
|
|
func (d *Database) RemoveDevice(
|
|
|
|
ctx context.Context, deviceID, localpart string,
|
|
|
|
) error {
|
|
|
|
return sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
if err := d.devices.deleteDevice(ctx, txn, deviceID, localpart); err != sql.ErrNoRows {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveDevices revokes one or more devices by deleting the entry in the database
|
|
|
|
// matching with the given device IDs and user ID localpart.
|
|
|
|
// If the devices don't exist, it will not return an error
|
|
|
|
// If something went wrong during the deletion, it will return the SQL error.
|
|
|
|
func (d *Database) RemoveDevices(
|
|
|
|
ctx context.Context, localpart string, devices []string,
|
|
|
|
) error {
|
|
|
|
return sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
if err := d.devices.deleteDevices(ctx, txn, localpart, devices); err != sql.ErrNoRows {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveAllDevices revokes devices by deleting the entry in the
|
|
|
|
// database matching the given user ID localpart.
|
|
|
|
// If something went wrong during the deletion, it will return the SQL error.
|
|
|
|
func (d *Database) RemoveAllDevices(
|
|
|
|
ctx context.Context, localpart, exceptDeviceID string,
|
|
|
|
) (devices []api.Device, err error) {
|
|
|
|
err = sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
devices, err = d.devices.selectDevicesByLocalpart(ctx, txn, localpart, exceptDeviceID)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := d.devices.deleteDevicesByLocalpart(ctx, txn, localpart, exceptDeviceID); err != sql.ErrNoRows {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateDeviceLastSeen updates a the last seen timestamp and the ip address
|
|
|
|
func (d *Database) UpdateDeviceLastSeen(ctx context.Context, localpart, deviceID, ipAddr string) error {
|
|
|
|
return sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
return d.devices.updateDeviceLastSeen(ctx, txn, localpart, deviceID, ipAddr)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateLoginToken generates a token, stores and returns it. The lifetime is
|
|
|
|
// determined by the loginTokenLifetime given to the Database constructor.
|
|
|
|
func (d *Database) CreateLoginToken(ctx context.Context, data *api.LoginTokenData) (*api.LoginTokenMetadata, error) {
|
|
|
|
tok, err := generateLoginToken()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
meta := &api.LoginTokenMetadata{
|
|
|
|
Token: tok,
|
|
|
|
Expiration: time.Now().Add(d.loginTokenLifetime),
|
|
|
|
}
|
|
|
|
|
|
|
|
err = sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
return d.loginTokens.insert(ctx, txn, meta, data)
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return meta, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func generateLoginToken() (string, error) {
|
|
|
|
b := make([]byte, loginTokenByteLength)
|
|
|
|
_, err := rand.Read(b)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return base64.RawURLEncoding.EncodeToString(b), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveLoginToken removes the named token (and may clean up other expired tokens).
|
|
|
|
func (d *Database) RemoveLoginToken(ctx context.Context, token string) error {
|
|
|
|
return sqlutil.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
|
|
return d.loginTokens.deleteByToken(ctx, txn, token)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetLoginTokenDataByToken returns the data associated with the given token.
|
|
|
|
// May return sql.ErrNoRows.
|
|
|
|
func (d *Database) GetLoginTokenDataByToken(ctx context.Context, token string) (*api.LoginTokenData, error) {
|
|
|
|
return d.loginTokens.selectByToken(ctx, token)
|
|
|
|
}
|