Open the SQLite user database in the server and pass it to the user manager and the usage store

Like the Postgres pool: user.OpenSQLite opens the auth-file (case-sensitive LIKE),
NewSQLiteManager takes the handle and migrates it, and the usage store gets the same handle
instead of reaching into the manager (Manager.DB is gone). The usage table stays part of the
user schema.
This commit is contained in:
binwiederhier committed 2026-10-31 01:50:10 +00:00
1 parent 6c32bee44e
commit 521ca530b5
7 files changed
+58 -35

No files matched your search

+5 -1
View File
@@ -475,7 +475,11 @@ func createUserManager(c *cli.Context) (*user.Manager, error) {
if !util.FileExists(authFile) {
return nil, errors.New("auth-file does not exist; please start the server at least once to create it")
}
return user.NewSQLiteManager(authFile, authStartupQueries, authConfig)
userDB, err := user.OpenSQLite(authFile)
if err != nil {
return nil, err
}
return user.NewSQLiteManager(userDB, authStartupQueries, authConfig)
}
return nil, errors.New("option database-url or auth-file not set; auth is unconfigured for this server")
}
+3 -1
View File
@@ -170,7 +170,9 @@ func TestCLI_User_ResetPass_ProvisionedRejected(t *testing.T) {
defer test.StopServer(t, s, port)
// Seed a provisioned user into the auth database via config provisioning
m, err := user.NewSQLiteManager(conf.AuthFile, "", &user.Config{
d, err := user.OpenSQLite(conf.AuthFile)
require.Nil(t, err)
m, err := user.NewSQLiteManager(d, "", &user.Config{
ProvisionEnabled: true,
Users: []*user.User{
{Name: "provuser", Hash: "$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C", Role: user.RoleUser},
+4 -2
View File
@@ -36,12 +36,14 @@ func forEachBackend(t *testing.T, f func(t *testing.T, newStore func() *Store))
// newTestUserDB returns a fresh SQLite user database, which holds the usage table
func newTestUserDB(t testing.TB) *db.DB {
manager, err := user.NewSQLiteManager(filepath.Join(t.TempDir(), "user.db"), "", &user.Config{})
d, err := user.OpenSQLite(filepath.Join(t.TempDir(), "user.db"))
require.Nil(t, err)
manager, err := user.NewSQLiteManager(d, "", &user.Config{})
require.Nil(t, err)
t.Cleanup(func() {
manager.Close()
})
return manager.DB()
return d
}
// readAll returns the rows written at or after since, by key
+14 -9
View File
@@ -306,6 +306,7 @@ func New(conf *Config) (*Server, error) {
}
s.priceCache = util.NewLookupCache(s.fetchStripePrices, conf.StripePriceCacheDuration)
var userManager *user.Manager
var userDB *db.DB // The shared pool on Postgres, the auth-file on SQLite; also holds the usage table
if conf.AuthFile != "" || pool != nil {
authConfig := &user.Config{
Filename: conf.AuthFile,
@@ -324,9 +325,13 @@ func New(conf *Config) (*Server, error) {
AccessWrittenFunc: s.broadcastAccessChanged,
}
if pool != nil {
userManager, err = user.NewPostgresManager(pool, authConfig)
userDB = pool
userManager, err = user.NewPostgresManager(userDB, authConfig)
} else {
userManager, err = user.NewSQLiteManager(conf.AuthFile, conf.AuthStartupQueries, authConfig)
if userDB, err = user.OpenSQLite(conf.AuthFile); err != nil {
return nil, err
}
userManager, err = user.NewSQLiteManager(userDB, conf.AuthStartupQueries, authConfig)
}
if err != nil {
return nil, err
@@ -348,7 +353,7 @@ func New(conf *Config) (*Server, error) {
}
s.userManager = userManager
s.limiter = limit.New(&limit.Config{
Store: createUsageStore(pool, userManager),
Store: createUsageStore(pool, userDB),
})
s.visitors = newVisitorEnv(conf, s.limiter, messageCache, userManager)
s.firebaseClient = firebaseClient
@@ -397,14 +402,14 @@ func (s *Server) dbPoolStats() []*metrics.DBPool {
return pools
}
// createUsageStore returns the store of the usage table, which is part of the user database: the
// shared pool on Postgres, the auth-file on SQLite. Without a user database, the daily counts are
// kept in memory only.
func createUsageStore(pool *db.DB, userManager *user.Manager) *limit.Store {
// createUsageStore returns the store of the usage table, which is part of the user schema (the
// user manager creates and migrates it): on the shared pool on Postgres, in the auth-file on
// SQLite. Without a user database, the daily counts are kept in memory only.
func createUsageStore(pool, userDB *db.DB) *limit.Store {
if pool != nil {
return limit.NewPostgresStore(pool)
} else if userManager != nil {
return limit.NewSQLiteStore(userManager.DB())
} else if userDB != nil {
return limit.NewSQLiteStore(userDB)
}
return nil
}
-6
View File
@@ -1890,12 +1890,6 @@ func (a *Manager) maybeProvisionTokens(tx *sql.Tx, provisionUsernames []string,
return nil
}
// DB returns the manager's database. It also holds the usage table, which package limit reads and
// writes (see the usage table in the schema files).
func (a *Manager) DB() *db.DB {
return a.db
}
// Close stops background goroutines and closes the underlying database.
func (a *Manager) Close() error {
select {
+12 -5
View File
@@ -322,8 +322,9 @@ var sqliteQueries = queries{
updateBilling: sqliteUpdateBillingQuery,
}
// NewSQLiteManager creates a new Manager backed by a SQLite database
func NewSQLiteManager(filename, startupQueries string, config *Config) (*Manager, error) {
// OpenSQLite opens the SQLite user database (auth-file), to pass to NewSQLiteManager and to the
// other stores whose tables are part of the user schema (the usage table, see package limit)
func OpenSQLite(filename string) (*db.DB, error) {
parentDir := filepath.Dir(filename)
if !util.FileExists(parentDir) {
return nil, fmt.Errorf("user database directory %s does not exist or is not accessible", parentDir)
@@ -338,14 +339,20 @@ func NewSQLiteManager(filename, startupQueries string, config *Config) (*Manager
if err != nil {
return nil, err
}
return db.New(&db.Host{DB: d}, nil), nil
}
// NewSQLiteManager creates a new Manager on the SQLite user database (see OpenSQLite), creating
// or migrating its schema
func NewSQLiteManager(d *db.DB, startupQueries string, config *Config) (*Manager, error) {
// Migrations must run before the startup queries: the 5 -> 6 table rebuilds rely on
// foreign keys being OFF, which is only guaranteed on fresh connections (the foreign_keys
// pragma is enabled as part of the builtin startup queries below)
if err := schema.Migrate(d, schema.SQLite, schemaStore, sqliteCreateTables, sqliteMigrations); err != nil {
if err := schema.Migrate(d.Primary(), schema.SQLite, schemaStore, sqliteCreateTables, sqliteMigrations); err != nil {
return nil, err
}
if err := runSQLiteStartupQueries(d, startupQueries); err != nil {
if err := runSQLiteStartupQueries(d.Primary(), startupQueries); err != nil {
return nil, err
}
return newManager(db.New(&db.Host{DB: d}, nil), sqliteQueries, config)
return newManager(d, sqliteQueries, config)
}
+20 -11
View File
@@ -33,7 +33,7 @@ func forEachBackend(t *testing.T, f func(t *testing.T, newManager newManagerFunc
t.Run("sqlite", func(t *testing.T) {
dir := t.TempDir()
f(t, func(config *Config) *Manager {
a, err := NewSQLiteManager(filepath.Join(dir, "user.db"), "", config)
a, err := newTestSQLiteManager(filepath.Join(dir, "user.db"), "", config)
require.Nil(t, err)
return a
})
@@ -1918,7 +1918,7 @@ func newTestManager(t *testing.T, newManager newManagerFunc, defaultAccess Permi
}
func newTestManagerFromFile(t *testing.T, filename, startupQueries string, defaultAccess Permission, bcryptCost int, statsWriterInterval time.Duration) *Manager {
a, err := NewSQLiteManager(filename, startupQueries, &Config{
a, err := newTestSQLiteManager(filename, startupQueries, &Config{
DefaultAccess: defaultAccess,
BcryptCost: bcryptCost,
QueueWriterInterval: statsWriterInterval,
@@ -1939,7 +1939,7 @@ func testDB(a *Manager) *db.DB {
func forEachStoreBackend(t *testing.T, f func(t *testing.T, manager *Manager)) {
t.Run("sqlite", func(t *testing.T) {
manager, err := NewSQLiteManager(filepath.Join(t.TempDir(), "user.db"), "", &Config{})
manager, err := newTestSQLiteManager(filepath.Join(t.TempDir(), "user.db"), "", &Config{})
require.Nil(t, err)
t.Cleanup(func() { manager.Close() })
f(t, manager)
@@ -2658,7 +2658,7 @@ func TestManager_AccessWritesTellPeers(t *testing.T) {
// topic matching being case-insensitive. SQLite's LIKE is case-insensitive for
// ASCII by default, which would let a request for "SECRET" match an ACL rule
// for "secret" -- a security hole. PostgreSQL's LIKE is already case-sensitive.
// NewSQLiteManager opens the database with case_sensitive_like enabled to close
// OpenSQLite opens the database with case_sensitive_like enabled to close
// this gap. This exercises the direct-DB path (cache disabled), which is the
// path that runs the LIKE query; the in-memory cache is independently
// case-sensitive (Go map keys / case-sensitive regex).
@@ -3640,11 +3640,11 @@ func TestMigrationFrom9To10Postgres(t *testing.T) {
func testMigrationFrom9To10(t *testing.T, a *Manager, setVersionQuery, setStatsQuery string) {
t.Helper()
require.Nil(t, a.AddUser("phil", "phil", RoleUser, false))
_, err := a.DB().Exec(`DROP TABLE IF EXISTS usage`)
_, err := a.db.Exec(`DROP TABLE IF EXISTS usage`)
require.Nil(t, err)
_, err = a.DB().Exec(setVersionQuery)
_, err = a.db.Exec(setVersionQuery)
require.Nil(t, err)
_, err = a.DB().Exec(setStatsQuery)
_, err = a.db.Exec(setStatsQuery)
require.Nil(t, err)
require.Nil(t, a.Close())
}
@@ -3653,14 +3653,23 @@ func testMigrationFrom9To10(t *testing.T, a *Manager, setVersionQuery, setStatsQ
func checkMigrated9To10(t *testing.T, a *Manager, selectVersionQuery, selectStatsQuery string) {
t.Helper()
var version int
require.Nil(t, a.DB().QueryRow(selectVersionQuery).Scan(&version))
require.Nil(t, a.db.QueryRow(selectVersionQuery).Scan(&version))
require.Equal(t, 10, version)
var messages, emails, calls int64
require.Nil(t, a.DB().QueryRow(selectStatsQuery).Scan(&messages, &emails, &calls))
require.Nil(t, a.db.QueryRow(selectStatsQuery).Scan(&messages, &emails, &calls))
require.Equal(t, []int64{0, 0, 0}, []int64{messages, emails, calls})
_, err := a.DB().Exec(`INSERT INTO usage (key, period_start, messages, emails, calls, updated_at) VALUES ('ip:1.2.3.4', 1793344380, 37, 0, 0, 1793380805)`)
_, err := a.db.Exec(`INSERT INTO usage (key, period_start, messages, emails, calls, updated_at) VALUES ('ip:1.2.3.4', 1793344380, 37, 0, 0, 1793380805)`)
require.Nil(t, err)
var count int64
require.Nil(t, a.DB().QueryRow(`SELECT messages FROM usage WHERE key = 'ip:1.2.3.4' AND updated_at >= 1793380805`).Scan(&count))
require.Nil(t, a.db.QueryRow(`SELECT messages FROM usage WHERE key = 'ip:1.2.3.4' AND updated_at >= 1793380805`).Scan(&count))
require.Equal(t, int64(37), count)
}
// newTestSQLiteManager opens the SQLite user database and creates a manager on it
func newTestSQLiteManager(filename, startupQueries string, config *Config) (*Manager, error) {
d, err := OpenSQLite(filename)
if err != nil {
return nil, err
}
return NewSQLiteManager(d, startupQueries, config)
}