Compare commits

..

5 Commits

Author SHA1 Message Date
Hein 873e8925d4 fix(handler): ensure target ID is provided for updates
Tests / Unit Tests (push) Failing after 9s
Tests / Integration Tests (push) Failing after 12s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 48s
Build , Vet Test, and Lint / Build (push) Successful in 1m18s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 1m27s
Build , Vet Test, and Lint / Lint Code (push) Successful in 1m28s
2026-07-24 16:35:22 +02:00
Hein b23916048a fix: ordering in before hook 2026-07-24 15:56:23 +02:00
Hein 47708fc87a fix(security): run RLS-scoping hooks in the same transaction as their queries
Tests / Unit Tests (push) Failing after 12s
Tests / Integration Tests (push) Failing after 16s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 51s
Build , Vet Test, and Lint / Build (push) Successful in 1m24s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 1m31s
Build , Vet Test, and Lint / Lint Code (push) Successful in 2m4s
BeforeRead/BeforeCreate hooks (used to set session-scoped RLS GUCs) were
firing against the pooled db handle while the actual queries ran as
separate calls to the same pool. Under connection pooling these could
land on different physical connections, silently bypassing row-level
security on creates and reads. handleUpdate already did this correctly;
handleRead/handleCreate in both resolvespec and restheadspec now wrap
hook execution and queries in a single RunInTransaction call.
2026-07-24 12:40:27 +02:00
Hein a85e572732 fix(hooks): reset Tx to pooled connection for post-commit hook calls
Tests / Unit Tests (push) Failing after 2m40s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 2m9s
Tests / Integration Tests (push) Failing after 2m47s
Build , Vet Test, and Lint / Build (push) Successful in 5m56s
Build , Vet Test, and Lint / Lint Code (push) Failing after 6m37s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 6m48s
BeforeScan (restheadspec handleUpdate) and BeforeResponse (funcspec
list/single query handlers) fire after RunInTransaction commits, but
hookCtx.Tx still pointed at the now-dead transaction. Any hook that
executed a query against Tx (e.g. setUserViaContext) failed with
"sql: transaction has already been committed or rolled back".
2026-07-20 13:45:13 +02:00
Hein 598fd687f6 feat(security): add GetUserRef method for opaque user identifiers
Tests / Integration Tests (push) Failing after 1s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 2m6s
Build , Vet Test, and Lint / Lint Code (push) Successful in 2m21s
Tests / Unit Tests (push) Failing after 21s
Build , Vet Test, and Lint / Build (push) Successful in 50s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 54s
2026-07-10 13:47:32 +02:00
21 changed files with 1165 additions and 871 deletions
+8 -2
View File
@@ -331,7 +331,10 @@ func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFun
w.Header().Set("Content-Range", fmt.Sprintf("items %d-%d/%d", respOffset, respOffset+len(dbobjlist), total))
logger.Info("Serving: Records %d of %d", len(dbobjlist), total)
// Execute BeforeResponse hook
// Execute BeforeResponse hook. The transaction has already committed by
// this point, so hooks must use the pooled connection rather than the
// now-dead tx.
hookCtx.Tx = h.db
hookCtx.Result = dbobjlist
hookCtx.Total = total
if err := h.hooks.Execute(BeforeResponse, hookCtx); err != nil {
@@ -631,7 +634,10 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
return
}
// Execute BeforeResponse hook
// Execute BeforeResponse hook. The transaction has already committed by
// this point, so hooks must use the pooled connection rather than the
// now-dead tx.
hookCtx.Tx = h.db
hookCtx.Result = dbobj
if err := h.hooks.Execute(BeforeResponse, hookCtx); err != nil {
logger.Error("BeforeResponse hook failed: %v", err)
+10
View File
@@ -71,6 +71,16 @@ func (f *funcSpecSecurityContext) GetUserID() (int, bool) {
return int(f.ctx.UserContext.UserID), true
}
// GetUserRef returns an opaque user identifier for row security lookups.
// It returns the full *security.UserContext so providers can read JWT claims
// (e.g. a UUID subject) instead of relying on the int user ID.
func (f *funcSpecSecurityContext) GetUserRef() (any, bool) {
if f.ctx.UserContext == nil {
return nil, false
}
return f.ctx.UserContext, true
}
func (f *funcSpecSecurityContext) GetSchema() string {
// funcspec doesn't have a schema concept, extract from SQL query or use default
return "public"
+11
View File
@@ -71,6 +71,17 @@ func (s *securityContext) GetUserID() (int, bool) {
return security.GetUserID(s.ctx.Context)
}
// GetUserRef returns an opaque user identifier for row security lookups.
// It prefers the full *security.UserContext (so providers can read JWT claims,
// e.g. a UUID subject) and falls back to the int user ID.
func (s *securityContext) GetUserRef() (any, bool) {
if userCtx, ok := security.GetUserContext(s.ctx.Context); ok {
return userCtx, true
}
userID, ok := security.GetUserID(s.ctx.Context)
return userID, ok
}
func (s *securityContext) GetSchema() string {
return s.ctx.Schema
}
+11
View File
@@ -84,6 +84,17 @@ func (s *securityContext) GetUserID() (int, bool) {
return security.GetUserID(s.ctx.Context)
}
// GetUserRef returns an opaque user identifier for row security lookups.
// It prefers the full *security.UserContext (so providers can read JWT claims,
// e.g. a UUID subject) and falls back to the int user ID.
func (s *securityContext) GetUserRef() (any, bool) {
if userCtx, ok := security.GetUserContext(s.ctx.Context); ok {
return userCtx, true
}
userID, ok := security.GetUserID(s.ctx.Context)
return userID, ok
}
func (s *securityContext) GetSchema() string {
return s.ctx.Schema
}
+497 -350
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -78,6 +78,17 @@ func (s *securityContext) GetUserID() (int, bool) {
return security.GetUserID(s.ctx.Context)
}
// GetUserRef returns an opaque user identifier for row security lookups.
// It prefers the full *security.UserContext (so providers can read JWT claims,
// e.g. a UUID subject) and falls back to the int user ID.
func (s *securityContext) GetUserRef() (any, bool) {
if userCtx, ok := security.GetUserContext(s.ctx.Context); ok {
return userCtx, true
}
userID, ok := security.GetUserID(s.ctx.Context)
return userID, ok
}
func (s *securityContext) GetSchema() string {
return s.ctx.Schema
}
File diff suppressed because it is too large Load Diff
+11
View File
@@ -77,6 +77,17 @@ func (s *securityContext) GetUserID() (int, bool) {
return security.GetUserID(s.ctx.Context)
}
// GetUserRef returns an opaque user identifier for row security lookups.
// It prefers the full *security.UserContext (so providers can read JWT claims,
// e.g. a UUID subject) and falls back to the int user ID.
func (s *securityContext) GetUserRef() (any, bool) {
if userCtx, ok := security.GetUserContext(s.ctx.Context); ok {
return userCtx, true
}
userID, ok := security.GetUserID(s.ctx.Context)
return userID, ok
}
func (s *securityContext) GetSchema() string {
return s.ctx.Schema
}
+2 -2
View File
@@ -74,8 +74,8 @@ func (c *CompositeSecurityProvider) GetColumnSecurity(ctx context.Context, userI
}
// GetRowSecurity delegates to the row security provider
func (c *CompositeSecurityProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error) {
return c.rowSec.GetRowSecurity(ctx, userID, schema, table)
func (c *CompositeSecurityProvider) GetRowSecurity(ctx context.Context, userRef any, schema, table string) (RowSecurity, error) {
return c.rowSec.GetRowSecurity(ctx, userRef, schema, table)
}
// Optional interface implementations (if wrapped providers support them)
+1 -1
View File
@@ -79,7 +79,7 @@ type mockRowSec struct {
supportsCache bool
}
func (m *mockRowSec) GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error) {
func (m *mockRowSec) GetRowSecurity(ctx context.Context, userRef any, schema, table string) (RowSecurity, error) {
return m.rowSec, m.err
}
+23 -9
View File
@@ -14,6 +14,11 @@ import (
type SecurityContext interface {
GetContext() context.Context
GetUserID() (int, bool)
// GetUserRef returns an opaque user identifier for row security lookups.
// Unlike GetUserID, it is not required to be an integer: implementations backed by
// non-integer identifiers (e.g. UUIDs) can return a string, or the full
// *security.UserContext so a RowSecurityProvider can read JWT claims directly.
GetUserRef() (any, bool)
GetSchema() string
GetEntity() string
GetModel() interface{}
@@ -45,8 +50,13 @@ func loadSecurityRules(secCtx SecurityContext, securityList *SecurityList) error
// return err
}
// Load row security rules using the provider
_, err = securityList.LoadRowSecurity(secCtx.GetContext(), userID, schema, tablename, false)
// Load row security rules using the provider. Row security uses the opaque
// user ref (not the int-only user ID) so non-integer user identifiers work.
userRef, refOK := secCtx.GetUserRef()
if !refOK {
userRef = userID
}
_, err = securityList.LoadRowSecurity(secCtx.GetContext(), userRef, schema, tablename, false)
if err != nil {
logger.Warn("Failed to load row security: %v", err)
// Don't fail the request if no security rules exist
@@ -58,25 +68,29 @@ func loadSecurityRules(secCtx SecurityContext, securityList *SecurityList) error
// applyRowSecurity applies row-level security filters to the query (generic version)
func applyRowSecurity(secCtx SecurityContext, securityList *SecurityList) error {
userID, ok := secCtx.GetUserID()
userRef, ok := secCtx.GetUserRef()
if !ok {
return nil // No user context, skip
userID, idOK := secCtx.GetUserID()
if !idOK {
return nil // No user context, skip
}
userRef = userID
}
schema := secCtx.GetSchema()
tablename := secCtx.GetEntity()
// Get row security template
rowSec, err := securityList.GetRowSecurityTemplate(userID, schema, tablename)
rowSec, err := securityList.GetRowSecurityTemplate(userRef, schema, tablename)
if err != nil {
// No row security defined, allow query to proceed
logger.Debug("No row security for %s.%s@%d: %v", schema, tablename, userID, err)
logger.Debug("No row security for %s.%s@%v: %v", schema, tablename, userRef, err)
return nil
}
// Check if user has a blocking rule
if rowSec.HasBlock {
logger.Warn("User %d blocked from accessing %s.%s", userID, schema, tablename)
logger.Warn("User %v blocked from accessing %s.%s", userRef, schema, tablename)
return fmt.Errorf("access denied to %s", tablename)
}
@@ -112,8 +126,8 @@ func applyRowSecurity(secCtx SecurityContext, securityList *SecurityList) error
// Generate the WHERE clause from template
whereClause := rowSec.GetTemplate(pkName, modelType)
logger.Info("Applying row security filter for user %d on %s.%s: %s",
userID, schema, tablename, whereClause)
logger.Info("Applying row security filter for user %v on %s.%s: %s",
userRef, schema, tablename, whereClause)
// Apply the WHERE clause to the query
query := secCtx.GetQuery()
+4
View File
@@ -26,6 +26,10 @@ func (m *mockSecurityContext) GetUserID() (int, bool) {
return m.userID, m.hasUser
}
func (m *mockSecurityContext) GetUserRef() (any, bool) {
return m.userID, m.hasUser
}
func (m *mockSecurityContext) GetSchema() string {
return m.schema
}
+6 -2
View File
@@ -121,8 +121,12 @@ type ColumnSecurityProvider interface {
// RowSecurityProvider handles row-level security (filtering)
type RowSecurityProvider interface {
// GetRowSecurity loads row security rules for a user and entity
GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error)
// GetRowSecurity loads row security rules for a user and entity.
// userRef identifies the user and is opaque to the caller: it may be an int ID,
// a string/UUID, or the full *security.UserContext (see SecurityContext.GetUserRef),
// so providers backed by non-integer user identifiers (e.g. UUIDs) or that need
// access to JWT claims can implement row security without relying on a numeric ID.
GetRowSecurity(ctx context.Context, userRef any, schema, table string) (RowSecurity, error)
}
// SecurityProvider is the main interface combining all security concerns
+1 -4
View File
@@ -260,10 +260,7 @@ func (p *DatabasePasskeyProvider) BeginAuthentication(ctx context.Context, usern
// If username is provided, get user's credentials
var allowCredentials []PasskeyCredentialDescriptor
if username != "" {
var creds []struct {
ID string `json:"credential_id"`
Transports []string `json:"transports"`
}
var creds []passkeyCredential
if !p.capability.ShouldUseProcedure(ctx, p.queryMode, p.getDB(), p.sqlNames.PasskeyGetCredsByUsername) {
_, directCreds, err := p.getCredsByUsernameDirect(ctx, username)
+13 -18
View File
@@ -39,7 +39,7 @@ func (p *DatabasePasskeyProvider) storeCredentialDirect(ctx context.Context, par
var exists int
checkQuery := rewritePlaceholders(db, fmt.Sprintf(`SELECT 1 FROM %s WHERE credential_id = ?`, p.tableNames.UserPasskeyCredentials))
if err := db.QueryRowContext(ctx, checkQuery, params.CredentialID).Scan(&exists); err == nil {
return fmt.Errorf("Credential already exists")
return fmt.Errorf("credential already exists")
} else if !errors.Is(err, sql.ErrNoRows) {
return err
}
@@ -48,7 +48,7 @@ func (p *DatabasePasskeyProvider) storeCredentialDirect(ctx context.Context, par
userCheckQuery := rewritePlaceholders(db, fmt.Sprintf(`SELECT 1 FROM %s WHERE id = ?`, p.tableNames.Users))
if err := db.QueryRowContext(ctx, userCheckQuery, params.UserID).Scan(&userExists); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("User not found")
return fmt.Errorf("user not found")
}
return err
}
@@ -78,7 +78,7 @@ func (p *DatabasePasskeyProvider) getCredentialDirect(ctx context.Context, crede
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, 0, fmt.Errorf("Credential not found")
return 0, 0, fmt.Errorf("credential not found")
}
return 0, 0, fmt.Errorf("failed to get credential: %w", err)
}
@@ -91,7 +91,7 @@ func (p *DatabasePasskeyProvider) updateCounterDirect(ctx context.Context, crede
query := rewritePlaceholders(db, fmt.Sprintf(`SELECT sign_count FROM %s WHERE credential_id = ?`, p.tableNames.UserPasskeyCredentials))
if err := db.QueryRowContext(ctx, query, credentialIDB64).Scan(&oldCounter); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("Credential not found")
return fmt.Errorf("credential not found")
}
return err
}
@@ -188,7 +188,7 @@ func (p *DatabasePasskeyProvider) deleteCredentialDirect(ctx context.Context, us
return err
}
if rows == 0 {
return fmt.Errorf("Credential not found")
return fmt.Errorf("credential not found")
}
return nil
})
@@ -206,24 +206,19 @@ func (p *DatabasePasskeyProvider) updateNameDirect(ctx context.Context, userID i
return err
}
if rows == 0 {
return fmt.Errorf("Credential not found")
return fmt.Errorf("credential not found")
}
return nil
})
}
func (p *DatabasePasskeyProvider) getCredsByUsernameDirect(ctx context.Context, username string) (int, []struct {
type passkeyCredential struct {
ID string `json:"credential_id"`
Transports []string `json:"transports"`
}, error) {
type credT = struct {
ID string `json:"credential_id"`
Transports []string `json:"transports"`
}
var userID int
var creds []credT
}
err := p.runDBOpWithReconnect(func(db *sql.DB) error {
func (p *DatabasePasskeyProvider) getCredsByUsernameDirect(ctx context.Context, username string) (userID int, creds []passkeyCredential, err error) {
err = p.runDBOpWithReconnect(func(db *sql.DB) error {
userQuery := rewritePlaceholders(db, fmt.Sprintf(`SELECT id FROM %s WHERE username = ? AND is_active = ?`, p.tableNames.Users))
if err := db.QueryRowContext(ctx, userQuery, username, true).Scan(&userID); err != nil {
return err
@@ -236,7 +231,7 @@ func (p *DatabasePasskeyProvider) getCredsByUsernameDirect(ctx context.Context,
}
defer rows.Close()
creds = make([]credT, 0)
creds = make([]passkeyCredential, 0)
for rows.Next() {
var credID string
var transportsJSON sql.NullString
@@ -247,13 +242,13 @@ func (p *DatabasePasskeyProvider) getCredsByUsernameDirect(ctx context.Context,
if transportsJSON.Valid && transportsJSON.String != "" {
_ = json.Unmarshal([]byte(transportsJSON.String), &transports)
}
creds = append(creds, credT{ID: credID, Transports: transports})
creds = append(creds, passkeyCredential{ID: credID, Transports: transports})
}
return rows.Err()
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return 0, nil, fmt.Errorf("User not found")
return 0, nil, fmt.Errorf("user not found")
}
return 0, nil, fmt.Errorf("failed to get credentials: %w", err)
}
+10 -7
View File
@@ -34,7 +34,10 @@ type RowSecurity struct {
Tablename string `json:"tablename"`
Template string `json:"template"`
HasBlock bool `json:"has_block"`
UserID int `json:"user_id"`
// UserID is the opaque user reference the security rules were loaded for.
// It may be an int, a string/UUID, or a *UserContext, depending on what the
// RowSecurityProvider/SecurityContext.GetUserRef implementation returns.
UserID any `json:"user_id"`
}
func (m *RowSecurity) GetTemplate(pPrimaryKeyName string, pModelType reflect.Type) string {
@@ -42,7 +45,7 @@ func (m *RowSecurity) GetTemplate(pPrimaryKeyName string, pModelType reflect.Typ
str = strings.ReplaceAll(str, "{PrimaryKeyName}", pPrimaryKeyName)
str = strings.ReplaceAll(str, "{TableName}", m.Tablename)
str = strings.ReplaceAll(str, "{SchemaName}", m.Schema)
str = strings.ReplaceAll(str, "{UserID}", fmt.Sprintf("%d", m.UserID))
str = strings.ReplaceAll(str, "{UserID}", fmt.Sprintf("%v", m.UserID))
return str
}
@@ -413,7 +416,7 @@ func (m *SecurityList) ClearSecurity(pUserID int, pSchema, pTablename string) er
return nil
}
func (m *SecurityList) LoadRowSecurity(ctx context.Context, pUserID int, pSchema, pTablename string, pOverwrite bool) (RowSecurity, error) {
func (m *SecurityList) LoadRowSecurity(ctx context.Context, pUserRef any, pSchema, pTablename string, pOverwrite bool) (RowSecurity, error) {
if m.provider == nil {
return RowSecurity{}, fmt.Errorf("security provider not set")
}
@@ -424,10 +427,10 @@ func (m *SecurityList) LoadRowSecurity(ctx context.Context, pUserID int, pSchema
if m.RowSecurity == nil {
m.RowSecurity = make(map[string]RowSecurity, 0)
}
secKey := fmt.Sprintf("%s.%s@%d", pSchema, pTablename, pUserID)
secKey := fmt.Sprintf("%s.%s@%v", pSchema, pTablename, pUserRef)
// Call the provider to load security rules
record, err := m.provider.GetRowSecurity(ctx, pUserID, pSchema, pTablename)
record, err := m.provider.GetRowSecurity(ctx, pUserRef, pSchema, pTablename)
if err != nil {
return RowSecurity{}, fmt.Errorf("GetRowSecurity failed: %v", err)
}
@@ -436,7 +439,7 @@ func (m *SecurityList) LoadRowSecurity(ctx context.Context, pUserID int, pSchema
return record, nil
}
func (m *SecurityList) GetRowSecurityTemplate(pUserID int, pSchema, pTablename string) (RowSecurity, error) {
func (m *SecurityList) GetRowSecurityTemplate(pUserRef any, pSchema, pTablename string) (RowSecurity, error) {
defer logger.CatchPanic("GetRowSecurityTemplate")()
if m.RowSecurity == nil {
@@ -446,7 +449,7 @@ func (m *SecurityList) GetRowSecurityTemplate(pUserID int, pSchema, pTablename s
m.RowSecurityMutex.RLock()
defer m.RowSecurityMutex.RUnlock()
rowSec, ok := m.RowSecurity[fmt.Sprintf("%s.%s@%d", pSchema, pTablename, pUserID)]
rowSec, ok := m.RowSecurity[fmt.Sprintf("%s.%s@%v", pSchema, pTablename, pUserRef)]
if !ok {
return RowSecurity{}, fmt.Errorf("no row security data")
}
+1 -1
View File
@@ -44,7 +44,7 @@ func (m *mockSecurityProvider) GetColumnSecurity(ctx context.Context, userID int
return m.columnSecurity, nil
}
func (m *mockSecurityProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error) {
func (m *mockSecurityProvider) GetRowSecurity(ctx context.Context, userRef any, schema, table string) (RowSecurity, error) {
return m.rowSecurity, nil
}
+6 -6
View File
@@ -907,7 +907,7 @@ func (p *DatabaseRowSecurityProvider) reconnectDB() error {
return nil
}
func (p *DatabaseRowSecurityProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error) {
func (p *DatabaseRowSecurityProvider) GetRowSecurity(ctx context.Context, userRef any, schema, table string) (RowSecurity, error) {
if !p.capability.ShouldUseProcedure(ctx, p.queryMode, p.getDB(), p.sqlNames.RowSecurity) {
return RowSecurity{}, ErrDirectModeUnsupported
}
@@ -917,7 +917,7 @@ func (p *DatabaseRowSecurityProvider) GetRowSecurity(ctx context.Context, userID
runQuery := func() error {
query := fmt.Sprintf(`SELECT p_template, p_block FROM %s($1, $2, $3)`, p.sqlNames.RowSecurity)
return p.getDB().QueryRowContext(ctx, query, schema, table, userID).Scan(&template, &hasBlock)
return p.getDB().QueryRowContext(ctx, query, schema, table, userRef).Scan(&template, &hasBlock)
}
err := runQuery()
if isDBClosed(err) {
@@ -932,7 +932,7 @@ func (p *DatabaseRowSecurityProvider) GetRowSecurity(ctx context.Context, userID
return RowSecurity{
Schema: schema,
Tablename: table,
UserID: userID,
UserID: userRef,
Template: template,
HasBlock: hasBlock,
}, nil
@@ -969,14 +969,14 @@ func NewConfigRowSecurityProvider(templates map[string]string, blocked map[strin
}
}
func (p *ConfigRowSecurityProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error) {
func (p *ConfigRowSecurityProvider) GetRowSecurity(ctx context.Context, userRef any, schema, table string) (RowSecurity, error) {
key := fmt.Sprintf("%s.%s", schema, table)
if p.blocked[key] {
return RowSecurity{
Schema: schema,
Tablename: table,
UserID: userID,
UserID: userRef,
HasBlock: true,
}, nil
}
@@ -985,7 +985,7 @@ func (p *ConfigRowSecurityProvider) GetRowSecurity(ctx context.Context, userID i
return RowSecurity{
Schema: schema,
Tablename: table,
UserID: userID,
UserID: userRef,
Template: template,
HasBlock: false,
}, nil
+9 -9
View File
@@ -23,8 +23,8 @@ import (
// than introducing a mismatch between modes.
var (
errUsernameExists = errors.New("Username already exists")
errEmailExists = errors.New("Email already exists")
errUsernameExists = errors.New("username already exists")
errEmailExists = errors.New("email already exists")
)
func (a *DatabaseAuthenticator) loginDirect(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
@@ -40,7 +40,7 @@ func (a *DatabaseAuthenticator) loginDirect(ctx context.Context, req LoginReques
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("Invalid credentials")
return nil, fmt.Errorf("invalid credentials")
}
return nil, fmt.Errorf("login query failed: %w", err)
}
@@ -88,13 +88,13 @@ func (a *DatabaseAuthenticator) loginDirect(ctx context.Context, req LoginReques
func (a *DatabaseAuthenticator) registerDirect(ctx context.Context, req RegisterRequest) (*LoginResponse, error) {
if req.Username == "" {
return nil, fmt.Errorf("Username is required")
return nil, fmt.Errorf("username is required")
}
if req.Email == "" {
return nil, fmt.Errorf("Email is required")
return nil, fmt.Errorf("email is required")
}
if req.Password == "" {
return nil, fmt.Errorf("Password is required")
return nil, fmt.Errorf("password is required")
}
rolesStr := strings.Join(req.Roles, ",")
@@ -197,7 +197,7 @@ func (a *DatabaseAuthenticator) logoutDirect(ctx context.Context, req LogoutRequ
return fmt.Errorf("logout query failed: %w", err)
}
if rows == 0 {
return fmt.Errorf("Session not found")
return fmt.Errorf("session not found")
}
if req.Token != "" {
@@ -222,7 +222,7 @@ func (a *DatabaseAuthenticator) sessionDirect(ctx context.Context, token string)
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("Invalid or expired session")
return nil, fmt.Errorf("invalid or expired session")
}
return nil, fmt.Errorf("session query failed: %w", err)
}
@@ -262,7 +262,7 @@ func (a *DatabaseAuthenticator) refreshTokenDirect(ctx context.Context, oldToken
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("Invalid or expired refresh token")
return nil, fmt.Errorf("invalid or expired refresh token")
}
return nil, fmt.Errorf("refresh token query failed: %w", err)
}
@@ -22,7 +22,7 @@ func (p *DatabaseTwoFactorProvider) enable2FADirect(ctx context.Context, userID
if rows, err := res.RowsAffected(); err != nil {
return err
} else if rows == 0 {
return fmt.Errorf("User not found")
return fmt.Errorf("user not found")
}
delQuery := rewritePlaceholders(db, fmt.Sprintf(`DELETE FROM %s WHERE user_id = ?`, p.tableNames.UserTOTPBackupCodes))
@@ -50,7 +50,7 @@ func (p *DatabaseTwoFactorProvider) disable2FADirect(ctx context.Context, userID
if rows, err := res.RowsAffected(); err != nil {
return err
} else if rows == 0 {
return fmt.Errorf("User not found")
return fmt.Errorf("user not found")
}
delQuery := rewritePlaceholders(db, fmt.Sprintf(`DELETE FROM %s WHERE user_id = ?`, p.tableNames.UserTOTPBackupCodes))
@@ -67,7 +67,7 @@ func (p *DatabaseTwoFactorProvider) get2FAStatusDirect(ctx context.Context, user
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, fmt.Errorf("User not found")
return false, fmt.Errorf("user not found")
}
return false, fmt.Errorf("get 2FA status query failed: %w", err)
}
@@ -83,7 +83,7 @@ func (p *DatabaseTwoFactorProvider) get2FASecretDirect(ctx context.Context, user
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", fmt.Errorf("User not found")
return "", fmt.Errorf("user not found")
}
return "", fmt.Errorf("get 2FA secret query failed: %w", err)
}
@@ -101,7 +101,7 @@ func (p *DatabaseTwoFactorProvider) regenerateBackupCodesDirect(ctx context.Cont
return err
}
if count == 0 {
return fmt.Errorf("User not found or TOTP not enabled")
return fmt.Errorf("user not found or TOTP not enabled")
}
delQuery := rewritePlaceholders(db, fmt.Sprintf(`DELETE FROM %s WHERE user_id = ?`, p.tableNames.UserTOTPBackupCodes))
@@ -134,7 +134,7 @@ func (p *DatabaseTwoFactorProvider) validateBackupCodeDirect(ctx context.Context
return err
}
if used {
return fmt.Errorf("Backup code already used")
return fmt.Errorf("backup code already used")
}
updQuery := rewritePlaceholders(db, fmt.Sprintf(`UPDATE %s SET used = ?, used_at = ? WHERE id = ?`, p.tableNames.UserTOTPBackupCodes))
+11
View File
@@ -71,6 +71,17 @@ func (s *securityContext) GetUserID() (int, bool) {
return security.GetUserID(s.ctx.Context)
}
// GetUserRef returns an opaque user identifier for row security lookups.
// It prefers the full *security.UserContext (so providers can read JWT claims,
// e.g. a UUID subject) and falls back to the int user ID.
func (s *securityContext) GetUserRef() (any, bool) {
if userCtx, ok := security.GetUserContext(s.ctx.Context); ok {
return userCtx, true
}
userID, ok := security.GetUserID(s.ctx.Context)
return userID, ok
}
func (s *securityContext) GetSchema() string {
return s.ctx.Schema
}