mirror of
https://github.com/multipleof4/ntfy.git
synced 2026-10-08 21:05:21 +00:00
WIP: Postgres read-only replica
This commit is contained in:
+24
-24
@@ -49,7 +49,7 @@ var (
|
||||
// Manager handles user authentication, authorization, and management
|
||||
type Manager struct {
|
||||
config *Config
|
||||
db *sql.DB
|
||||
db *db.DB
|
||||
queries queries
|
||||
statsQueue map[string]*Stats // "Queue" to asynchronously write user stats to the database (UserID -> Stats)
|
||||
tokenQueue map[string]*TokenUpdate // "Queue" to asynchronously write token access stats to the database (Token ID -> TokenUpdate)
|
||||
@@ -58,7 +58,7 @@ type Manager struct {
|
||||
|
||||
var _ Auther = (*Manager)(nil)
|
||||
|
||||
func newManager(db *sql.DB, queries queries, config *Config) (*Manager, error) {
|
||||
func newManager(d *db.DB, queries queries, config *Config) (*Manager, error) {
|
||||
if config.BcryptCost <= 0 {
|
||||
config.BcryptCost = DefaultUserPasswordBcryptCost
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func newManager(db *sql.DB, queries queries, config *Config) (*Manager, error) {
|
||||
}
|
||||
manager := &Manager{
|
||||
config: config,
|
||||
db: db,
|
||||
db: d,
|
||||
statsQueue: make(map[string]*Stats),
|
||||
tokenQueue: make(map[string]*TokenUpdate),
|
||||
queries: queries,
|
||||
@@ -388,7 +388,7 @@ func (a *Manager) writeUserStatsQueue() error {
|
||||
|
||||
// User returns the user with the given username if it exists, or ErrUserNotFound otherwise
|
||||
func (a *Manager) User(username string) (*User, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserByName, username)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectUserByName, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -397,7 +397,7 @@ func (a *Manager) User(username string) (*User, error) {
|
||||
|
||||
// UserByID returns the user with the given ID if it exists, or ErrUserNotFound otherwise
|
||||
func (a *Manager) UserByID(id string) (*User, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserByID, id)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectUserByID, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -406,7 +406,7 @@ func (a *Manager) UserByID(id string) (*User, error) {
|
||||
|
||||
// userByToken returns the user with the given token if it exists and is not expired, or ErrUserNotFound otherwise
|
||||
func (a *Manager) userByToken(token string) (*User, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserByToken, token, time.Now().Unix())
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectUserByToken, token, time.Now().Unix())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -415,7 +415,7 @@ func (a *Manager) userByToken(token string) (*User, error) {
|
||||
|
||||
// UserByStripeCustomer returns the user with the given Stripe customer ID if it exists, or ErrUserNotFound otherwise
|
||||
func (a *Manager) UserByStripeCustomer(customerID string) (*User, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserByStripeCustomerID, customerID)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectUserByStripeCustomerID, customerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -425,7 +425,7 @@ func (a *Manager) UserByStripeCustomer(customerID string) (*User, error) {
|
||||
// Users returns a list of users. It loads all users in a single query
|
||||
// rather than one query per user to avoid N+1 performance issues.
|
||||
func (a *Manager) Users() ([]*User, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUsers)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectUsers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -434,7 +434,7 @@ func (a *Manager) Users() ([]*User, error) {
|
||||
|
||||
// UsersCount returns the number of users in the database
|
||||
func (a *Manager) UsersCount() (int64, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserCount)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectUserCount)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -642,7 +642,7 @@ func (a *Manager) AllowReservation(username string, topic string) error {
|
||||
// - Furthermore, the query prioritizes more specific permissions (longer!) over more generic ones, e.g. "test*" > "*"
|
||||
// - It also prioritizes write permissions over read permissions
|
||||
func (a *Manager) authorizeTopicAccess(usernameOrEveryone, topic string) (read, write, found bool, err error) {
|
||||
rows, err := a.db.Query(a.queries.selectTopicPerms, Everyone, usernameOrEveryone, topic)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectTopicPerms, Everyone, usernameOrEveryone, topic)
|
||||
if err != nil {
|
||||
return false, false, false, err
|
||||
}
|
||||
@@ -660,7 +660,7 @@ func (a *Manager) authorizeTopicAccess(usernameOrEveryone, topic string) (read,
|
||||
|
||||
// AllGrants returns all user-specific access control entries, mapped to their respective user IDs
|
||||
func (a *Manager) AllGrants() (map[string][]Grant, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserAllAccess)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectUserAllAccess)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -688,7 +688,7 @@ func (a *Manager) AllGrants() (map[string][]Grant, error) {
|
||||
|
||||
// Grants returns all user-specific access control entries
|
||||
func (a *Manager) Grants(username string) ([]Grant, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserAccess, username)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectUserAccess, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -753,7 +753,7 @@ func (a *Manager) RemoveReservations(username string, topics ...string) error {
|
||||
|
||||
// Reservations returns all user-owned topics, and the associated everyone-access
|
||||
func (a *Manager) Reservations(username string) ([]Reservation, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserReservations, Everyone, username)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectUserReservations, Everyone, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -779,7 +779,7 @@ func (a *Manager) Reservations(username string) ([]Reservation, error) {
|
||||
|
||||
// HasReservation returns true if the given topic access is owned by the user
|
||||
func (a *Manager) HasReservation(username, topic string) (bool, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserHasReservation, username, escapeUnderscore(topic))
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectUserHasReservation, username, escapeUnderscore(topic))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -796,7 +796,7 @@ func (a *Manager) HasReservation(username, topic string) (bool, error) {
|
||||
|
||||
// ReservationsCount returns the number of reservations owned by this user
|
||||
func (a *Manager) ReservationsCount(username string) (int64, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserReservationsCount, username)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectUserReservationsCount, username)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -813,7 +813,7 @@ func (a *Manager) ReservationsCount(username string) (int64, error) {
|
||||
|
||||
// ReservationOwner returns user ID of the user that owns this topic, or an empty string if it's not owned by anyone
|
||||
func (a *Manager) ReservationOwner(topic string) (string, error) {
|
||||
rows, err := a.db.Query(a.queries.selectUserReservationsOwner, escapeUnderscore(topic))
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectUserReservationsOwner, escapeUnderscore(topic))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -830,7 +830,7 @@ func (a *Manager) ReservationOwner(topic string) (string, error) {
|
||||
|
||||
// otherAccessCount returns the number of access entries for the given topic that are not owned by the user
|
||||
func (a *Manager) otherAccessCount(username, topic string) (int, error) {
|
||||
rows, err := a.db.Query(a.queries.selectOtherAccessCount, escapeUnderscore(topic), escapeUnderscore(topic), username)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectOtherAccessCount, escapeUnderscore(topic), escapeUnderscore(topic), username)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -962,7 +962,7 @@ func (a *Manager) canChangeToken(userID, token string) error {
|
||||
|
||||
// Token returns a specific token for a user
|
||||
func (a *Manager) Token(userID, token string) (*Token, error) {
|
||||
rows, err := a.db.Query(a.queries.selectToken, userID, token)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectToken, userID, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -972,7 +972,7 @@ func (a *Manager) Token(userID, token string) (*Token, error) {
|
||||
|
||||
// Tokens returns all existing tokens for the user with the given user ID
|
||||
func (a *Manager) Tokens(userID string) ([]*Token, error) {
|
||||
rows, err := a.db.Query(a.queries.selectTokens, userID)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectTokens, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -991,7 +991,7 @@ func (a *Manager) Tokens(userID string) ([]*Token, error) {
|
||||
}
|
||||
|
||||
func (a *Manager) allProvisionedTokens() ([]*Token, error) {
|
||||
rows, err := a.db.Query(a.queries.selectAllProvisionedTokens)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectAllProvisionedTokens)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1114,7 +1114,7 @@ func (a *Manager) RemoveTier(code string) error {
|
||||
|
||||
// Tiers returns a list of all Tier structs
|
||||
func (a *Manager) Tiers() ([]*Tier, error) {
|
||||
rows, err := a.db.Query(a.queries.selectTiers)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectTiers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1134,7 +1134,7 @@ func (a *Manager) Tiers() ([]*Tier, error) {
|
||||
|
||||
// Tier returns a Tier based on the code, or ErrTierNotFound if it does not exist
|
||||
func (a *Manager) Tier(code string) (*Tier, error) {
|
||||
rows, err := a.db.Query(a.queries.selectTierByCode, code)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectTierByCode, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1144,7 +1144,7 @@ func (a *Manager) Tier(code string) (*Tier, error) {
|
||||
|
||||
// TierByStripePrice returns a Tier based on the Stripe price ID, or ErrTierNotFound if it does not exist
|
||||
func (a *Manager) TierByStripePrice(priceID string) (*Tier, error) {
|
||||
rows, err := a.db.Query(a.queries.selectTierByPriceID, priceID, priceID)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectTierByPriceID, priceID, priceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1185,7 +1185,7 @@ func (a *Manager) readTier(rows *sql.Rows) (*Tier, error) {
|
||||
|
||||
// PhoneNumbers returns all phone numbers for the user with the given user ID
|
||||
func (a *Manager) PhoneNumbers(userID string) ([]string, error) {
|
||||
rows, err := a.db.Query(a.queries.selectPhoneNumbers, userID)
|
||||
rows, err := a.db.ReadOnly().Query(a.queries.selectPhoneNumbers, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"heckel.io/ntfy/v2/db"
|
||||
)
|
||||
|
||||
// PostgreSQL queries
|
||||
@@ -278,9 +278,9 @@ var postgresQueries = queries{
|
||||
}
|
||||
|
||||
// NewPostgresManager creates a new Manager backed by a PostgreSQL database
|
||||
func NewPostgresManager(db *sql.DB, config *Config) (*Manager, error) {
|
||||
if err := setupPostgres(db); err != nil {
|
||||
func NewPostgresManager(d *db.DB, config *Config) (*Manager, error) {
|
||||
if err := setupPostgres(d.SetupPrimary()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newManager(db, postgresQueries, config)
|
||||
return newManager(d, postgresQueries, config)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
||||
|
||||
"heckel.io/ntfy/v2/db"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
@@ -280,15 +281,15 @@ func NewSQLiteManager(filename, startupQueries string, config *Config) (*Manager
|
||||
if !util.FileExists(parentDir) {
|
||||
return nil, fmt.Errorf("user database directory %s does not exist or is not accessible", parentDir)
|
||||
}
|
||||
db, err := sql.Open("sqlite3", filename)
|
||||
sqlDB, err := sql.Open("sqlite3", filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := setupSQLite(db); err != nil {
|
||||
if err := setupSQLite(sqlDB); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := runSQLiteStartupQueries(db, startupQueries); err != nil {
|
||||
if err := runSQLiteStartupQueries(sqlDB, startupQueries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newManager(db, sqliteQueries, config)
|
||||
return newManager(db.NewDB(sqlDB, nil), sqliteQueries, config)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"heckel.io/ntfy/v2/db"
|
||||
"heckel.io/ntfy/v2/db/pg"
|
||||
dbtest "heckel.io/ntfy/v2/db/test"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
@@ -38,7 +39,7 @@ func forEachBackend(t *testing.T, f func(t *testing.T, newManager newManagerFunc
|
||||
f(t, func(config *Config) *Manager {
|
||||
pool, err := pg.Open(schemaDSN)
|
||||
require.Nil(t, err)
|
||||
a, err := NewPostgresManager(pool, config)
|
||||
a, err := NewPostgresManager(db.NewDB(pool, nil), config)
|
||||
require.Nil(t, err)
|
||||
return a
|
||||
})
|
||||
@@ -1734,8 +1735,8 @@ func TestMigrationFrom4(t *testing.T) {
|
||||
require.Nil(t, a.Authorize(nil, "up", PermissionRead)) // % matches 0 or more characters
|
||||
}
|
||||
|
||||
func checkSchemaVersion(t *testing.T, db *sql.DB) {
|
||||
rows, err := db.Query(`SELECT version FROM schemaVersion`)
|
||||
func checkSchemaVersion(t *testing.T, d *db.DB) {
|
||||
rows, err := d.Query(`SELECT version FROM schemaVersion`)
|
||||
require.Nil(t, err)
|
||||
require.True(t, rows.Next())
|
||||
|
||||
@@ -1771,7 +1772,7 @@ func newTestManagerFromConfig(t *testing.T, newManager newManagerFunc, conf *Con
|
||||
return a
|
||||
}
|
||||
|
||||
func testDB(a *Manager) *sql.DB {
|
||||
func testDB(a *Manager) *db.DB {
|
||||
return a.db
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user