refactor(security): move all database access into pkg/security/lookup

pkg/security no longer contains SQL. Every provider calls a store interface
from lookup, implemented by a procedure backend (Postgres stored procedures,
the default there) and a direct backend (dialect-driven SQL for postgres,
sqlite, mysql and mssql with configurable table and column names).

- add sectypes, lookup, lookup/{dialect,procedure,direct,backends,ddl,conformance}
- split totp and providers sub packages out of the core package
- replace SQLNames/TableNames/QueryMode with lookup.Config (see breaking_changes.md)
- direct backend now covers column/row security and API-key login
- move txsettings SQL to lookup.ApplyTxSettings; remove password.go
- move schema scripts under lookup/, add reference DDL per dialect
- add a shared conformance suite; run it on sqlite, and on Postgres in a
  podman/docker container (RESOLVESPEC_TEST_CONTAINERS=1)
- fix procedure schema bugs found on real Postgres: duplicate p_data
  parameter, JSON null arrays, expires_at timezone casts, passkey list
  GROUP BY, missing resolvespec_passkey_login; accept zone-less timestamps
This commit is contained in:
Hein
2026-10-01 13:19:44 +02:00
parent 60bd0a6dd3
commit c9fa8c60f2
118 changed files with 11218 additions and 5565 deletions
+14 -85
View File
@@ -13,6 +13,7 @@ import (
"time"
"github.com/bitechdev/ResolveSpec/pkg/logger"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup"
"golang.org/x/oauth2"
)
@@ -234,92 +235,20 @@ func (a *DatabaseAuthenticator) getOAuth2Provider(providerName string) (*OAuth2P
// oauth2GetOrCreateUser finds or creates a user based on OAuth2 info using stored procedure
func (a *DatabaseAuthenticator) oauth2GetOrCreateUser(ctx context.Context, userCtx *UserContext, providerName string) (int, error) {
if !a.capability.ShouldUseProcedure(ctx, a.queryMode, a.getDB(), a.sqlNames.OAuthGetOrCreateUser) {
return a.oauth2GetOrCreateUserDirect(ctx, userCtx, providerName)
}
userData := map[string]interface{}{
"username": userCtx.UserName,
"email": userCtx.Email,
"remote_id": userCtx.RemoteID,
"user_level": userCtx.UserLevel,
"roles": userCtx.Roles,
"auth_provider": providerName,
}
userJSON, err := json.Marshal(userData)
if err != nil {
return 0, fmt.Errorf("failed to marshal user data: %w", err)
}
var success bool
var errMsg *string
var userID *int
err = a.getDB().QueryRowContext(ctx, fmt.Sprintf(`
SELECT p_success, p_error, p_user_id
FROM %s($1::jsonb)
`, a.sqlNames.OAuthGetOrCreateUser), userJSON).Scan(&success, &errMsg, &userID)
if err != nil {
return 0, fmt.Errorf("failed to get or create user: %w", err)
}
if !success {
if errMsg != nil {
return 0, fmt.Errorf("%s", *errMsg)
}
return 0, fmt.Errorf("failed to get or create user")
}
if userID == nil {
return 0, fmt.Errorf("user ID not returned")
}
return *userID, nil
return a.src.get().OAuthUser.GetOrCreateUser(ctx, userCtx, providerName)
}
// oauth2CreateSession creates a new OAuth2 session using stored procedure
func (a *DatabaseAuthenticator) oauth2CreateSession(ctx context.Context, sessionToken string, userID int, token *oauth2.Token, expiresAt time.Time, providerName string) error {
if !a.capability.ShouldUseProcedure(ctx, a.queryMode, a.getDB(), a.sqlNames.OAuthCreateSession) {
return a.oauth2CreateSessionDirect(ctx, sessionToken, userID, token, expiresAt, providerName)
}
sessionData := map[string]interface{}{
"session_token": sessionToken,
"user_id": userID,
"access_token": token.AccessToken,
"refresh_token": token.RefreshToken,
"token_type": token.TokenType,
"expires_at": expiresAt,
"auth_provider": providerName,
}
sessionJSON, err := json.Marshal(sessionData)
if err != nil {
return fmt.Errorf("failed to marshal session data: %w", err)
}
var success bool
var errMsg *string
err = a.getDB().QueryRowContext(ctx, fmt.Sprintf(`
SELECT p_success, p_error
FROM %s($1::jsonb)
`, a.sqlNames.OAuthCreateSession), sessionJSON).Scan(&success, &errMsg)
if err != nil {
return fmt.Errorf("failed to create session: %w", err)
}
if !success {
if errMsg != nil {
return fmt.Errorf("%s", *errMsg)
}
return fmt.Errorf("failed to create session")
}
return nil
return a.src.get().OAuthUser.CreateSession(ctx, lookup.OAuthSession{
SessionToken: sessionToken,
UserID: userID,
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
TokenType: token.TokenType,
ExpiresAt: expiresAt,
Provider: providerName,
})
}
// validateState validates state using in-memory storage
@@ -420,7 +349,7 @@ func (a *DatabaseAuthenticator) OAuth2RefreshToken(ctx context.Context, refreshT
}
// Get session by refresh token from database
session, err := a.oauthGetByRefreshToken(ctx, refreshToken)
session, err := a.src.get().OAuthUser.GetByRefreshToken(ctx, refreshToken)
if err != nil {
return nil, err
}
@@ -447,12 +376,12 @@ func (a *DatabaseAuthenticator) OAuth2RefreshToken(ctx context.Context, refreshT
}
// Update session in database with new tokens
if err := a.oauthUpdateRefreshTokenRecord(ctx, session.UserID, refreshToken, newSessionToken, newToken.AccessToken, newToken.RefreshToken, newToken.Expiry); err != nil {
if err := a.src.get().OAuthUser.UpdateRefreshToken(ctx, session.UserID, refreshToken, newSessionToken, newToken.AccessToken, newToken.RefreshToken, newToken.Expiry); err != nil {
return nil, err
}
// Get user data
userCtx, err := a.oauthGetUserByID(ctx, session.UserID)
userCtx, err := a.src.get().OAuthUser.GetUser(ctx, session.UserID)
if err != nil {
return nil, err
}