diff --git a/pkg/funcspec/security_adapter.go b/pkg/funcspec/security_adapter.go index fd5233a..f0201cc 100644 --- a/pkg/funcspec/security_adapter.go +++ b/pkg/funcspec/security_adapter.go @@ -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" diff --git a/pkg/mqttspec/security_hooks.go b/pkg/mqttspec/security_hooks.go index 9f9b799..a7731d2 100644 --- a/pkg/mqttspec/security_hooks.go +++ b/pkg/mqttspec/security_hooks.go @@ -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 } diff --git a/pkg/resolvemcp/security_hooks.go b/pkg/resolvemcp/security_hooks.go index 843e6b5..cb8f2ab 100644 --- a/pkg/resolvemcp/security_hooks.go +++ b/pkg/resolvemcp/security_hooks.go @@ -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 } diff --git a/pkg/resolvespec/security_hooks.go b/pkg/resolvespec/security_hooks.go index 47b97c5..3040018 100644 --- a/pkg/resolvespec/security_hooks.go +++ b/pkg/resolvespec/security_hooks.go @@ -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 } diff --git a/pkg/restheadspec/handler.go b/pkg/restheadspec/handler.go index dcc0170..9926ef9 100644 --- a/pkg/restheadspec/handler.go +++ b/pkg/restheadspec/handler.go @@ -1494,6 +1494,20 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id // Fetch the updated record after the transaction commits to capture any trigger changes fetchedRecord := reflect.New(reflect.TypeOf(model)).Interface() selectQuery := h.db.NewSelect().Model(fetchedRecord).Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID) + + // Execute BeforeScan hooks so row security is re-applied to the post-update + // re-fetch, same as it is for the initial read and the update query itself. + // Without this, the re-fetch can return a row the caller isn't authorized to see. + hookCtx.Query = selectQuery + if err := h.hooks.Execute(BeforeScan, hookCtx); err != nil { + logger.Error("BeforeScan hook failed: %v", err) + h.sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err) + return + } + if modifiedQuery, ok := hookCtx.Query.(common.SelectQuery); ok { + selectQuery = modifiedQuery + } + if err := selectQuery.ScanModel(ctx); err != nil { logger.Error("Failed to fetch updated record: %v", err) h.sendError(w, http.StatusInternalServerError, "fetch_error", "Failed to fetch updated record", err) diff --git a/pkg/restheadspec/security_hooks.go b/pkg/restheadspec/security_hooks.go index 05e2bf5..07bc555 100644 --- a/pkg/restheadspec/security_hooks.go +++ b/pkg/restheadspec/security_hooks.go @@ -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 } diff --git a/pkg/security/composite.go b/pkg/security/composite.go index 088fe0c..fa5150e 100644 --- a/pkg/security/composite.go +++ b/pkg/security/composite.go @@ -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) diff --git a/pkg/security/composite_test.go b/pkg/security/composite_test.go index a4d376f..207cf97 100644 --- a/pkg/security/composite_test.go +++ b/pkg/security/composite_test.go @@ -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 } diff --git a/pkg/security/hooks.go b/pkg/security/hooks.go index 52138d8..7628914 100644 --- a/pkg/security/hooks.go +++ b/pkg/security/hooks.go @@ -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() diff --git a/pkg/security/hooks_test.go b/pkg/security/hooks_test.go index b4787a5..aa80f61 100644 --- a/pkg/security/hooks_test.go +++ b/pkg/security/hooks_test.go @@ -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 } diff --git a/pkg/security/interfaces.go b/pkg/security/interfaces.go index 1fc701a..1e2c18a 100644 --- a/pkg/security/interfaces.go +++ b/pkg/security/interfaces.go @@ -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 diff --git a/pkg/security/passkey_provider.go b/pkg/security/passkey_provider.go index 8b9bf05..003e968 100644 --- a/pkg/security/passkey_provider.go +++ b/pkg/security/passkey_provider.go @@ -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) diff --git a/pkg/security/passkey_provider_direct.go b/pkg/security/passkey_provider_direct.go index f2b1dd0..229121f 100644 --- a/pkg/security/passkey_provider_direct.go +++ b/pkg/security/passkey_provider_direct.go @@ -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) } diff --git a/pkg/security/provider.go b/pkg/security/provider.go index 7dcf609..8f638f5 100644 --- a/pkg/security/provider.go +++ b/pkg/security/provider.go @@ -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") } diff --git a/pkg/security/provider_test.go b/pkg/security/provider_test.go index d3d535a..79ef6f3 100644 --- a/pkg/security/provider_test.go +++ b/pkg/security/provider_test.go @@ -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 } diff --git a/pkg/security/providers.go b/pkg/security/providers.go index c91bf28..ca6d20f 100644 --- a/pkg/security/providers.go +++ b/pkg/security/providers.go @@ -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 diff --git a/pkg/security/providers_direct.go b/pkg/security/providers_direct.go index c884fb7..a148a1e 100644 --- a/pkg/security/providers_direct.go +++ b/pkg/security/providers_direct.go @@ -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) } diff --git a/pkg/security/totp_provider_database_direct.go b/pkg/security/totp_provider_database_direct.go index 9055cd4..500c4c2 100644 --- a/pkg/security/totp_provider_database_direct.go +++ b/pkg/security/totp_provider_database_direct.go @@ -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)) diff --git a/pkg/websocketspec/security_hooks.go b/pkg/websocketspec/security_hooks.go index e0606c8..99b0f76 100644 --- a/pkg/websocketspec/security_hooks.go +++ b/pkg/websocketspec/security_hooks.go @@ -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 }