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
+28 -4
View File
@@ -313,7 +313,7 @@ func (h *Handler) handleRequest(client *Client, msg *Message) {
// handleRead processes a read operation
func (h *Handler) handleRead(client *Client, msg *Message, hookCtx *HookContext) {
// 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)
h.sendError(client.ID, msg.ID, "hook_error", err.Error())
return
@@ -356,7 +356,7 @@ func (h *Handler) handleRead(client *Client, msg *Message, hookCtx *HookContext)
// handleCreate processes a create operation
func (h *Handler) handleCreate(client *Client, msg *Message, hookCtx *HookContext) {
// 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)
h.sendError(client.ID, msg.ID, "hook_error", err.Error())
return
@@ -390,7 +390,7 @@ func (h *Handler) handleCreate(client *Client, msg *Message, hookCtx *HookContex
// handleUpdate processes an update operation
func (h *Handler) handleUpdate(client *Client, msg *Message, hookCtx *HookContext) {
// 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)
h.sendError(client.ID, msg.ID, "hook_error", err.Error())
return
@@ -424,7 +424,7 @@ func (h *Handler) handleUpdate(client *Client, msg *Message, hookCtx *HookContex
// handleDelete processes a delete operation
func (h *Handler) handleDelete(client *Client, msg *Message, hookCtx *HookContext) {
// 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)
h.sendError(client.ID, msg.ID, "hook_error", err.Error())
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
if err := query.ScanModel(hookCtx.Context); err != nil {
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
if err := query.ScanModel(hookCtx.Context); err != nil {
return nil, nil, fmt.Errorf("failed to read records: %w", err)
+4
View File
@@ -34,6 +34,7 @@ const (
AfterUpdate = websocketspec.AfterUpdate
BeforeDelete = websocketspec.BeforeDelete
AfterDelete = websocketspec.AfterDelete
BeforeScan = websocketspec.BeforeScan
// Subscription hooks
BeforeSubscribe = websocketspec.BeforeSubscribe
@@ -46,6 +47,9 @@ const (
AfterConnect = websocketspec.AfterConnect
BeforeDisconnect = websocketspec.BeforeDisconnect
AfterDisconnect = websocketspec.AfterDisconnect
// BeforeOp fires immediately before every SQL operation (read, create, update, delete)
BeforeOp = websocketspec.BeforeOp
)
// 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)
})
// 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 {
secCtx := newSecurityContext(hookCtx)
return security.ApplyColumnSecurity(secCtx, securityList)
})
// Hook 3 (Optional): Audit logging
// Hook 4 (Optional): Audit logging
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
secCtx := newSecurityContext(hookCtx)
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 {
secCtx := newSecurityContext(hookCtx)
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 {
secCtx := newSecurityContext(hookCtx)
return security.CheckModelDeleteAllowed(secCtx)