feat(hooks): add BeforeOp hook and fix BeforeScan row-security gap
Tests / Unit Tests (push) Failing after 11s
Tests / Integration Tests (push) Failing after 15s
Build , Vet Test, and Lint / Build (push) Successful in 1m45s
Build , Vet Test, and Lint / Run Vet Tests (1.24.x) (push) Successful in 2m0s
Build , Vet Test, and Lint / Run Vet Tests (1.23.x) (push) Successful in 2m0s
Build , Vet Test, and Lint / Lint Code (push) Successful in 2m4s

Adds a BeforeOp HookType across resolvespec, restheadspec, websocketspec,
mqttspec, and funcspec that fires before every SQL operation (read,
create, update, delete, scan/query) via a new ExecuteBeforeOp helper.

Also closes a row-level-security gap: resolvespec registered a BeforeScan
hook for ApplyRowSecurity but never fired it, and websocketspec/mqttspec
had no BeforeScan hook point at all, so row security was never applied
to their queries. BeforeScan now fires right before the actual scan in
all three, with the (possibly hook-modified) query used for execution.
This commit is contained in:
2026-07-25 11:39:31 +02:00
parent 873e8925d4
commit 52d3dca1fa
12 changed files with 191 additions and 43 deletions
+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 {
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"
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
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)
statusCode, errCode, errMsg = http.StatusBadRequest, "hook_error", "Hook execution failed"
return err
@@ -1184,7 +1184,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
results := make([]interface{}, 0)
txErr := h.db.RunInTransaction(ctx, func(tx common.Database) error {
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"
return err
}
@@ -1269,7 +1269,7 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
Query: query,
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)
}
@@ -1423,7 +1423,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
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)
}
@@ -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
hookCtx.Query = query
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)
}
@@ -1544,7 +1544,7 @@ func (h *Handler) handleUpdate(ctx context.Context, w common.ResponseWriter, id
// pooled connection rather than the now-dead tx.
hookCtx.Tx = h.db
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)
h.sendError(w, http.StatusInternalServerError, "hook_error", "Hook execution failed", err)
return
@@ -1619,7 +1619,7 @@ func (h *Handler) handleDelete(ctx context.Context, w common.ResponseWriter, id
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)
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,
}
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)
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,
}
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)
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,
}
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)
h.sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
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
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)
h.sendError(w, http.StatusBadRequest, "hook_error", "Hook execution failed", err)
return
+15
View File
@@ -34,6 +34,11 @@ const (
// Scan/Execute operation hooks
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
@@ -137,6 +142,16 @@ func (r *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
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
func (r *HookRegistry) Clear(hookType HookType) {
delete(r.hooks, hookType)