From 87eaa9e18c0e1fcc19e2b434493aa25fff7b344f Mon Sep 17 00:00:00 2001 From: Hein Date: Sun, 20 Sep 2026 15:51:02 +0200 Subject: [PATCH] fix(security): skip row security enforcement for specific operations * Add ShouldSkipRowSecurity function to determine when to bypass row security * Update ApplyRowSecurity to utilize operation context for enforcement --- pkg/resolvespec/security_hooks.go | 7 ++++ pkg/security/hooks.go | 67 +++++++++++++++++++++++-------- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/pkg/resolvespec/security_hooks.go b/pkg/resolvespec/security_hooks.go index 3040018..24ac114 100644 --- a/pkg/resolvespec/security_hooks.go +++ b/pkg/resolvespec/security_hooks.go @@ -31,6 +31,9 @@ func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList // Hook 2: BeforeScan - Apply row-level security filters handler.Hooks().Register(BeforeScan, func(hookCtx *HookContext) error { secCtx := newSecurityContext(hookCtx) + if security.ShouldSkipRowSecurity(secCtx, hookCtx.Operation) { + return nil + } return security.ApplyRowSecurity(secCtx, securityList) }) @@ -97,6 +100,10 @@ func (s *securityContext) GetEntity() string { return s.ctx.Entity } +func (s *securityContext) GetOperation() string { + return s.ctx.Operation +} + func (s *securityContext) GetModel() interface{} { return s.ctx.Model } diff --git a/pkg/security/hooks.go b/pkg/security/hooks.go index 7628914..25693fd 100644 --- a/pkg/security/hooks.go +++ b/pkg/security/hooks.go @@ -232,9 +232,28 @@ func LoadSecurityRules(secCtx SecurityContext, securityList *SecurityList) error // ApplyRowSecurity is a public wrapper for applyRowSecurity that accepts a SecurityContext // This allows other packages to apply row-level security using the generic interface func ApplyRowSecurity(secCtx SecurityContext, securityList *SecurityList) error { + // Spec adapters that expose the dispatched operation can enforce the same + // model-rule bypass even when ApplyRowSecurity is called directly. + if operationCtx, ok := secCtx.(interface{ GetOperation() string }); ok && + ShouldSkipRowSecurity(secCtx, operationCtx.GetOperation()) { + return nil + } return applyRowSecurity(secCtx, securityList) } +// ShouldSkipRowSecurity reports whether row-security enforcement should be +// skipped for the operation. It uses the same model-rule resolution as +// CheckModelAuthAllowed so the model registry remains the single source of +// truth for security behavior. +func ShouldSkipRowSecurity(secCtx SecurityContext, operation string) bool { + rules, ok := resolveModelRules(secCtx) + if !ok { + return false + } + + return rules.SecurityDisabled || (operation == "read" && rules.CanPublicRead) +} + // ApplyColumnSecurity is a public wrapper for applyColumnSecurity that accepts a SecurityContext // This allows other packages to apply column-level security using the generic interface func ApplyColumnSecurity(secCtx SecurityContext, securityList *SecurityList) error { @@ -303,25 +322,14 @@ func checkModelDeleteAllowed(secCtx SecurityContext) error { // 7. Guest (UserID == 0) → return "authentication required". // 8. Authenticated user → allow (operation-specific checks remain in BeforeUpdate/BeforeDelete). func CheckModelAuthAllowed(secCtx SecurityContext, operation string) error { - rules, ok := GetModelRulesFromContext(secCtx.GetContext()) + rules, ok := resolveModelRules(secCtx) if !ok { - schema := secCtx.GetSchema() - entity := secCtx.GetEntity() - var err error - if schema != "" { - rules, err = modelregistry.GetModelRulesByName(fmt.Sprintf("%s.%s", schema, entity)) - } - if err != nil || schema == "" { - rules, err = modelregistry.GetModelRulesByName(entity) - } - if err != nil { - // Model not registered - fall through to auth check - userID, _ := secCtx.GetUserID() - if userID == 0 { - return fmt.Errorf("authentication required") - } - return nil + // Model not registered - fall through to auth check + userID, _ := secCtx.GetUserID() + if userID == 0 { + return fmt.Errorf("authentication required") } + return nil } if rules.SecurityDisabled { @@ -347,6 +355,31 @@ func CheckModelAuthAllowed(secCtx SecurityContext, operation string) error { return nil } +// resolveModelRules returns model rules from the request context first, then +// falls back to the schema-qualified and unqualified registry names. +func resolveModelRules(secCtx SecurityContext) (modelregistry.ModelRules, bool) { + if rules, ok := GetModelRulesFromContext(secCtx.GetContext()); ok { + return rules, true + } + + schema := secCtx.GetSchema() + entity := secCtx.GetEntity() + var err error + if schema != "" { + var rules modelregistry.ModelRules + rules, err = modelregistry.GetModelRulesByName(fmt.Sprintf("%s.%s", schema, entity)) + if err == nil { + return rules, true + } + } + + rules, err := modelregistry.GetModelRulesByName(entity) + if err != nil { + return modelregistry.ModelRules{}, false + } + return rules, true +} + // CheckModelUpdateAllowed is the public wrapper for checkModelUpdateAllowed. func CheckModelUpdateAllowed(secCtx SecurityContext) error { return checkModelUpdateAllowed(secCtx)