mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-07-30 14:17:39 +00:00
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
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:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user