Merge branch 'main' of https://github.com/bitechdev/ResolveSpec
Tests / Integration Tests (push) Failing after 16s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 51s
Tests / Unit Tests (push) Failing after 11s
Build , Vet Test, and Lint / Build (push) Successful in 1m23s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 1m35s
Build , Vet Test, and Lint / Lint Code (push) Successful in 1m35s

This commit is contained in:
Hein
2026-07-28 17:48:14 +02:00
12 changed files with 191 additions and 43 deletions
+4 -4
View File
@@ -197,7 +197,7 @@ func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFun
hookCtx.Tx = tx hookCtx.Tx = tx
// Execute BeforeQueryList hook (inside transaction) // Execute BeforeQueryList hook (inside transaction)
if err := h.hooks.Execute(BeforeQueryList, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeQueryList, hookCtx); err != nil {
logger.Error("BeforeQueryList hook failed: %v", err) logger.Error("BeforeQueryList hook failed: %v", err)
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err) sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
return err return err
@@ -261,7 +261,7 @@ func (h *Handler) SqlQueryList(sqlquery string, options SqlQueryOptions) HTTPFun
// Execute BeforeSQLExec hook // Execute BeforeSQLExec hook
hookCtx.SQLQuery = sqlquery hookCtx.SQLQuery = sqlquery
if err := h.hooks.Execute(BeforeSQLExec, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeSQLExec, hookCtx); err != nil {
logger.Error("BeforeSQLExec hook failed: %v", err) logger.Error("BeforeSQLExec hook failed: %v", err)
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err) sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
return err return err
@@ -563,7 +563,7 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
hookCtx.Tx = tx hookCtx.Tx = tx
// Execute BeforeQuery hook (inside transaction) // Execute BeforeQuery hook (inside transaction)
if err := h.hooks.Execute(BeforeQuery, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeQuery, hookCtx); err != nil {
logger.Error("BeforeQuery hook failed: %v", err) logger.Error("BeforeQuery hook failed: %v", err)
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err) sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
return err return err
@@ -582,7 +582,7 @@ func (h *Handler) SqlQuery(sqlquery string, options SqlQueryOptions) HTTPFuncTyp
sqlquery = hookCtx.SQLQuery sqlquery = hookCtx.SQLQuery
// Execute BeforeSQLExec hook // Execute BeforeSQLExec hook
if err := h.hooks.Execute(BeforeSQLExec, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeSQLExec, hookCtx); err != nil {
logger.Error("BeforeSQLExec hook failed: %v", err) logger.Error("BeforeSQLExec hook failed: %v", err)
sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err) sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
return err return err
+14
View File
@@ -28,6 +28,10 @@ const (
// Response hooks (before response is sent) // Response hooks (before response is sent)
BeforeResponse HookType = "before_response" BeforeResponse HookType = "before_response"
// BeforeOp fires immediately before every SQL operation (query, query list, SQL exec).
// It fires at each individual SQL-operation hook point, so it runs once per statement executed.
BeforeOp HookType = "before_op"
) )
// HookContext contains all the data available to a hook // HookContext contains all the data available to a hook
@@ -130,6 +134,16 @@ func (r *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
return nil return nil
} }
// ExecuteBeforeOp executes the BeforeOp hook followed by the given SQL-operation hook
// (BeforeQuery, BeforeQueryList, or BeforeSQLExec). BeforeOp always runs first so it can
// observe/veto every SQL operation regardless of type.
func (r *HookRegistry) ExecuteBeforeOp(hookType HookType, ctx *HookContext) error {
if err := r.Execute(BeforeOp, ctx); err != nil {
return err
}
return r.Execute(hookType, ctx)
}
// Clear removes all hooks for the specified type // Clear removes all hooks for the specified type
func (r *HookRegistry) Clear(hookType HookType) { func (r *HookRegistry) Clear(hookType HookType) {
delete(r.hooks, hookType) delete(r.hooks, hookType)
+28 -4
View File
@@ -313,7 +313,7 @@ func (h *Handler) handleRequest(client *Client, msg *Message) {
// handleRead processes a read operation // handleRead processes a read operation
func (h *Handler) handleRead(client *Client, msg *Message, hookCtx *HookContext) { func (h *Handler) handleRead(client *Client, msg *Message, hookCtx *HookContext) {
// Execute before hook // Execute before hook
if err := h.hooks.Execute(BeforeRead, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeRead, hookCtx); err != nil {
logger.Error("[MQTTSpec] BeforeRead hook failed: %v", err) logger.Error("[MQTTSpec] BeforeRead hook failed: %v", err)
h.sendError(client.ID, msg.ID, "hook_error", err.Error()) h.sendError(client.ID, msg.ID, "hook_error", err.Error())
return return
@@ -356,7 +356,7 @@ func (h *Handler) handleRead(client *Client, msg *Message, hookCtx *HookContext)
// handleCreate processes a create operation // handleCreate processes a create operation
func (h *Handler) handleCreate(client *Client, msg *Message, hookCtx *HookContext) { func (h *Handler) handleCreate(client *Client, msg *Message, hookCtx *HookContext) {
// Execute before hook // Execute before hook
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
logger.Error("[MQTTSpec] BeforeCreate hook failed: %v", err) logger.Error("[MQTTSpec] BeforeCreate hook failed: %v", err)
h.sendError(client.ID, msg.ID, "hook_error", err.Error()) h.sendError(client.ID, msg.ID, "hook_error", err.Error())
return return
@@ -390,7 +390,7 @@ func (h *Handler) handleCreate(client *Client, msg *Message, hookCtx *HookContex
// handleUpdate processes an update operation // handleUpdate processes an update operation
func (h *Handler) handleUpdate(client *Client, msg *Message, hookCtx *HookContext) { func (h *Handler) handleUpdate(client *Client, msg *Message, hookCtx *HookContext) {
// Execute before hook // Execute before hook
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeUpdate, hookCtx); err != nil {
logger.Error("[MQTTSpec] BeforeUpdate hook failed: %v", err) logger.Error("[MQTTSpec] BeforeUpdate hook failed: %v", err)
h.sendError(client.ID, msg.ID, "hook_error", err.Error()) h.sendError(client.ID, msg.ID, "hook_error", err.Error())
return return
@@ -424,7 +424,7 @@ func (h *Handler) handleUpdate(client *Client, msg *Message, hookCtx *HookContex
// handleDelete processes a delete operation // handleDelete processes a delete operation
func (h *Handler) handleDelete(client *Client, msg *Message, hookCtx *HookContext) { func (h *Handler) handleDelete(client *Client, msg *Message, hookCtx *HookContext) {
// Execute before hook // Execute before hook
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
logger.Error("[MQTTSpec] BeforeDelete hook failed: %v", err) logger.Error("[MQTTSpec] BeforeDelete hook failed: %v", err)
h.sendError(client.ID, msg.ID, "hook_error", err.Error()) h.sendError(client.ID, msg.ID, "hook_error", err.Error())
return return
@@ -686,6 +686,18 @@ func (h *Handler) readByID(hookCtx *HookContext) (interface{}, error) {
} }
} }
// Execute BeforeScan hooks - pass query so hooks (e.g. row-level security) can modify it
if hookCtx.Metadata == nil {
hookCtx.Metadata = make(map[string]interface{})
}
hookCtx.Metadata["query"] = query
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
return nil, fmt.Errorf("BeforeScan hook failed: %w", err)
}
if modifiedQuery, ok := hookCtx.Metadata["query"].(common.SelectQuery); ok {
query = modifiedQuery
}
// Execute query // Execute query
if err := query.ScanModel(hookCtx.Context); err != nil { if err := query.ScanModel(hookCtx.Context); err != nil {
return nil, fmt.Errorf("failed to read record: %w", err) return nil, fmt.Errorf("failed to read record: %w", err)
@@ -738,6 +750,18 @@ func (h *Handler) readMultiple(hookCtx *HookContext) (data interface{}, metadata
} }
} }
// Execute BeforeScan hooks - pass query so hooks (e.g. row-level security) can modify it
if hookCtx.Metadata == nil {
hookCtx.Metadata = make(map[string]interface{})
}
hookCtx.Metadata["query"] = query
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
return nil, nil, fmt.Errorf("BeforeScan hook failed: %w", err)
}
if modifiedQuery, ok := hookCtx.Metadata["query"].(common.SelectQuery); ok {
query = modifiedQuery
}
// Execute query // Execute query
if err := query.ScanModel(hookCtx.Context); err != nil { if err := query.ScanModel(hookCtx.Context); err != nil {
return nil, nil, fmt.Errorf("failed to read records: %w", err) return nil, nil, fmt.Errorf("failed to read records: %w", err)
+4
View File
@@ -34,6 +34,7 @@ const (
AfterUpdate = websocketspec.AfterUpdate AfterUpdate = websocketspec.AfterUpdate
BeforeDelete = websocketspec.BeforeDelete BeforeDelete = websocketspec.BeforeDelete
AfterDelete = websocketspec.AfterDelete AfterDelete = websocketspec.AfterDelete
BeforeScan = websocketspec.BeforeScan
// Subscription hooks // Subscription hooks
BeforeSubscribe = websocketspec.BeforeSubscribe BeforeSubscribe = websocketspec.BeforeSubscribe
@@ -46,6 +47,9 @@ const (
AfterConnect = websocketspec.AfterConnect AfterConnect = websocketspec.AfterConnect
BeforeDisconnect = websocketspec.BeforeDisconnect BeforeDisconnect = websocketspec.BeforeDisconnect
AfterDisconnect = websocketspec.AfterDisconnect AfterDisconnect = websocketspec.AfterDisconnect
// BeforeOp fires immediately before every SQL operation (read, create, update, delete)
BeforeOp = websocketspec.BeforeOp
) )
// NewHookRegistry creates a new hook registry // NewHookRegistry creates a new hook registry
+10 -4
View File
@@ -27,25 +27,31 @@ func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList
return security.LoadSecurityRules(secCtx, securityList) return security.LoadSecurityRules(secCtx, securityList)
}) })
// Hook 2: AfterRead - Apply column-level security (masking) // Hook 2: BeforeScan - Apply row-level security filters
handler.Hooks().Register(BeforeScan, func(hookCtx *HookContext) error {
secCtx := newSecurityContext(hookCtx)
return security.ApplyRowSecurity(secCtx, securityList)
})
// Hook 3: AfterRead - Apply column-level security (masking)
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error { handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
secCtx := newSecurityContext(hookCtx) secCtx := newSecurityContext(hookCtx)
return security.ApplyColumnSecurity(secCtx, securityList) return security.ApplyColumnSecurity(secCtx, securityList)
}) })
// Hook 3 (Optional): Audit logging // Hook 4 (Optional): Audit logging
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error { handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
secCtx := newSecurityContext(hookCtx) secCtx := newSecurityContext(hookCtx)
return security.LogDataAccess(secCtx) return security.LogDataAccess(secCtx)
}) })
// Hook 4: BeforeUpdate - enforce CanUpdate rule from context/registry // Hook 5: BeforeUpdate - enforce CanUpdate rule from context/registry
handler.Hooks().Register(BeforeUpdate, func(hookCtx *HookContext) error { handler.Hooks().Register(BeforeUpdate, func(hookCtx *HookContext) error {
secCtx := newSecurityContext(hookCtx) secCtx := newSecurityContext(hookCtx)
return security.CheckModelUpdateAllowed(secCtx) return security.CheckModelUpdateAllowed(secCtx)
}) })
// Hook 5: BeforeDelete - enforce CanDelete rule from context/registry // Hook 6: BeforeDelete - enforce CanDelete rule from context/registry
handler.Hooks().Register(BeforeDelete, func(hookCtx *HookContext) error { handler.Hooks().Register(BeforeDelete, func(hookCtx *HookContext) error {
secCtx := newSecurityContext(hookCtx) secCtx := newSecurityContext(hookCtx)
return security.CheckModelDeleteAllowed(secCtx) return security.CheckModelDeleteAllowed(secCtx)
+31 -11
View File
@@ -288,7 +288,7 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
Writer: w, Writer: w,
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeRead, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeRead, hookCtx); err != nil {
statusCode, errCode, errMsg = http.StatusInternalServerError, "hook_error", "BeforeRead hook failed" statusCode, errCode, errMsg = http.StatusInternalServerError, "hook_error", "BeforeRead hook failed"
return err return err
} }
@@ -538,6 +538,16 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
pkName := reflection.GetPrimaryKeyName(singleResult) pkName := reflection.GetPrimaryKeyName(singleResult)
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID) query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
// Execute BeforeScan hooks - pass query chain so hooks (e.g. row-level security) can modify it
hookCtx.Query = query
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
logger.Error("BeforeScan hook failed: %v", err)
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
return err
}
query = hookCtx.Query
if err := query.Scan(ctx, singleResult); err != nil { if err := query.Scan(ctx, singleResult); err != nil {
logger.Error("Error querying record: %v", err) logger.Error("Error querying record: %v", err)
statusCode, errCode, errMsg = http.StatusInternalServerError, "query_error", "Error executing query" statusCode, errCode, errMsg = http.StatusInternalServerError, "query_error", "Error executing query"
@@ -546,6 +556,16 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
result = singleResult result = singleResult
} else { } else {
logger.Debug("Querying multiple records") logger.Debug("Querying multiple records")
// Execute BeforeScan hooks - pass query chain so hooks (e.g. row-level security) can modify it
hookCtx.Query = query
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
logger.Error("BeforeScan hook failed: %v", err)
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
return err
}
query = hookCtx.Query
// Use the modelPtr already created and set on the query // Use the modelPtr already created and set on the query
if err := query.Scan(ctx, modelPtr); err != nil { if err := query.Scan(ctx, modelPtr); err != nil {
logger.Error("Error querying records: %v", err) logger.Error("Error querying records: %v", err)
@@ -635,7 +655,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
Writer: w, Writer: w,
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
return fmt.Errorf("BeforeCreate hook failed: %w", err) return fmt.Errorf("BeforeCreate hook failed: %w", err)
} }
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok { if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
@@ -682,7 +702,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
Writer: w, Writer: w,
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
return fmt.Errorf("BeforeCreate hook failed: %w", err) return fmt.Errorf("BeforeCreate hook failed: %w", err)
} }
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok { if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
@@ -764,7 +784,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
Writer: w, Writer: w,
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
return fmt.Errorf("BeforeCreate hook failed: %w", err) return fmt.Errorf("BeforeCreate hook failed: %w", err)
} }
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok { if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
@@ -811,7 +831,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
Writer: w, Writer: w,
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
return fmt.Errorf("BeforeCreate hook failed: %w", err) return fmt.Errorf("BeforeCreate hook failed: %w", err)
} }
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok { if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
@@ -895,7 +915,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
Writer: w, Writer: w,
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
return fmt.Errorf("BeforeCreate hook failed: %w", err) return fmt.Errorf("BeforeCreate hook failed: %w", err)
} }
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok { if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
@@ -948,7 +968,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
Writer: w, Writer: w,
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
return fmt.Errorf("BeforeCreate hook failed: %w", err) return fmt.Errorf("BeforeCreate hook failed: %w", err)
} }
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok { if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
@@ -1081,7 +1101,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeUpdate, hookCtx); err != nil {
return fmt.Errorf("BeforeUpdate hook failed: %w", err) return fmt.Errorf("BeforeUpdate hook failed: %w", err)
} }
@@ -1302,7 +1322,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeUpdate, hookCtx); err != nil {
return fmt.Errorf("BeforeUpdate hook failed for ID %v: %w", itemID, err) return fmt.Errorf("BeforeUpdate hook failed for ID %v: %w", itemID, err)
} }
@@ -1458,7 +1478,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, url
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeUpdate, hookCtx); err != nil {
return fmt.Errorf("BeforeUpdate hook failed for ID %v: %w", itemID, err) return fmt.Errorf("BeforeUpdate hook failed for ID %v: %w", itemID, err)
} }
@@ -1561,7 +1581,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
Writer: w, Writer: w,
Tx: h.db, Tx: h.db,
} }
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
logger.Error("BeforeDelete hook failed: %v", err) logger.Error("BeforeDelete hook failed: %v", err)
h.sendError(w, http.StatusForbidden, "delete_forbidden", "Delete operation not allowed", err) h.sendError(w, http.StatusForbidden, "delete_forbidden", "Delete operation not allowed", err)
return return
+15
View File
@@ -34,6 +34,11 @@ const (
// Scan/Execute operation hooks (for query building) // Scan/Execute operation hooks (for query building)
BeforeScan HookType = "before_scan" BeforeScan HookType = "before_scan"
// BeforeOp fires immediately before every SQL operation (read, create, update, delete, scan).
// Unlike BeforeHandle, which fires once before operation dispatch, BeforeOp fires at each
// individual SQL-operation hook point, so it runs once per statement executed.
BeforeOp HookType = "before_op"
) )
// HookContext contains all the data available to a hook // HookContext contains all the data available to a hook
@@ -128,6 +133,16 @@ func (r *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
return nil return nil
} }
// ExecuteBeforeOp executes the BeforeOp hook followed by the given SQL-operation hook
// (BeforeRead, BeforeCreate, BeforeUpdate, BeforeDelete, or BeforeScan). BeforeOp always
// runs first so it can observe/veto every SQL operation regardless of type.
func (r *HookRegistry) ExecuteBeforeOp(hookType HookType, ctx *HookContext) error {
if err := r.Execute(BeforeOp, ctx); err != nil {
return err
}
return r.Execute(hookType, ctx)
}
// Clear removes all hooks for the specified type // Clear removes all hooks for the specified type
func (r *HookRegistry) Clear(hookType HookType) { func (r *HookRegistry) Clear(hookType HookType) {
delete(r.hooks, hookType) delete(r.hooks, hookType)
+12 -12
View File
@@ -372,7 +372,7 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
txErr := h.db.RunInTransaction(ctx, func(tx common.Database) error { txErr := h.db.RunInTransaction(ctx, func(tx common.Database) error {
hookCtx.Tx = tx hookCtx.Tx = tx
if err := h.hooks.Execute(BeforeRead, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeRead, hookCtx); err != nil {
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed" statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
return err return err
} }
@@ -792,7 +792,7 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
// Execute BeforeScan hooks - pass query chain so hooks can modify it // Execute BeforeScan hooks - pass query chain so hooks can modify it
hookCtx.Query = query hookCtx.Query = query
if err := h.hooks.Execute(BeforeScan, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
logger.Error("BeforeScan hook failed: %v", err) logger.Error("BeforeScan hook failed: %v", err)
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed" statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
return err return err
@@ -1184,7 +1184,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
results := make([]interface{}, 0) results := make([]interface{}, 0)
txErr := h.db.RunInTransaction(ctx, func(tx common.Database) error { txErr := h.db.RunInTransaction(ctx, func(tx common.Database) error {
hookCtx.Tx = tx hookCtx.Tx = tx
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed" statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
return err return err
} }
@@ -1269,7 +1269,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
Query: query, Query: query,
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeScan, itemHookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeScan, itemHookCtx); err != nil {
return fmt.Errorf("BeforeScan hook failed for item %d: %w", i, err) return fmt.Errorf("BeforeScan hook failed for item %d: %w", i, err)
} }
@@ -1423,7 +1423,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
Writer: w, Writer: w,
} }
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeUpdate, hookCtx); err != nil {
return fmt.Errorf("BeforeUpdate hook failed: %w", err) return fmt.Errorf("BeforeUpdate hook failed: %w", err)
} }
@@ -1500,7 +1500,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
// Execute BeforeScan hooks - pass query chain so hooks can modify it // Execute BeforeScan hooks - pass query chain so hooks can modify it
hookCtx.Query = query hookCtx.Query = query
hookCtx.Tx = tx hookCtx.Tx = tx
if err := h.hooks.Execute(BeforeScan, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
return fmt.Errorf("BeforeScan hook failed: %w", err) return fmt.Errorf("BeforeScan hook failed: %w", err)
} }
@@ -1544,7 +1544,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
// pooled connection rather than the now-dead tx. // pooled connection rather than the now-dead tx.
hookCtx.Tx = h.db hookCtx.Tx = h.db
hookCtx.Query = selectQuery hookCtx.Query = selectQuery
if err := h.hooks.Execute(BeforeScan, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
logger.Error("BeforeScan hook failed: %v", err) logger.Error("BeforeScan hook failed: %v", err)
h.sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err) h.sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
return return
@@ -1619,7 +1619,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
logger.Error("BeforeDelete hook failed for ID %s: %v", itemID, err) logger.Error("BeforeDelete hook failed for ID %s: %v", itemID, err)
return fmt.Errorf("delete not allowed for ID %s: %w", itemID, err) return fmt.Errorf("delete not allowed for ID %s: %w", itemID, err)
} }
@@ -1693,7 +1693,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
logger.Error("BeforeDelete hook failed for ID %v: %v", itemID, err) logger.Error("BeforeDelete hook failed for ID %v: %v", itemID, err)
return fmt.Errorf("delete not allowed for ID %v: %w", itemID, err) return fmt.Errorf("delete not allowed for ID %v: %w", itemID, err)
} }
@@ -1751,7 +1751,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
Tx: tx, Tx: tx,
} }
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
logger.Error("BeforeDelete hook failed for ID %v: %v", itemID, err) logger.Error("BeforeDelete hook failed for ID %v: %v", itemID, err)
return fmt.Errorf("delete not allowed for ID %v: %w", itemID, err) return fmt.Errorf("delete not allowed for ID %v: %w", itemID, err)
} }
@@ -1836,7 +1836,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
Data: recordToDelete, Data: recordToDelete,
} }
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
logger.Error("BeforeDelete hook failed: %v", err) logger.Error("BeforeDelete hook failed: %v", err)
h.sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err) h.sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
return return
@@ -1847,7 +1847,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
// Execute BeforeScan hooks - pass query chain so hooks can modify it // Execute BeforeScan hooks - pass query chain so hooks can modify it
hookCtx.Query = query hookCtx.Query = query
if err := h.hooks.Execute(BeforeScan, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
logger.Error("BeforeScan hook failed: %v", err) logger.Error("BeforeScan hook failed: %v", err)
h.sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err) h.sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
return return
+15
View File
@@ -34,6 +34,11 @@ const (
// Scan/Execute operation hooks // Scan/Execute operation hooks
BeforeScan HookType = "before_scan" BeforeScan HookType = "before_scan"
// BeforeOp fires immediately before every SQL operation (read, create, update, delete, scan).
// Unlike BeforeHandle, which fires once before operation dispatch, BeforeOp fires at each
// individual SQL-operation hook point, so it runs once per statement executed.
BeforeOp HookType = "before_op"
) )
// HookContext contains all the data available to a hook // HookContext contains all the data available to a hook
@@ -137,6 +142,16 @@ func (r *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
return nil return nil
} }
// ExecuteBeforeOp executes the BeforeOp hook followed by the given SQL-operation hook
// (BeforeRead, BeforeCreate, BeforeUpdate, BeforeDelete, or BeforeScan). BeforeOp always
// runs first so it can observe/veto every SQL operation regardless of type.
func (r *HookRegistry) ExecuteBeforeOp(hookType HookType, ctx *HookContext) error {
if err := r.Execute(BeforeOp, ctx); err != nil {
return err
}
return r.Execute(hookType, ctx)
}
// Clear removes all hooks for the specified type // Clear removes all hooks for the specified type
func (r *HookRegistry) Clear(hookType HookType) { func (r *HookRegistry) Clear(hookType HookType) {
delete(r.hooks, hookType) delete(r.hooks, hookType)
+28 -4
View File
@@ -221,7 +221,7 @@ func (h *Handler) handleRequest(conn *Connection, msg *Message) {
// handleRead processes a read operation // handleRead processes a read operation
func (h *Handler) handleRead(conn *Connection, msg *Message, hookCtx *HookContext) { func (h *Handler) handleRead(conn *Connection, msg *Message, hookCtx *HookContext) {
// Execute before hook // Execute before hook
if err := h.hooks.Execute(BeforeRead, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeRead, hookCtx); err != nil {
logger.Error("[WebSocketSpec] BeforeRead hook failed: %v", err) logger.Error("[WebSocketSpec] BeforeRead hook failed: %v", err)
errResp := NewErrorResponse(msg.ID, "hook_error", err.Error()) errResp := NewErrorResponse(msg.ID, "hook_error", err.Error())
_ = conn.SendJSON(errResp) _ = conn.SendJSON(errResp)
@@ -273,7 +273,7 @@ func (h *Handler) handleRead(conn *Connection, msg *Message, hookCtx *HookContex
// handleCreate processes a create operation // handleCreate processes a create operation
func (h *Handler) handleCreate(conn *Connection, msg *Message, hookCtx *HookContext) { func (h *Handler) handleCreate(conn *Connection, msg *Message, hookCtx *HookContext) {
// Execute before hook // Execute before hook
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeCreate, hookCtx); err != nil {
logger.Error("[WebSocketSpec] BeforeCreate hook failed: %v", err) logger.Error("[WebSocketSpec] BeforeCreate hook failed: %v", err)
errResp := NewErrorResponse(msg.ID, "hook_error", err.Error()) errResp := NewErrorResponse(msg.ID, "hook_error", err.Error())
_ = conn.SendJSON(errResp) _ = conn.SendJSON(errResp)
@@ -311,7 +311,7 @@ func (h *Handler) handleCreate(conn *Connection, msg *Message, hookCtx *HookCont
// handleUpdate processes an update operation // handleUpdate processes an update operation
func (h *Handler) handleUpdate(conn *Connection, msg *Message, hookCtx *HookContext) { func (h *Handler) handleUpdate(conn *Connection, msg *Message, hookCtx *HookContext) {
// Execute before hook // Execute before hook
if err := h.hooks.Execute(BeforeUpdate, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeUpdate, hookCtx); err != nil {
logger.Error("[WebSocketSpec] BeforeUpdate hook failed: %v", err) logger.Error("[WebSocketSpec] BeforeUpdate hook failed: %v", err)
errResp := NewErrorResponse(msg.ID, "hook_error", err.Error()) errResp := NewErrorResponse(msg.ID, "hook_error", err.Error())
_ = conn.SendJSON(errResp) _ = conn.SendJSON(errResp)
@@ -349,7 +349,7 @@ func (h *Handler) handleUpdate(conn *Connection, msg *Message, hookCtx *HookCont
// handleDelete processes a delete operation // handleDelete processes a delete operation
func (h *Handler) handleDelete(conn *Connection, msg *Message, hookCtx *HookContext) { func (h *Handler) handleDelete(conn *Connection, msg *Message, hookCtx *HookContext) {
// Execute before hook // Execute before hook
if err := h.hooks.Execute(BeforeDelete, hookCtx); err != nil { if err := h.hooks.ExecuteBeforeOp(BeforeDelete, hookCtx); err != nil {
logger.Error("[WebSocketSpec] BeforeDelete hook failed: %v", err) logger.Error("[WebSocketSpec] BeforeDelete hook failed: %v", err)
errResp := NewErrorResponse(msg.ID, "hook_error", err.Error()) errResp := NewErrorResponse(msg.ID, "hook_error", err.Error())
_ = conn.SendJSON(errResp) _ = conn.SendJSON(errResp)
@@ -574,6 +574,18 @@ func (h *Handler) readByID(hookCtx *HookContext) (interface{}, error) {
} }
} }
// Execute BeforeScan hooks - pass query so hooks (e.g. row-level security) can modify it
if hookCtx.Metadata == nil {
hookCtx.Metadata = make(map[string]interface{})
}
hookCtx.Metadata["query"] = query
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
return nil, fmt.Errorf("BeforeScan hook failed: %w", err)
}
if modifiedQuery, ok := hookCtx.Metadata["query"].(common.SelectQuery); ok {
query = modifiedQuery
}
// Execute query // Execute query
if err := query.ScanModel(hookCtx.Context); err != nil { if err := query.ScanModel(hookCtx.Context); err != nil {
return nil, fmt.Errorf("failed to read record: %w", err) return nil, fmt.Errorf("failed to read record: %w", err)
@@ -624,6 +636,18 @@ func (h *Handler) readMultiple(hookCtx *HookContext) (data interface{}, metadata
} }
} }
// Execute BeforeScan hooks - pass query so hooks (e.g. row-level security) can modify it
if hookCtx.Metadata == nil {
hookCtx.Metadata = make(map[string]interface{})
}
hookCtx.Metadata["query"] = query
if err := h.hooks.ExecuteBeforeOp(BeforeScan, hookCtx); err != nil {
return nil, nil, fmt.Errorf("BeforeScan hook failed: %w", err)
}
if modifiedQuery, ok := hookCtx.Metadata["query"].(common.SelectQuery); ok {
query = modifiedQuery
}
// Execute query // Execute query
if err := query.ScanModel(hookCtx.Context); err != nil { if err := query.ScanModel(hookCtx.Context); err != nil {
return nil, nil, fmt.Errorf("failed to read records: %w", err) return nil, nil, fmt.Errorf("failed to read records: %w", err)
+20
View File
@@ -35,6 +35,11 @@ const (
// AfterDelete is called after a delete operation // AfterDelete is called after a delete operation
AfterDelete HookType = "after_delete" AfterDelete HookType = "after_delete"
// BeforeScan is called right before a read query is executed against the database,
// after all filters/sort/pagination have been applied. Use this for row-level
// security that needs to modify the query (stored in HookContext.Metadata["query"]).
BeforeScan HookType = "before_scan"
// BeforeSubscribe is called before creating a subscription // BeforeSubscribe is called before creating a subscription
BeforeSubscribe HookType = "before_subscribe" BeforeSubscribe HookType = "before_subscribe"
// AfterSubscribe is called after creating a subscription // AfterSubscribe is called after creating a subscription
@@ -54,6 +59,11 @@ const (
BeforeDisconnect HookType = "before_disconnect" BeforeDisconnect HookType = "before_disconnect"
// AfterDisconnect is called after a connection is closed // AfterDisconnect is called after a connection is closed
AfterDisconnect HookType = "after_disconnect" AfterDisconnect HookType = "after_disconnect"
// BeforeOp fires immediately before every SQL operation (read, create, update, delete).
// Unlike BeforeHandle, which fires once before operation dispatch, BeforeOp fires at each
// individual SQL-operation hook point, so it runs once per statement executed.
BeforeOp HookType = "before_op"
) )
// HookContext contains context information for hook execution // HookContext contains context information for hook execution
@@ -197,6 +207,16 @@ func (hr *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
return nil return nil
} }
// ExecuteBeforeOp executes the BeforeOp hook followed by the given SQL-operation hook
// (BeforeRead, BeforeCreate, BeforeUpdate, or BeforeDelete). BeforeOp always runs first
// so it can observe/veto every SQL operation regardless of type.
func (hr *HookRegistry) ExecuteBeforeOp(hookType HookType, ctx *HookContext) error {
if err := hr.Execute(BeforeOp, ctx); err != nil {
return err
}
return hr.Execute(hookType, ctx)
}
// HasHooks checks if any hooks are registered for a hook type // HasHooks checks if any hooks are registered for a hook type
func (hr *HookRegistry) HasHooks(hookType HookType) bool { func (hr *HookRegistry) HasHooks(hookType HookType) bool {
hooks, exists := hr.hooks[hookType] hooks, exists := hr.hooks[hookType]
+10 -4
View File
@@ -27,25 +27,31 @@ func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList
return security.LoadSecurityRules(secCtx, securityList) return security.LoadSecurityRules(secCtx, securityList)
}) })
// Hook 2: AfterRead - Apply column-level security (masking) // Hook 2: BeforeScan - Apply row-level security filters
handler.Hooks().Register(BeforeScan, func(hookCtx *HookContext) error {
secCtx := newSecurityContext(hookCtx)
return security.ApplyRowSecurity(secCtx, securityList)
})
// Hook 3: AfterRead - Apply column-level security (masking)
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error { handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
secCtx := newSecurityContext(hookCtx) secCtx := newSecurityContext(hookCtx)
return security.ApplyColumnSecurity(secCtx, securityList) return security.ApplyColumnSecurity(secCtx, securityList)
}) })
// Hook 3 (Optional): Audit logging // Hook 4 (Optional): Audit logging
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error { handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
secCtx := newSecurityContext(hookCtx) secCtx := newSecurityContext(hookCtx)
return security.LogDataAccess(secCtx) return security.LogDataAccess(secCtx)
}) })
// Hook 4: BeforeUpdate - enforce CanUpdate rule from context/registry // Hook 5: BeforeUpdate - enforce CanUpdate rule from context/registry
handler.Hooks().Register(BeforeUpdate, func(hookCtx *HookContext) error { handler.Hooks().Register(BeforeUpdate, func(hookCtx *HookContext) error {
secCtx := newSecurityContext(hookCtx) secCtx := newSecurityContext(hookCtx)
return security.CheckModelUpdateAllowed(secCtx) return security.CheckModelUpdateAllowed(secCtx)
}) })
// Hook 5: BeforeDelete - enforce CanDelete rule from context/registry // Hook 6: BeforeDelete - enforce CanDelete rule from context/registry
handler.Hooks().Register(BeforeDelete, func(hookCtx *HookContext) error { handler.Hooks().Register(BeforeDelete, func(hookCtx *HookContext) error {
secCtx := newSecurityContext(hookCtx) secCtx := newSecurityContext(hookCtx)
return security.CheckModelDeleteAllowed(secCtx) return security.CheckModelDeleteAllowed(secCtx)