feat(security): add query mode handling for database operations
Tests / Integration Tests (push) Failing after 1s
Tests / Unit Tests (push) Failing after 24s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 59s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 58s
Build , Vet Test, and Lint / Build (push) Successful in 4m37s
Build , Vet Test, and Lint / Lint Code (push) Failing after 5m35s

* Introduced QueryMode to select between stored procedure and direct SQL execution.
* Implemented dbCapability to probe for stored procedure existence.
* Added table names configuration for direct SQL operations.
* Updated DatabaseTwoFactorProvider to support query mode and table names.
* Implemented direct SQL methods mirroring stored procedures for TOTP operations.
* Added tests for query mode logic and table names validation.
This commit is contained in:
Hein
2026-07-07 15:29:29 +02:00
parent 8a06aacfb2
commit eee83f9dc6
21 changed files with 3303 additions and 171 deletions
+52 -11
View File
@@ -23,6 +23,10 @@ type DatabaseKeyStoreOptions struct {
CacheTTL time.Duration
// SQLNames provides custom procedure names. If nil, uses DefaultKeyStoreSQLNames().
SQLNames *KeyStoreSQLNames
// TableNames provides custom table names for Direct mode. If nil, uses DefaultKeyStoreTableNames().
TableNames *KeyStoreTableNames
// QueryMode selects stored-procedure vs Direct-mode SQL. Default (zero value) is ModeAuto.
QueryMode QueryMode
// DBFactory is called to obtain a fresh *sql.DB when the existing connection is closed.
// If nil, reconnection is disabled.
DBFactory func() (*sql.DB, error)
@@ -38,12 +42,15 @@ type DatabaseKeyStoreOptions struct {
// cache TTL, a deleted key may continue to authenticate for up to CacheTTL
// (default 2 minutes) if the cache entry cannot be invalidated.
type DatabaseKeyStore struct {
db *sql.DB
dbMu sync.RWMutex
dbFactory func() (*sql.DB, error)
sqlNames *KeyStoreSQLNames
cache *cache.Cache
cacheTTL time.Duration
db *sql.DB
dbMu sync.RWMutex
dbFactory func() (*sql.DB, error)
sqlNames *KeyStoreSQLNames
tableNames *KeyStoreTableNames
queryMode QueryMode
capability *dbCapability
cache *cache.Cache
cacheTTL time.Duration
}
// NewDatabaseKeyStore creates a DatabaseKeyStore with optional configuration.
@@ -60,12 +67,16 @@ func NewDatabaseKeyStore(db *sql.DB, opts ...DatabaseKeyStoreOptions) *DatabaseK
c = cache.GetDefaultCache()
}
names := MergeKeyStoreSQLNames(DefaultKeyStoreSQLNames(), o.SQLNames)
tableNames := resolveKeyStoreTableNames(o.TableNames)
return &DatabaseKeyStore{
db: db,
dbFactory: o.DBFactory,
sqlNames: names,
cache: c,
cacheTTL: o.CacheTTL,
db: db,
dbFactory: o.DBFactory,
sqlNames: names,
tableNames: tableNames,
queryMode: o.QueryMode,
capability: newDBCapability(),
cache: c,
cacheTTL: o.CacheTTL,
}
}
@@ -86,6 +97,9 @@ func (ks *DatabaseKeyStore) reconnectDB() error {
ks.dbMu.Lock()
ks.db = newDB
ks.dbMu.Unlock()
if ks.capability != nil {
ks.capability.reset()
}
return nil
}
@@ -99,6 +113,14 @@ func (ks *DatabaseKeyStore) CreateKey(ctx context.Context, req CreateKeyRequest)
rawKey := base64.RawURLEncoding.EncodeToString(rawBytes)
hash := hashSHA256Hex(rawKey)
if !ks.capability.ShouldUseProcedure(ctx, ks.queryMode, ks.getDB(), ks.sqlNames.CreateKey) {
key, err := ks.createKeyDirect(ctx, req, hash)
if err != nil {
return nil, err
}
return &CreateKeyResponse{Key: *key, RawKey: rawKey}, nil
}
type createRequest struct {
UserID int `json:"user_id"`
KeyType KeyType `json:"key_type"`
@@ -145,6 +167,10 @@ func (ks *DatabaseKeyStore) CreateKey(ctx context.Context, req CreateKeyRequest)
// GetUserKeys returns all active, non-expired keys for the given user.
// Pass an empty KeyType to return all types.
func (ks *DatabaseKeyStore) GetUserKeys(ctx context.Context, userID int, keyType KeyType) ([]UserKey, error) {
if !ks.capability.ShouldUseProcedure(ctx, ks.queryMode, ks.getDB(), ks.sqlNames.GetUserKeys) {
return ks.getUserKeysDirect(ctx, userID, keyType)
}
var success bool
var errorMsg sql.NullString
var keysJSON sql.NullString
@@ -173,6 +199,10 @@ func (ks *DatabaseKeyStore) GetUserKeys(ctx context.Context, userID int, keyType
// The delete procedure returns the key_hash so no separate lookup is needed.
// Note: cache invalidation is best-effort; a cached entry may persist for up to CacheTTL.
func (ks *DatabaseKeyStore) DeleteKey(ctx context.Context, userID int, keyID int64) error {
if !ks.capability.ShouldUseProcedure(ctx, ks.queryMode, ks.getDB(), ks.sqlNames.DeleteKey) {
return ks.deleteKeyDirect(ctx, userID, keyID)
}
var success bool
var errorMsg sql.NullString
var keyHash sql.NullString
@@ -207,6 +237,17 @@ func (ks *DatabaseKeyStore) ValidateKey(ctx context.Context, rawKey string, keyT
}
}
if !ks.capability.ShouldUseProcedure(ctx, ks.queryMode, ks.getDB(), ks.sqlNames.ValidateKey) {
key, err := ks.validateKeyDirect(ctx, hash, keyType)
if err != nil {
return nil, err
}
if ks.cache != nil {
_ = ks.cache.Set(ctx, cacheKey, *key, ks.cacheTTL)
}
return key, nil
}
var success bool
var errorMsg sql.NullString
var keyJSON sql.NullString