Initial massaging of clientapi etc (not working yet)
This commit is contained in:
parent
f64e8feeec
commit
359bd722a0
|
@ -140,7 +140,7 @@ func RetrieveUserProfile(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID string,
|
userID string,
|
||||||
asAPI AppServiceQueryAPI,
|
asAPI AppServiceQueryAPI,
|
||||||
accountDB *accounts.Database,
|
accountDB accounts.Database,
|
||||||
) (*authtypes.Profile, error) {
|
) (*authtypes.Profile, error) {
|
||||||
localpart, _, err := gomatrixserverlib.SplitID('@', userID)
|
localpart, _, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -33,7 +33,7 @@ import (
|
||||||
// OutputRoomEventConsumer consumes events that originated in the room server.
|
// OutputRoomEventConsumer consumes events that originated in the room server.
|
||||||
type OutputRoomEventConsumer struct {
|
type OutputRoomEventConsumer struct {
|
||||||
roomServerConsumer *common.ContinualConsumer
|
roomServerConsumer *common.ContinualConsumer
|
||||||
db *accounts.Database
|
db accounts.Database
|
||||||
asDB *storage.Database
|
asDB *storage.Database
|
||||||
query api.RoomserverQueryAPI
|
query api.RoomserverQueryAPI
|
||||||
alias api.RoomserverAliasAPI
|
alias api.RoomserverAliasAPI
|
||||||
|
@ -46,7 +46,7 @@ type OutputRoomEventConsumer struct {
|
||||||
func NewOutputRoomEventConsumer(
|
func NewOutputRoomEventConsumer(
|
||||||
cfg *config.Dendrite,
|
cfg *config.Dendrite,
|
||||||
kafkaConsumer sarama.Consumer,
|
kafkaConsumer sarama.Consumer,
|
||||||
store *accounts.Database,
|
store accounts.Database,
|
||||||
appserviceDB *storage.Database,
|
appserviceDB *storage.Database,
|
||||||
queryAPI api.RoomserverQueryAPI,
|
queryAPI api.RoomserverQueryAPI,
|
||||||
aliasAPI api.RoomserverAliasAPI,
|
aliasAPI api.RoomserverAliasAPI,
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package accounts
|
package postgres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package accounts
|
package postgres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package accounts
|
package postgres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package accounts
|
package postgres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package accounts
|
package postgres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
381
clientapi/auth/storage/accounts/postgres/storage.go
Normal file
381
clientapi/auth/storage/accounts/postgres/storage.go
Normal file
|
@ -0,0 +1,381 @@
|
||||||
|
// 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"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/common"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
// Import the postgres database driver.
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Database represents an account database
|
||||||
|
type Database struct {
|
||||||
|
db *sql.DB
|
||||||
|
common.PartitionOffsetStatements
|
||||||
|
accounts accountsStatements
|
||||||
|
profiles profilesStatements
|
||||||
|
memberships membershipStatements
|
||||||
|
accountDatas accountDataStatements
|
||||||
|
threepids threepidStatements
|
||||||
|
filter filterStatements
|
||||||
|
serverName gomatrixserverlib.ServerName
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatabase creates a new accounts and profiles database
|
||||||
|
func NewDatabase(dataSourceName string, serverName gomatrixserverlib.ServerName) (*Database, error) {
|
||||||
|
var db *sql.DB
|
||||||
|
var err error
|
||||||
|
if db, err = sql.Open("postgres", dataSourceName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
partitions := common.PartitionOffsetStatements{}
|
||||||
|
if err = partitions.Prepare(db, "account"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
a := accountsStatements{}
|
||||||
|
if err = a.prepare(db, serverName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p := profilesStatements{}
|
||||||
|
if err = p.prepare(db); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m := membershipStatements{}
|
||||||
|
if err = m.prepare(db); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ac := accountDataStatements{}
|
||||||
|
if err = ac.prepare(db); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t := threepidStatements{}
|
||||||
|
if err = t.prepare(db); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f := filterStatements{}
|
||||||
|
if err = f.prepare(db); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Database{db, partitions, a, p, m, ac, t, f, serverName}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
) (*authtypes.Account, error) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// account already exists, it will return nil, nil.
|
||||||
|
func (d *Database) CreateAccount(
|
||||||
|
ctx context.Context, localpart, plaintextPassword, appserviceID string,
|
||||||
|
) (*authtypes.Account, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Generate a password hash if this is not a password-less user
|
||||||
|
hash := ""
|
||||||
|
if plaintextPassword != "" {
|
||||||
|
hash, err = hashPassword(plaintextPassword)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := d.profiles.insertProfile(ctx, localpart); err != nil {
|
||||||
|
if common.IsUniqueConstraintViolationErr(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d.accounts.insertAccount(ctx, localpart, hash, appserviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveMembership saves the user matching a given localpart as a member of a given
|
||||||
|
// room. It also stores the ID of the membership event.
|
||||||
|
// If a membership already exists between the user and the room, or if the
|
||||||
|
// insert fails, returns the SQL error
|
||||||
|
func (d *Database) saveMembership(
|
||||||
|
ctx context.Context, txn *sql.Tx, localpart, roomID, eventID string,
|
||||||
|
) error {
|
||||||
|
return d.memberships.insertMembership(ctx, txn, localpart, roomID, eventID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeMembershipsByEventIDs removes the memberships corresponding to the
|
||||||
|
// `join` membership events IDs in the eventIDs slice.
|
||||||
|
// If the removal fails, or if there is no membership to remove, returns an error
|
||||||
|
func (d *Database) removeMembershipsByEventIDs(
|
||||||
|
ctx context.Context, txn *sql.Tx, eventIDs []string,
|
||||||
|
) error {
|
||||||
|
return d.memberships.deleteMembershipsByEventIDs(ctx, txn, eventIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMemberships adds the "join" membership events included in a given state
|
||||||
|
// events array, and removes those which ID is included in a given array of events
|
||||||
|
// IDs. All of the process is run in a transaction, which commits only once/if every
|
||||||
|
// insertion and deletion has been successfully processed.
|
||||||
|
// Returns a SQL error if there was an issue with any part of the process
|
||||||
|
func (d *Database) UpdateMemberships(
|
||||||
|
ctx context.Context, eventsToAdd []gomatrixserverlib.Event, idsToRemove []string,
|
||||||
|
) error {
|
||||||
|
return common.WithTransaction(d.db, func(txn *sql.Tx) error {
|
||||||
|
if err := d.removeMembershipsByEventIDs(ctx, txn, idsToRemove); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, event := range eventsToAdd {
|
||||||
|
if err := d.newMembership(ctx, txn, event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMembershipInRoomByLocalpart returns the membership for an user
|
||||||
|
// matching the given localpart if he is a member of the room matching roomID,
|
||||||
|
// if not sql.ErrNoRows is returned.
|
||||||
|
// If there was an issue during the retrieval, returns the SQL error
|
||||||
|
func (d *Database) GetMembershipInRoomByLocalpart(
|
||||||
|
ctx context.Context, localpart, roomID string,
|
||||||
|
) (authtypes.Membership, error) {
|
||||||
|
return d.memberships.selectMembershipInRoomByLocalpart(ctx, localpart, roomID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMembershipsByLocalpart returns an array containing the memberships for all
|
||||||
|
// the rooms a user matching a given localpart is a member of
|
||||||
|
// If no membership match the given localpart, returns an empty array
|
||||||
|
// If there was an issue during the retrieval, returns the SQL error
|
||||||
|
func (d *Database) GetMembershipsByLocalpart(
|
||||||
|
ctx context.Context, localpart string,
|
||||||
|
) (memberships []authtypes.Membership, err error) {
|
||||||
|
return d.memberships.selectMembershipsByLocalpart(ctx, localpart)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newMembership saves a new membership in the database.
|
||||||
|
// If the event isn't a valid m.room.member event with type `join`, does nothing.
|
||||||
|
// If an error occurred, returns the SQL error
|
||||||
|
func (d *Database) newMembership(
|
||||||
|
ctx context.Context, txn *sql.Tx, ev gomatrixserverlib.Event,
|
||||||
|
) error {
|
||||||
|
if ev.Type() == "m.room.member" && ev.StateKey() != nil {
|
||||||
|
localpart, serverName, err := gomatrixserverlib.SplitID('@', *ev.StateKey())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only want state events from local users
|
||||||
|
if string(serverName) != string(d.serverName) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
eventID := ev.EventID()
|
||||||
|
roomID := ev.RoomID()
|
||||||
|
membership, err := ev.Membership()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only "join" membership events can be considered as new memberships
|
||||||
|
if membership == gomatrixserverlib.Join {
|
||||||
|
if err := d.saveMembership(ctx, txn, localpart, roomID, eventID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
ctx context.Context, localpart, roomID, dataType, content string,
|
||||||
|
) error {
|
||||||
|
return d.accountDatas.insertAccountData(ctx, localpart, roomID, dataType, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) (
|
||||||
|
global []gomatrixserverlib.ClientEvent,
|
||||||
|
rooms map[string][]gomatrixserverlib.ClientEvent,
|
||||||
|
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,
|
||||||
|
) (data *gomatrixserverlib.ClientEvent, err error) {
|
||||||
|
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) {
|
||||||
|
return d.accounts.selectNewNumericLocalpart(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashPassword(plaintext string) (hash string, err error) {
|
||||||
|
hashBytes, err := bcrypt.GenerateFromPassword([]byte(plaintext), bcrypt.DefaultCost)
|
||||||
|
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.
|
||||||
|
var Err3PIDInUse = errors.New("This third-party identifier is already in use")
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
return common.WithTransaction(d.db, func(txn *sql.Tx) error {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFilter looks up the filter associated with a given local user and filter ID.
|
||||||
|
// Returns a filter structure. Otherwise returns an error if no such filter exists
|
||||||
|
// or if there was an error talking to the database.
|
||||||
|
func (d *Database) GetFilter(
|
||||||
|
ctx context.Context, localpart string, filterID string,
|
||||||
|
) (*gomatrixserverlib.Filter, error) {
|
||||||
|
return d.filter.selectFilter(ctx, localpart, filterID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutFilter puts the passed filter into the database.
|
||||||
|
// Returns the filterID as a string. Otherwise returns an error if something
|
||||||
|
// goes wrong.
|
||||||
|
func (d *Database) PutFilter(
|
||||||
|
ctx context.Context, localpart string, filter *gomatrixserverlib.Filter,
|
||||||
|
) (string, error) {
|
||||||
|
return d.filter.insertFilter(ctx, filter, 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,
|
||||||
|
) (*authtypes.Account, error) {
|
||||||
|
return d.accounts.selectAccountByLocalpart(ctx, localpart)
|
||||||
|
}
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package accounts
|
package postgres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -1,381 +1,50 @@
|
||||||
// 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 accounts
|
package accounts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"net/url"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/common"
|
"github.com/matrix-org/dendrite/common"
|
||||||
|
"github.com/matrix-org/dendrite/mediaapi/storage/postgres"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
|
|
||||||
// Import the postgres database driver.
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Database represents an account database
|
type Database interface {
|
||||||
type Database struct {
|
common.PartitionStorer
|
||||||
db *sql.DB
|
GetAccountByPassword(ctx context.Context, localpart, plaintextPassword string) (*authtypes.Account, error)
|
||||||
common.PartitionOffsetStatements
|
GetProfileByLocalpart(ctx context.Context, localpart string) (*authtypes.Profile, error)
|
||||||
accounts accountsStatements
|
SetAvatarURL(ctx context.Context, localpart string, avatarURL string) error
|
||||||
profiles profilesStatements
|
SetDisplayName(ctx context.Context, localpart string, displayName string) error
|
||||||
memberships membershipStatements
|
CreateAccount(ctx context.Context, localpart, plaintextPassword, appserviceID string) (*authtypes.Account, error)
|
||||||
accountDatas accountDataStatements
|
UpdateMemberships(ctx context.Context, eventsToAdd []gomatrixserverlib.Event, idsToRemove []string) error
|
||||||
threepids threepidStatements
|
GetMembershipInRoomByLocalpart(ctx context.Context, localpart, roomID string) (authtypes.Membership, error)
|
||||||
filter filterStatements
|
GetMembershipsByLocalpart(ctx context.Context, localpart string) (memberships []authtypes.Membership, err error)
|
||||||
serverName gomatrixserverlib.ServerName
|
SaveAccountData(ctx context.Context, localpart, roomID, dataType, content string) error
|
||||||
|
GetAccountData(ctx context.Context, localpart string) (global []gomatrixserverlib.ClientEvent, rooms map[string][]gomatrixserverlib.ClientEvent, err error)
|
||||||
|
GetAccountDataByType(ctx context.Context, localpart, roomID, dataType string) (data *gomatrixserverlib.ClientEvent, err error)
|
||||||
|
GetNewNumericLocalpart(ctx context.Context) (int64, error)
|
||||||
|
SaveThreePIDAssociation(ctx context.Context, threepid, localpart, medium string) (err error)
|
||||||
|
RemoveThreePIDAssociation(ctx context.Context, threepid string, medium string) (err error)
|
||||||
|
GetLocalpartForThreePID(ctx context.Context, threepid string, medium string) (localpart string, err error)
|
||||||
|
GetThreePIDsForLocalpart(ctx context.Context, localpart string) (threepids []authtypes.ThreePID, err error)
|
||||||
|
GetFilter(ctx context.Context, localpart string, filterID string) (*gomatrixserverlib.Filter, error)
|
||||||
|
PutFilter(ctx context.Context, localpart string, filter *gomatrixserverlib.Filter) (string, error)
|
||||||
|
CheckAccountAvailability(ctx context.Context, localpart string) (bool, error)
|
||||||
|
GetAccountByLocalpart(ctx context.Context, localpart string) (*authtypes.Account, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDatabase creates a new accounts and profiles database
|
func Open(dataSourceName string) (Database, error) {
|
||||||
func NewDatabase(dataSourceName string, serverName gomatrixserverlib.ServerName) (*Database, error) {
|
uri, err := url.Parse(dataSourceName)
|
||||||
var db *sql.DB
|
|
||||||
var err error
|
|
||||||
if db, err = sql.Open("postgres", dataSourceName); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
partitions := common.PartitionOffsetStatements{}
|
|
||||||
if err = partitions.Prepare(db, "account"); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
a := accountsStatements{}
|
|
||||||
if err = a.prepare(db, serverName); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p := profilesStatements{}
|
|
||||||
if err = p.prepare(db); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
m := membershipStatements{}
|
|
||||||
if err = m.prepare(db); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ac := accountDataStatements{}
|
|
||||||
if err = ac.prepare(db); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
t := threepidStatements{}
|
|
||||||
if err = t.prepare(db); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
f := filterStatements{}
|
|
||||||
if err = f.prepare(db); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Database{db, partitions, a, p, m, ac, t, f, serverName}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
) (*authtypes.Account, error) {
|
|
||||||
hash, err := d.accounts.selectPasswordHash(ctx, localpart)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return postgres.Open(dataSourceName)
|
||||||
}
|
}
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(plaintextPassword)); err != nil {
|
switch uri.Scheme {
|
||||||
return nil, err
|
case "postgres":
|
||||||
|
return postgres.Open(dataSourceName)
|
||||||
|
case "file":
|
||||||
|
// return sqlite3.Open(dataSourceName)
|
||||||
|
default:
|
||||||
|
return postgres.Open(dataSourceName)
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// account already exists, it will return nil, nil.
|
|
||||||
func (d *Database) CreateAccount(
|
|
||||||
ctx context.Context, localpart, plaintextPassword, appserviceID string,
|
|
||||||
) (*authtypes.Account, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// Generate a password hash if this is not a password-less user
|
|
||||||
hash := ""
|
|
||||||
if plaintextPassword != "" {
|
|
||||||
hash, err = hashPassword(plaintextPassword)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := d.profiles.insertProfile(ctx, localpart); err != nil {
|
|
||||||
if common.IsUniqueConstraintViolationErr(err) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return d.accounts.insertAccount(ctx, localpart, hash, appserviceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveMembership saves the user matching a given localpart as a member of a given
|
|
||||||
// room. It also stores the ID of the membership event.
|
|
||||||
// If a membership already exists between the user and the room, or if the
|
|
||||||
// insert fails, returns the SQL error
|
|
||||||
func (d *Database) saveMembership(
|
|
||||||
ctx context.Context, txn *sql.Tx, localpart, roomID, eventID string,
|
|
||||||
) error {
|
|
||||||
return d.memberships.insertMembership(ctx, txn, localpart, roomID, eventID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeMembershipsByEventIDs removes the memberships corresponding to the
|
|
||||||
// `join` membership events IDs in the eventIDs slice.
|
|
||||||
// If the removal fails, or if there is no membership to remove, returns an error
|
|
||||||
func (d *Database) removeMembershipsByEventIDs(
|
|
||||||
ctx context.Context, txn *sql.Tx, eventIDs []string,
|
|
||||||
) error {
|
|
||||||
return d.memberships.deleteMembershipsByEventIDs(ctx, txn, eventIDs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateMemberships adds the "join" membership events included in a given state
|
|
||||||
// events array, and removes those which ID is included in a given array of events
|
|
||||||
// IDs. All of the process is run in a transaction, which commits only once/if every
|
|
||||||
// insertion and deletion has been successfully processed.
|
|
||||||
// Returns a SQL error if there was an issue with any part of the process
|
|
||||||
func (d *Database) UpdateMemberships(
|
|
||||||
ctx context.Context, eventsToAdd []gomatrixserverlib.Event, idsToRemove []string,
|
|
||||||
) error {
|
|
||||||
return common.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
||||||
if err := d.removeMembershipsByEventIDs(ctx, txn, idsToRemove); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, event := range eventsToAdd {
|
|
||||||
if err := d.newMembership(ctx, txn, event); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMembershipInRoomByLocalpart returns the membership for an user
|
|
||||||
// matching the given localpart if he is a member of the room matching roomID,
|
|
||||||
// if not sql.ErrNoRows is returned.
|
|
||||||
// If there was an issue during the retrieval, returns the SQL error
|
|
||||||
func (d *Database) GetMembershipInRoomByLocalpart(
|
|
||||||
ctx context.Context, localpart, roomID string,
|
|
||||||
) (authtypes.Membership, error) {
|
|
||||||
return d.memberships.selectMembershipInRoomByLocalpart(ctx, localpart, roomID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMembershipsByLocalpart returns an array containing the memberships for all
|
|
||||||
// the rooms a user matching a given localpart is a member of
|
|
||||||
// If no membership match the given localpart, returns an empty array
|
|
||||||
// If there was an issue during the retrieval, returns the SQL error
|
|
||||||
func (d *Database) GetMembershipsByLocalpart(
|
|
||||||
ctx context.Context, localpart string,
|
|
||||||
) (memberships []authtypes.Membership, err error) {
|
|
||||||
return d.memberships.selectMembershipsByLocalpart(ctx, localpart)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newMembership saves a new membership in the database.
|
|
||||||
// If the event isn't a valid m.room.member event with type `join`, does nothing.
|
|
||||||
// If an error occurred, returns the SQL error
|
|
||||||
func (d *Database) newMembership(
|
|
||||||
ctx context.Context, txn *sql.Tx, ev gomatrixserverlib.Event,
|
|
||||||
) error {
|
|
||||||
if ev.Type() == "m.room.member" && ev.StateKey() != nil {
|
|
||||||
localpart, serverName, err := gomatrixserverlib.SplitID('@', *ev.StateKey())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only want state events from local users
|
|
||||||
if string(serverName) != string(d.serverName) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
eventID := ev.EventID()
|
|
||||||
roomID := ev.RoomID()
|
|
||||||
membership, err := ev.Membership()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only "join" membership events can be considered as new memberships
|
|
||||||
if membership == gomatrixserverlib.Join {
|
|
||||||
if err := d.saveMembership(ctx, txn, localpart, roomID, eventID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(
|
|
||||||
ctx context.Context, localpart, roomID, dataType, content string,
|
|
||||||
) error {
|
|
||||||
return d.accountDatas.insertAccountData(ctx, localpart, roomID, dataType, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) (
|
|
||||||
global []gomatrixserverlib.ClientEvent,
|
|
||||||
rooms map[string][]gomatrixserverlib.ClientEvent,
|
|
||||||
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,
|
|
||||||
) (data *gomatrixserverlib.ClientEvent, err error) {
|
|
||||||
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) {
|
|
||||||
return d.accounts.selectNewNumericLocalpart(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hashPassword(plaintext string) (hash string, err error) {
|
|
||||||
hashBytes, err := bcrypt.GenerateFromPassword([]byte(plaintext), bcrypt.DefaultCost)
|
|
||||||
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.
|
|
||||||
var Err3PIDInUse = errors.New("This third-party identifier is already in use")
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
return common.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFilter looks up the filter associated with a given local user and filter ID.
|
|
||||||
// Returns a filter structure. Otherwise returns an error if no such filter exists
|
|
||||||
// or if there was an error talking to the database.
|
|
||||||
func (d *Database) GetFilter(
|
|
||||||
ctx context.Context, localpart string, filterID string,
|
|
||||||
) (*gomatrixserverlib.Filter, error) {
|
|
||||||
return d.filter.selectFilter(ctx, localpart, filterID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutFilter puts the passed filter into the database.
|
|
||||||
// Returns the filterID as a string. Otherwise returns an error if something
|
|
||||||
// goes wrong.
|
|
||||||
func (d *Database) PutFilter(
|
|
||||||
ctx context.Context, localpart string, filter *gomatrixserverlib.Filter,
|
|
||||||
) (string, error) {
|
|
||||||
return d.filter.insertFilter(ctx, filter, 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,
|
|
||||||
) (*authtypes.Account, error) {
|
|
||||||
return d.accounts.selectAccountByLocalpart(ctx, localpart)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package devices
|
package postgres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
167
clientapi/auth/storage/devices/postgres/storage.go
Normal file
167
clientapi/auth/storage/devices/postgres/storage.go
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
// 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"
|
||||||
|
"crypto/rand"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/common"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The length of generated device IDs
|
||||||
|
var deviceIDByteLength = 6
|
||||||
|
|
||||||
|
// Database represents a device database.
|
||||||
|
type Database struct {
|
||||||
|
db *sql.DB
|
||||||
|
devices devicesStatements
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatabase creates a new device database
|
||||||
|
func NewDatabase(dataSourceName string, serverName gomatrixserverlib.ServerName) (*Database, error) {
|
||||||
|
var db *sql.DB
|
||||||
|
var err error
|
||||||
|
if db, err = sql.Open("postgres", dataSourceName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d := devicesStatements{}
|
||||||
|
if err = d.prepare(db, serverName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Database{db, d}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
) (*authtypes.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,
|
||||||
|
) (*authtypes.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,
|
||||||
|
) ([]authtypes.Device, error) {
|
||||||
|
return d.devices.selectDevicesByLocalpart(ctx, localpart)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
) (dev *authtypes.Device, returnErr error) {
|
||||||
|
if deviceID != nil {
|
||||||
|
returnErr = common.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)
|
||||||
|
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 = common.WithTransaction(d.db, func(txn *sql.Tx) error {
|
||||||
|
var err error
|
||||||
|
dev, err = d.devices.insertDevice(ctx, txn, newDeviceID, localpart, accessToken, displayName)
|
||||||
|
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 common.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 common.WithTransaction(d.db, func(txn *sql.Tx) error {
|
||||||
|
if err := d.devices.deleteDevice(ctx, txn, deviceID, localpart); 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 string,
|
||||||
|
) error {
|
||||||
|
return common.WithTransaction(d.db, func(txn *sql.Tx) error {
|
||||||
|
if err := d.devices.deleteDevicesByLocalpart(ctx, txn, localpart); err != sql.ErrNoRows {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,167 +1,34 @@
|
||||||
// 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 devices
|
package devices
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"net/url"
|
||||||
"database/sql"
|
|
||||||
"encoding/base64"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/common"
|
"github.com/matrix-org/dendrite/mediaapi/storage/postgres"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// The length of generated device IDs
|
type Database interface {
|
||||||
var deviceIDByteLength = 6
|
GetDeviceByAccessToken(ctx context.Context, token string) (*authtypes.Device, error)
|
||||||
|
GetDeviceByID(ctx context.Context, localpart, deviceID string) (*authtypes.Device, error)
|
||||||
// Database represents a device database.
|
GetDevicesByLocalpart(ctx context.Context, localpart string) ([]authtypes.Device, error)
|
||||||
type Database struct {
|
CreateDevice(ctx context.Context, localpart string, deviceID *string, accessToken string, displayName *string)
|
||||||
db *sql.DB
|
UpdateDevice(ctx context.Context, localpart, deviceID string, displayName *string) error
|
||||||
devices devicesStatements
|
RemoveDevice(ctx context.Context, deviceID, localpart string) error
|
||||||
|
RemoveAllDevices(ctx context.Context, localpart string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDatabase creates a new device database
|
func Open(dataSourceName string) (Database, error) {
|
||||||
func NewDatabase(dataSourceName string, serverName gomatrixserverlib.ServerName) (*Database, error) {
|
uri, err := url.Parse(dataSourceName)
|
||||||
var db *sql.DB
|
|
||||||
var err error
|
|
||||||
if db, err = sql.Open("postgres", dataSourceName); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
d := devicesStatements{}
|
|
||||||
if err = d.prepare(db, serverName); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Database{db, d}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
) (*authtypes.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,
|
|
||||||
) (*authtypes.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,
|
|
||||||
) ([]authtypes.Device, error) {
|
|
||||||
return d.devices.selectDevicesByLocalpart(ctx, localpart)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
) (dev *authtypes.Device, returnErr error) {
|
|
||||||
if deviceID != nil {
|
|
||||||
returnErr = common.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)
|
|
||||||
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 = common.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
||||||
var err error
|
|
||||||
dev, err = d.devices.insertDevice(ctx, txn, newDeviceID, localpart, accessToken, displayName)
|
|
||||||
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 {
|
if err != nil {
|
||||||
return "", err
|
return postgres.Open(dataSourceName)
|
||||||
|
}
|
||||||
|
switch uri.Scheme {
|
||||||
|
case "postgres":
|
||||||
|
return postgres.Open(dataSourceName)
|
||||||
|
case "file":
|
||||||
|
//return sqlite3.Open(dataSourceName)
|
||||||
|
default:
|
||||||
|
return postgres.Open(dataSourceName)
|
||||||
}
|
}
|
||||||
// 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 common.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 common.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
||||||
if err := d.devices.deleteDevice(ctx, txn, deviceID, localpart); 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 string,
|
|
||||||
) error {
|
|
||||||
return common.WithTransaction(d.db, func(txn *sql.Tx) error {
|
|
||||||
if err := d.devices.deleteDevicesByLocalpart(ctx, txn, localpart); err != sql.ErrNoRows {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue