mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-07-08 03:47:37 +00:00
eee83f9dc6
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.
243 lines
9.1 KiB
Go
243 lines
9.1 KiB
Go
package security
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
// oauthRefreshSession is the session data needed to refresh an OAuth2 token,
|
|
// shared by both the stored-procedure and Direct-mode code paths.
|
|
type oauthRefreshSession struct {
|
|
UserID int `json:"user_id"`
|
|
AccessToken string `json:"access_token"`
|
|
TokenType string `json:"token_type"`
|
|
Expiry time.Time `json:"expiry"`
|
|
}
|
|
|
|
// oauth2GetOrCreateUserDirect mirrors resolvespec_oauth_getorcreateuser.
|
|
func (a *DatabaseAuthenticator) oauth2GetOrCreateUserDirect(ctx context.Context, userCtx *UserContext, providerName string) (int, error) {
|
|
rolesStr := strings.Join(userCtx.Roles, ",")
|
|
var userID int
|
|
|
|
err := a.runDBOpWithReconnect(func(db *sql.DB) error {
|
|
query := rewritePlaceholders(db, fmt.Sprintf(`SELECT id FROM %s WHERE email = ?`, a.tableNames.Users))
|
|
err := db.QueryRowContext(ctx, query, userCtx.Email).Scan(&userID)
|
|
if err == nil {
|
|
now := time.Now()
|
|
updQuery := rewritePlaceholders(db, fmt.Sprintf(
|
|
`UPDATE %s SET last_login_at = ?, updated_at = ?, remote_id = COALESCE(remote_id, ?), auth_provider = COALESCE(auth_provider, ?) WHERE id = ?`,
|
|
a.tableNames.Users))
|
|
_, err := db.ExecContext(ctx, updQuery, now, now, userCtx.RemoteID, providerName, userID)
|
|
return err
|
|
}
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
|
|
now := time.Now()
|
|
insQuery := rewritePlaceholders(db, fmt.Sprintf(
|
|
`INSERT INTO %s (username, email, password, user_level, roles, is_active, created_at, updated_at, last_login_at, remote_id, auth_provider) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
a.tableNames.Users))
|
|
res, err := db.ExecContext(ctx, insQuery, userCtx.UserName, userCtx.Email, userCtx.UserLevel, rolesStr, true, now, now, now, userCtx.RemoteID, providerName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
id, err := res.LastInsertId()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
userID = int(id)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to get or create user: %w", err)
|
|
}
|
|
return userID, nil
|
|
}
|
|
|
|
// oauth2CreateSessionDirect mirrors resolvespec_oauth_createsession (insert-or-update by session_token).
|
|
func (a *DatabaseAuthenticator) oauth2CreateSessionDirect(ctx context.Context, sessionToken string, userID int, token *oauth2.Token, expiresAt time.Time, providerName string) error {
|
|
return a.runDBOpWithReconnect(func(db *sql.DB) error {
|
|
var exists int
|
|
checkQuery := rewritePlaceholders(db, fmt.Sprintf(`SELECT 1 FROM %s WHERE session_token = ?`, a.tableNames.UserSessions))
|
|
err := db.QueryRowContext(ctx, checkQuery, sessionToken).Scan(&exists)
|
|
now := time.Now()
|
|
if err == nil {
|
|
updQuery := rewritePlaceholders(db, fmt.Sprintf(
|
|
`UPDATE %s SET access_token = ?, refresh_token = ?, token_type = ?, expires_at = ?, last_activity_at = ? WHERE session_token = ?`,
|
|
a.tableNames.UserSessions))
|
|
_, err := db.ExecContext(ctx, updQuery, token.AccessToken, token.RefreshToken, token.TokenType, expiresAt, now, sessionToken)
|
|
return err
|
|
}
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
|
|
insQuery := rewritePlaceholders(db, fmt.Sprintf(
|
|
`INSERT INTO %s (session_token, user_id, expires_at, created_at, last_activity_at, access_token, refresh_token, token_type, auth_provider) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
a.tableNames.UserSessions))
|
|
_, err = db.ExecContext(ctx, insQuery, sessionToken, userID, expiresAt, now, now, token.AccessToken, token.RefreshToken, token.TokenType, providerName)
|
|
return err
|
|
})
|
|
}
|
|
|
|
// oauthGetByRefreshToken retrieves the session for a refresh token, dispatching between
|
|
// the resolvespec_oauth_getrefreshtoken stored procedure and Direct-mode SQL.
|
|
func (a *DatabaseAuthenticator) oauthGetByRefreshToken(ctx context.Context, refreshToken string) (*oauthRefreshSession, error) {
|
|
if !a.capability.ShouldUseProcedure(ctx, a.queryMode, a.getDB(), a.sqlNames.OAuthGetRefreshToken) {
|
|
var session oauthRefreshSession
|
|
err := a.runDBOpWithReconnect(func(db *sql.DB) error {
|
|
query := rewritePlaceholders(db, fmt.Sprintf(
|
|
`SELECT user_id, access_token, token_type, expires_at FROM %s WHERE refresh_token = ? AND expires_at > ?`,
|
|
a.tableNames.UserSessions))
|
|
return db.QueryRowContext(ctx, query, refreshToken, time.Now()).Scan(&session.UserID, &session.AccessToken, &session.TokenType, &session.Expiry)
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, fmt.Errorf("refresh token not found or expired")
|
|
}
|
|
return nil, fmt.Errorf("failed to get session by refresh token: %w", err)
|
|
}
|
|
return &session, nil
|
|
}
|
|
|
|
var success bool
|
|
var errMsg *string
|
|
var sessionData []byte
|
|
|
|
err := a.getDB().QueryRowContext(ctx, fmt.Sprintf(`
|
|
SELECT p_success, p_error, p_data::text
|
|
FROM %s($1)
|
|
`, a.sqlNames.OAuthGetRefreshToken), refreshToken).Scan(&success, &errMsg, &sessionData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get session by refresh token: %w", err)
|
|
}
|
|
if !success {
|
|
if errMsg != nil {
|
|
return nil, fmt.Errorf("%s", *errMsg)
|
|
}
|
|
return nil, fmt.Errorf("invalid or expired refresh token")
|
|
}
|
|
|
|
var session oauthRefreshSession
|
|
if err := json.Unmarshal(sessionData, &session); err != nil {
|
|
return nil, fmt.Errorf("failed to parse session data: %w", err)
|
|
}
|
|
return &session, nil
|
|
}
|
|
|
|
// oauthUpdateRefreshTokenRecord updates a session with new tokens, dispatching between
|
|
// the resolvespec_oauth_updaterefreshtoken stored procedure and Direct-mode SQL.
|
|
func (a *DatabaseAuthenticator) oauthUpdateRefreshTokenRecord(ctx context.Context, userID int, oldRefreshToken, newSessionToken, newAccessToken, newRefreshToken string, expiresAt time.Time) error {
|
|
if !a.capability.ShouldUseProcedure(ctx, a.queryMode, a.getDB(), a.sqlNames.OAuthUpdateRefreshToken) {
|
|
var rows int64
|
|
err := a.runDBOpWithReconnect(func(db *sql.DB) error {
|
|
query := rewritePlaceholders(db, fmt.Sprintf(
|
|
`UPDATE %s SET session_token = ?, access_token = ?, refresh_token = ?, expires_at = ?, last_activity_at = ? WHERE user_id = ? AND refresh_token = ?`,
|
|
a.tableNames.UserSessions))
|
|
res, err := db.ExecContext(ctx, query, newSessionToken, newAccessToken, newRefreshToken, expiresAt, time.Now(), userID, oldRefreshToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rows, err = res.RowsAffected()
|
|
return err
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update session: %w", err)
|
|
}
|
|
if rows == 0 {
|
|
return fmt.Errorf("session not found")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
updateData := map[string]interface{}{
|
|
"user_id": userID,
|
|
"old_refresh_token": oldRefreshToken,
|
|
"new_session_token": newSessionToken,
|
|
"new_access_token": newAccessToken,
|
|
"new_refresh_token": newRefreshToken,
|
|
"expires_at": expiresAt,
|
|
}
|
|
updateJSON, err := json.Marshal(updateData)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal update data: %w", err)
|
|
}
|
|
|
|
var updateSuccess bool
|
|
var updateErrMsg *string
|
|
err = a.getDB().QueryRowContext(ctx, fmt.Sprintf(`
|
|
SELECT p_success, p_error
|
|
FROM %s($1::jsonb)
|
|
`, a.sqlNames.OAuthUpdateRefreshToken), updateJSON).Scan(&updateSuccess, &updateErrMsg)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to update session: %w", err)
|
|
}
|
|
if !updateSuccess {
|
|
if updateErrMsg != nil {
|
|
return fmt.Errorf("%s", *updateErrMsg)
|
|
}
|
|
return fmt.Errorf("failed to update session")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// oauthGetUserByID retrieves user data by ID, dispatching between the
|
|
// resolvespec_oauth_getuser stored procedure and Direct-mode SQL.
|
|
func (a *DatabaseAuthenticator) oauthGetUserByID(ctx context.Context, userID int) (*UserContext, error) {
|
|
if !a.capability.ShouldUseProcedure(ctx, a.queryMode, a.getDB(), a.sqlNames.OAuthGetUser) {
|
|
var username, email, roles, programUserTable sql.NullString
|
|
var userLevel, programUserID sql.NullInt64
|
|
err := a.runDBOpWithReconnect(func(db *sql.DB) error {
|
|
query := rewritePlaceholders(db, fmt.Sprintf(
|
|
`SELECT username, email, user_level, roles, program_user_id, program_user_table FROM %s WHERE id = ? AND is_active = ?`,
|
|
a.tableNames.Users))
|
|
return db.QueryRowContext(ctx, query, userID, true).Scan(&username, &email, &userLevel, &roles, &programUserID, &programUserTable)
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, fmt.Errorf("user not found")
|
|
}
|
|
return nil, fmt.Errorf("failed to get user data: %w", err)
|
|
}
|
|
return &UserContext{
|
|
UserID: userID,
|
|
UserName: username.String,
|
|
Email: email.String,
|
|
UserLevel: int(userLevel.Int64),
|
|
Roles: parseRoles(roles.String),
|
|
ProgramUserID: int(programUserID.Int64),
|
|
ProgramUserTable: programUserTable.String,
|
|
}, nil
|
|
}
|
|
|
|
var userSuccess bool
|
|
var userErrMsg *string
|
|
var userData []byte
|
|
err := a.getDB().QueryRowContext(ctx, fmt.Sprintf(`
|
|
SELECT p_success, p_error, p_data::text
|
|
FROM %s($1)
|
|
`, a.sqlNames.OAuthGetUser), userID).Scan(&userSuccess, &userErrMsg, &userData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user data: %w", err)
|
|
}
|
|
if !userSuccess {
|
|
if userErrMsg != nil {
|
|
return nil, fmt.Errorf("%s", *userErrMsg)
|
|
}
|
|
return nil, fmt.Errorf("failed to get user data")
|
|
}
|
|
var userCtx UserContext
|
|
if err := json.Unmarshal(userData, &userCtx); err != nil {
|
|
return nil, fmt.Errorf("failed to parse user context: %w", err)
|
|
}
|
|
return &userCtx, nil
|
|
}
|