mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-03-07 05:58:55 +00:00
feat(security): add BeforeHandle hook for auth checks after model resolution
- Implement BeforeHandle hook to enforce authentication based on model rules. - Integrate with existing security mechanisms to allow or deny access. - Update documentation to reflect new hook and its usage.
This commit is contained in:
@@ -2,14 +2,38 @@ package funcspec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/security"
|
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegisterSecurityHooks registers security hooks for funcspec handlers
|
// RegisterSecurityHooks registers security hooks for funcspec handlers
|
||||||
// Note: funcspec operates on SQL queries directly, so row-level security is not directly applicable
|
// Note: funcspec operates on SQL queries directly, so row-level security is not directly applicable
|
||||||
// We provide audit logging for data access tracking
|
// We provide auth enforcement and audit logging for data access tracking
|
||||||
func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList) {
|
func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList) {
|
||||||
|
// Hook 0: BeforeQueryList - Auth check before list query execution
|
||||||
|
handler.Hooks().Register(BeforeQueryList, func(hookCtx *HookContext) error {
|
||||||
|
if hookCtx.UserContext == nil || hookCtx.UserContext.UserID == 0 {
|
||||||
|
hookCtx.Abort = true
|
||||||
|
hookCtx.AbortMessage = "authentication required"
|
||||||
|
hookCtx.AbortCode = http.StatusUnauthorized
|
||||||
|
return fmt.Errorf("authentication required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hook 0: BeforeQuery - Auth check before single query execution
|
||||||
|
handler.Hooks().Register(BeforeQuery, func(hookCtx *HookContext) error {
|
||||||
|
if hookCtx.UserContext == nil || hookCtx.UserContext.UserID == 0 {
|
||||||
|
hookCtx.Abort = true
|
||||||
|
hookCtx.AbortMessage = "authentication required"
|
||||||
|
hookCtx.AbortCode = http.StatusUnauthorized
|
||||||
|
return fmt.Errorf("authentication required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// Hook 1: BeforeQueryList - Audit logging before query list execution
|
// Hook 1: BeforeQueryList - Audit logging before query list execution
|
||||||
handler.Hooks().Register(BeforeQueryList, func(hookCtx *HookContext) error {
|
handler.Hooks().Register(BeforeQueryList, func(hookCtx *HookContext) error {
|
||||||
secCtx := newFuncSpecSecurityContext(hookCtx)
|
secCtx := newFuncSpecSecurityContext(hookCtx)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
type ModelRules struct {
|
type ModelRules struct {
|
||||||
CanPublicRead bool // Whether the model can be read (GET operations)
|
CanPublicRead bool // Whether the model can be read (GET operations)
|
||||||
CanPublicUpdate bool // Whether the model can be updated (PUT/PATCH operations)
|
CanPublicUpdate bool // Whether the model can be updated (PUT/PATCH operations)
|
||||||
|
CanPublicCreate bool // Whether the model can be created (POST operations)
|
||||||
|
CanPublicDelete bool // Whether the model can be deleted (DELETE operations)
|
||||||
CanRead bool // Whether the model can be read (GET operations)
|
CanRead bool // Whether the model can be read (GET operations)
|
||||||
CanUpdate bool // Whether the model can be updated (PUT/PATCH operations)
|
CanUpdate bool // Whether the model can be updated (PUT/PATCH operations)
|
||||||
CanCreate bool // Whether the model can be created (POST operations)
|
CanCreate bool // Whether the model can be created (POST operations)
|
||||||
@@ -26,6 +28,8 @@ func DefaultModelRules() ModelRules {
|
|||||||
CanDelete: true,
|
CanDelete: true,
|
||||||
CanPublicRead: false,
|
CanPublicRead: false,
|
||||||
CanPublicUpdate: false,
|
CanPublicUpdate: false,
|
||||||
|
CanPublicCreate: false,
|
||||||
|
CanPublicDelete: false,
|
||||||
SecurityDisabled: false,
|
SecurityDisabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ MQTTSpec is an MQTT-based database query framework that enables real-time databa
|
|||||||
- **Full CRUD Operations**: Create, Read, Update, Delete with hooks
|
- **Full CRUD Operations**: Create, Read, Update, Delete with hooks
|
||||||
- **Real-time Subscriptions**: Subscribe to entity changes with filtering
|
- **Real-time Subscriptions**: Subscribe to entity changes with filtering
|
||||||
- **Database Agnostic**: GORM and Bun ORM support
|
- **Database Agnostic**: GORM and Bun ORM support
|
||||||
- **Lifecycle Hooks**: 12 hooks for authentication, authorization, validation, and auditing
|
- **Lifecycle Hooks**: 13 hooks for authentication, authorization, validation, and auditing
|
||||||
- **Multi-tenancy Support**: Built-in tenant isolation via hooks
|
- **Multi-tenancy Support**: Built-in tenant isolation via hooks
|
||||||
- **Thread-safe**: Proper concurrency handling throughout
|
- **Thread-safe**: Proper concurrency handling throughout
|
||||||
|
|
||||||
@@ -326,10 +326,11 @@ When any client creates/updates/deletes a user matching the subscription filters
|
|||||||
|
|
||||||
## Lifecycle Hooks
|
## Lifecycle Hooks
|
||||||
|
|
||||||
MQTTSpec provides 12 lifecycle hooks for implementing cross-cutting concerns:
|
MQTTSpec provides 13 lifecycle hooks for implementing cross-cutting concerns:
|
||||||
|
|
||||||
### Hook Types
|
### Hook Types
|
||||||
|
|
||||||
|
- `BeforeHandle` — fires after model resolution, before operation dispatch (auth checks)
|
||||||
- `BeforeConnect` / `AfterConnect` - Connection lifecycle
|
- `BeforeConnect` / `AfterConnect` - Connection lifecycle
|
||||||
- `BeforeDisconnect` / `AfterDisconnect` - Disconnection lifecycle
|
- `BeforeDisconnect` / `AfterDisconnect` - Disconnection lifecycle
|
||||||
- `BeforeRead` / `AfterRead` - Read operations
|
- `BeforeRead` / `AfterRead` - Read operations
|
||||||
@@ -339,6 +340,20 @@ MQTTSpec provides 12 lifecycle hooks for implementing cross-cutting concerns:
|
|||||||
- `BeforeSubscribe` / `AfterSubscribe` - Subscription creation
|
- `BeforeSubscribe` / `AfterSubscribe` - Subscription creation
|
||||||
- `BeforeUnsubscribe` / `AfterUnsubscribe` - Subscription removal
|
- `BeforeUnsubscribe` / `AfterUnsubscribe` - Subscription removal
|
||||||
|
|
||||||
|
### Security Hooks (Recommended)
|
||||||
|
|
||||||
|
Use `RegisterSecurityHooks` for integrated auth with model-rule support:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/bitechdev/ResolveSpec/pkg/security"
|
||||||
|
|
||||||
|
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||||
|
securityList := security.NewSecurityList(provider)
|
||||||
|
mqttspec.RegisterSecurityHooks(handler, securityList)
|
||||||
|
// Registers BeforeHandle (model auth), BeforeRead (load rules),
|
||||||
|
// AfterRead (column security + audit), BeforeUpdate, BeforeDelete
|
||||||
|
```
|
||||||
|
|
||||||
### Authentication Example (JWT)
|
### Authentication Example (JWT)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -657,7 +672,7 @@ handler, err := mqttspec.NewHandlerWithGORM(db,
|
|||||||
| **Network Efficiency** | Better for unreliable networks | Better for low-latency |
|
| **Network Efficiency** | Better for unreliable networks | Better for low-latency |
|
||||||
| **Best For** | IoT, mobile apps, distributed systems | Web applications, real-time dashboards |
|
| **Best For** | IoT, mobile apps, distributed systems | Web applications, real-time dashboards |
|
||||||
| **Message Protocol** | Same JSON structure | Same JSON structure |
|
| **Message Protocol** | Same JSON structure | Same JSON structure |
|
||||||
| **Hooks** | Same 12 hooks | Same 12 hooks |
|
| **Hooks** | Same 13 hooks | Same 13 hooks |
|
||||||
| **CRUD Operations** | Identical | Identical |
|
| **CRUD Operations** | Identical | Identical |
|
||||||
| **Subscriptions** | Identical (via MQTT topics) | Identical (via app-level) |
|
| **Subscriptions** | Identical (via MQTT topics) | Identical (via app-level) |
|
||||||
|
|
||||||
|
|||||||
@@ -284,6 +284,15 @@ func (h *Handler) handleRequest(client *Client, msg *Message) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute BeforeHandle hook - auth check fires here, after model resolution
|
||||||
|
hookCtx.Operation = string(msg.Operation)
|
||||||
|
if err := h.hooks.Execute(BeforeHandle, hookCtx); err != nil {
|
||||||
|
if hookCtx.Abort {
|
||||||
|
h.sendError(client.ID, msg.ID, "unauthorized", hookCtx.AbortMessage)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Route to operation handler
|
// Route to operation handler
|
||||||
switch msg.Operation {
|
switch msg.Operation {
|
||||||
case OperationRead:
|
case OperationRead:
|
||||||
|
|||||||
@@ -20,8 +20,11 @@ type (
|
|||||||
HookRegistry = websocketspec.HookRegistry
|
HookRegistry = websocketspec.HookRegistry
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hook type constants - all 12 lifecycle hooks
|
// Hook type constants - all lifecycle hooks
|
||||||
const (
|
const (
|
||||||
|
// BeforeHandle fires after model resolution, before operation dispatch
|
||||||
|
BeforeHandle = websocketspec.BeforeHandle
|
||||||
|
|
||||||
// CRUD operation hooks
|
// CRUD operation hooks
|
||||||
BeforeRead = websocketspec.BeforeRead
|
BeforeRead = websocketspec.BeforeRead
|
||||||
AfterRead = websocketspec.AfterRead
|
AfterRead = websocketspec.AfterRead
|
||||||
|
|||||||
108
pkg/mqttspec/security_hooks.go
Normal file
108
pkg/mqttspec/security_hooks.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package mqttspec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
|
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterSecurityHooks registers all security-related hooks with the MQTT handler
|
||||||
|
func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList) {
|
||||||
|
// Hook 0: BeforeHandle - enforce auth after model resolution
|
||||||
|
handler.Hooks().Register(BeforeHandle, func(hookCtx *HookContext) error {
|
||||||
|
if err := security.CheckModelAuthAllowed(newSecurityContext(hookCtx), hookCtx.Operation); err != nil {
|
||||||
|
hookCtx.Abort = true
|
||||||
|
hookCtx.AbortMessage = err.Error()
|
||||||
|
hookCtx.AbortCode = http.StatusUnauthorized
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hook 1: BeforeRead - Load security rules
|
||||||
|
handler.Hooks().Register(BeforeRead, func(hookCtx *HookContext) error {
|
||||||
|
secCtx := newSecurityContext(hookCtx)
|
||||||
|
return security.LoadSecurityRules(secCtx, securityList)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hook 2: 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
|
||||||
|
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
||||||
|
secCtx := newSecurityContext(hookCtx)
|
||||||
|
return security.LogDataAccess(secCtx)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Hook 4: 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
|
||||||
|
handler.Hooks().Register(BeforeDelete, func(hookCtx *HookContext) error {
|
||||||
|
secCtx := newSecurityContext(hookCtx)
|
||||||
|
return security.CheckModelDeleteAllowed(secCtx)
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.Info("Security hooks registered for mqttspec handler")
|
||||||
|
}
|
||||||
|
|
||||||
|
// securityContext adapts mqttspec.HookContext to security.SecurityContext interface
|
||||||
|
type securityContext struct {
|
||||||
|
ctx *HookContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSecurityContext(ctx *HookContext) security.SecurityContext {
|
||||||
|
return &securityContext{ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *securityContext) GetContext() context.Context {
|
||||||
|
return s.ctx.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *securityContext) GetUserID() (int, bool) {
|
||||||
|
return security.GetUserID(s.ctx.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *securityContext) GetSchema() string {
|
||||||
|
return s.ctx.Schema
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *securityContext) GetEntity() string {
|
||||||
|
return s.ctx.Entity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *securityContext) GetModel() interface{} {
|
||||||
|
return s.ctx.Model
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQuery retrieves a stored query from hook metadata
|
||||||
|
func (s *securityContext) GetQuery() interface{} {
|
||||||
|
if s.ctx.Metadata == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.ctx.Metadata["query"]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetQuery stores the query in hook metadata
|
||||||
|
func (s *securityContext) SetQuery(query interface{}) {
|
||||||
|
if s.ctx.Metadata == nil {
|
||||||
|
s.ctx.Metadata = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
s.ctx.Metadata["query"] = query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *securityContext) GetResult() interface{} {
|
||||||
|
return s.ctx.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *securityContext) SetResult(result interface{}) {
|
||||||
|
s.ctx.Result = result
|
||||||
|
}
|
||||||
@@ -644,6 +644,7 @@ handler.Hooks().Register(resolvespec.BeforeCreate, func(ctx *resolvespec.HookCon
|
|||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Tags []Tag `json:"tags,omitempty" gorm:"many2many:post_tags"`
|
Tags []Tag `json:"tags,omitempty" gorm:"many2many:post_tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schema.Table format
|
// Schema.Table format
|
||||||
handler.registry.RegisterModel("core.users", &User{})
|
handler.registry.RegisterModel("core.users", &User{})
|
||||||
handler.registry.RegisterModel("core.posts", &Post{})
|
handler.registry.RegisterModel("core.posts", &Post{})
|
||||||
@@ -654,11 +655,13 @@ handler.Hooks().Register(resolvespec.BeforeCreate, func(ctx *resolvespec.HookCon
|
|||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/resolvespec"
|
"github.com/bitechdev/ResolveSpec/pkg/resolvespec"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,26 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
|||||||
validator := common.NewColumnValidator(model)
|
validator := common.NewColumnValidator(model)
|
||||||
req.Options = validator.FilterRequestOptions(req.Options)
|
req.Options = validator.FilterRequestOptions(req.Options)
|
||||||
|
|
||||||
|
// Execute BeforeHandle hook - auth check fires here, after model resolution
|
||||||
|
beforeCtx := &HookContext{
|
||||||
|
Context: ctx,
|
||||||
|
Handler: h,
|
||||||
|
Schema: schema,
|
||||||
|
Entity: entity,
|
||||||
|
Model: model,
|
||||||
|
Writer: w,
|
||||||
|
Request: r,
|
||||||
|
Operation: req.Operation,
|
||||||
|
}
|
||||||
|
if err := h.hooks.Execute(BeforeHandle, beforeCtx); err != nil {
|
||||||
|
code := http.StatusUnauthorized
|
||||||
|
if beforeCtx.AbortCode != 0 {
|
||||||
|
code = beforeCtx.AbortCode
|
||||||
|
}
|
||||||
|
h.sendError(w, code, "unauthorized", beforeCtx.AbortMessage, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch req.Operation {
|
switch req.Operation {
|
||||||
case "read":
|
case "read":
|
||||||
h.handleRead(ctx, w, id, req.Options)
|
h.handleRead(ctx, w, id, req.Options)
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import (
|
|||||||
type HookType string
|
type HookType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// BeforeHandle fires after model resolution, before operation dispatch.
|
||||||
|
// Use this for auth checks that need model rules and user context simultaneously.
|
||||||
|
BeforeHandle HookType = "before_handle"
|
||||||
|
|
||||||
// Read operation hooks
|
// Read operation hooks
|
||||||
BeforeRead HookType = "before_read"
|
BeforeRead HookType = "before_read"
|
||||||
AfterRead HookType = "after_read"
|
AfterRead HookType = "after_read"
|
||||||
@@ -43,6 +47,9 @@ type HookContext struct {
|
|||||||
Writer common.ResponseWriter
|
Writer common.ResponseWriter
|
||||||
Request common.Request
|
Request common.Request
|
||||||
|
|
||||||
|
// Operation being dispatched (e.g. "read", "create", "update", "delete")
|
||||||
|
Operation string
|
||||||
|
|
||||||
// Operation-specific fields
|
// Operation-specific fields
|
||||||
ID string
|
ID string
|
||||||
Data interface{} // For create/update operations
|
Data interface{} // For create/update operations
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package resolvespec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/common"
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
@@ -10,6 +11,17 @@ import (
|
|||||||
|
|
||||||
// RegisterSecurityHooks registers all security-related hooks with the handler
|
// RegisterSecurityHooks registers all security-related hooks with the handler
|
||||||
func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList) {
|
func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList) {
|
||||||
|
// Hook 0: BeforeHandle - enforce auth after model resolution
|
||||||
|
handler.Hooks().Register(BeforeHandle, func(hookCtx *HookContext) error {
|
||||||
|
if err := security.CheckModelAuthAllowed(newSecurityContext(hookCtx), hookCtx.Operation); err != nil {
|
||||||
|
hookCtx.Abort = true
|
||||||
|
hookCtx.AbortMessage = err.Error()
|
||||||
|
hookCtx.AbortCode = http.StatusUnauthorized
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// Hook 1: BeforeRead - Load security rules
|
// Hook 1: BeforeRead - Load security rules
|
||||||
handler.Hooks().Register(BeforeRead, func(hookCtx *HookContext) error {
|
handler.Hooks().Register(BeforeRead, func(hookCtx *HookContext) error {
|
||||||
secCtx := newSecurityContext(hookCtx)
|
secCtx := newSecurityContext(hookCtx)
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ handler.Hooks.Register(restheadspec.BeforeCreate, func(ctx *restheadspec.HookCon
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Available Hook Types**:
|
**Available Hook Types**:
|
||||||
|
* `BeforeHandle` — fires after model resolution, before operation dispatch (auth checks)
|
||||||
* `BeforeRead`, `AfterRead`
|
* `BeforeRead`, `AfterRead`
|
||||||
* `BeforeCreate`, `AfterCreate`
|
* `BeforeCreate`, `AfterCreate`
|
||||||
* `BeforeUpdate`, `AfterUpdate`
|
* `BeforeUpdate`, `AfterUpdate`
|
||||||
@@ -157,11 +158,13 @@ handler.Hooks.Register(restheadspec.BeforeCreate, func(ctx *restheadspec.HookCon
|
|||||||
* `Handler`: Access to handler, database, and registry
|
* `Handler`: Access to handler, database, and registry
|
||||||
* `Schema`, `Entity`, `TableName`: Request info
|
* `Schema`, `Entity`, `TableName`: Request info
|
||||||
* `Model`: The registered model type
|
* `Model`: The registered model type
|
||||||
|
* `Operation`: Current operation string (`"read"`, `"create"`, `"update"`, `"delete"`)
|
||||||
* `Options`: Parsed request options (filters, sorting, etc.)
|
* `Options`: Parsed request options (filters, sorting, etc.)
|
||||||
* `ID`: Record ID (for single-record operations)
|
* `ID`: Record ID (for single-record operations)
|
||||||
* `Data`: Request data (for create/update)
|
* `Data`: Request data (for create/update)
|
||||||
* `Result`: Operation result (for after hooks)
|
* `Result`: Operation result (for after hooks)
|
||||||
* `Writer`: Response writer (allows hooks to modify response)
|
* `Writer`: Response writer (allows hooks to modify response)
|
||||||
|
* `Abort`, `AbortMessage`, `AbortCode`: Set in hook to abort with an error response
|
||||||
|
|
||||||
## Cursor Pagination
|
## Cursor Pagination
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,41 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
|||||||
// Add request-scoped data to context (including options)
|
// Add request-scoped data to context (including options)
|
||||||
ctx = WithRequestData(ctx, schema, entity, tableName, model, modelPtr, options)
|
ctx = WithRequestData(ctx, schema, entity, tableName, model, modelPtr, options)
|
||||||
|
|
||||||
|
// Derive operation for auth check
|
||||||
|
var operation string
|
||||||
|
switch method {
|
||||||
|
case "GET":
|
||||||
|
operation = "read"
|
||||||
|
case "POST":
|
||||||
|
operation = "create"
|
||||||
|
case "PUT", "PATCH":
|
||||||
|
operation = "update"
|
||||||
|
case "DELETE":
|
||||||
|
operation = "delete"
|
||||||
|
default:
|
||||||
|
operation = "read"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute BeforeHandle hook - auth check fires here, after model resolution
|
||||||
|
beforeCtx := &HookContext{
|
||||||
|
Context: ctx,
|
||||||
|
Handler: h,
|
||||||
|
Schema: schema,
|
||||||
|
Entity: entity,
|
||||||
|
Model: model,
|
||||||
|
Writer: w,
|
||||||
|
Request: r,
|
||||||
|
Operation: operation,
|
||||||
|
}
|
||||||
|
if err := h.hooks.Execute(BeforeHandle, beforeCtx); err != nil {
|
||||||
|
code := http.StatusUnauthorized
|
||||||
|
if beforeCtx.AbortCode != 0 {
|
||||||
|
code = beforeCtx.AbortCode
|
||||||
|
}
|
||||||
|
h.sendError(w, code, "unauthorized", beforeCtx.AbortMessage, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch method {
|
switch method {
|
||||||
case "GET":
|
case "GET":
|
||||||
if id != "" {
|
if id != "" {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import (
|
|||||||
type HookType string
|
type HookType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// BeforeHandle fires after model resolution, before operation dispatch.
|
||||||
|
// Use this for auth checks that need model rules and user context simultaneously.
|
||||||
|
BeforeHandle HookType = "before_handle"
|
||||||
|
|
||||||
// Read operation hooks
|
// Read operation hooks
|
||||||
BeforeRead HookType = "before_read"
|
BeforeRead HookType = "before_read"
|
||||||
AfterRead HookType = "after_read"
|
AfterRead HookType = "after_read"
|
||||||
@@ -42,6 +46,9 @@ type HookContext struct {
|
|||||||
Model interface{}
|
Model interface{}
|
||||||
Options ExtendedRequestOptions
|
Options ExtendedRequestOptions
|
||||||
|
|
||||||
|
// Operation being dispatched (e.g. "read", "create", "update", "delete")
|
||||||
|
Operation string
|
||||||
|
|
||||||
// Operation-specific fields
|
// Operation-specific fields
|
||||||
ID string
|
ID string
|
||||||
Data interface{} // For create/update operations
|
Data interface{} // For create/update operations
|
||||||
@@ -56,6 +63,14 @@ type HookContext struct {
|
|||||||
// Response writer - allows hooks to modify response
|
// Response writer - allows hooks to modify response
|
||||||
Writer common.ResponseWriter
|
Writer common.ResponseWriter
|
||||||
|
|
||||||
|
// Request - the original HTTP request
|
||||||
|
Request common.Request
|
||||||
|
|
||||||
|
// Allow hooks to abort the operation
|
||||||
|
Abort bool // If set to true, the operation will be aborted
|
||||||
|
AbortMessage string // Message to return if aborted
|
||||||
|
AbortCode int // HTTP status code if aborted
|
||||||
|
|
||||||
// Tx provides access to the database/transaction for executing additional SQL
|
// Tx provides access to the database/transaction for executing additional SQL
|
||||||
// This allows hooks to run custom queries in addition to the main Query chain
|
// This allows hooks to run custom queries in addition to the main Query chain
|
||||||
Tx common.Database
|
Tx common.Database
|
||||||
@@ -110,6 +125,12 @@ func (r *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
|
|||||||
logger.Error("Hook %d for %s failed: %v", i+1, hookType, err)
|
logger.Error("Hook %d for %s failed: %v", i+1, hookType, err)
|
||||||
return fmt.Errorf("hook execution failed: %w", err)
|
return fmt.Errorf("hook execution failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if hook requested abort
|
||||||
|
if ctx.Abort {
|
||||||
|
logger.Warn("Hook %d for %s requested abort: %s", i+1, hookType, ctx.AbortMessage)
|
||||||
|
return fmt.Errorf("operation aborted by hook: %s", ctx.AbortMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// logger.Debug("All hooks for %s executed successfully", hookType)
|
// logger.Debug("All hooks for %s executed successfully", hookType)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package restheadspec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/security"
|
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||||
@@ -9,6 +10,17 @@ import (
|
|||||||
|
|
||||||
// RegisterSecurityHooks registers all security-related hooks with the handler
|
// RegisterSecurityHooks registers all security-related hooks with the handler
|
||||||
func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList) {
|
func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList) {
|
||||||
|
// Hook 0: BeforeHandle - enforce auth after model resolution
|
||||||
|
handler.Hooks().Register(BeforeHandle, func(hookCtx *HookContext) error {
|
||||||
|
if err := security.CheckModelAuthAllowed(newSecurityContext(hookCtx), hookCtx.Operation); err != nil {
|
||||||
|
hookCtx.Abort = true
|
||||||
|
hookCtx.AbortMessage = err.Error()
|
||||||
|
hookCtx.AbortCode = http.StatusUnauthorized
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// Hook 1: BeforeRead - Load security rules
|
// Hook 1: BeforeRead - Load security rules
|
||||||
handler.Hooks().Register(BeforeRead, func(hookCtx *HookContext) error {
|
handler.Hooks().Register(BeforeRead, func(hookCtx *HookContext) error {
|
||||||
secCtx := newSecurityContext(hookCtx)
|
secCtx := newSecurityContext(hookCtx)
|
||||||
|
|||||||
@@ -405,11 +405,16 @@ assert.Equal(t, "user_id = {UserID}", row.Template)
|
|||||||
```
|
```
|
||||||
HTTP Request
|
HTTP Request
|
||||||
↓
|
↓
|
||||||
NewAuthMiddleware → calls provider.Authenticate()
|
NewOptionalAuthMiddleware → calls provider.Authenticate()
|
||||||
↓ (adds UserContext to context)
|
↓ (adds UserContext or guest context; never 401)
|
||||||
SetSecurityMiddleware → adds SecurityList to context
|
SetSecurityMiddleware → adds SecurityList to context
|
||||||
↓
|
↓
|
||||||
Handler.Handle()
|
Handler.Handle() → resolves model
|
||||||
|
↓
|
||||||
|
BeforeHandle Hook → CheckModelAuthAllowed(secCtx, operation)
|
||||||
|
├─ SecurityDisabled → allow
|
||||||
|
├─ CanPublicRead/Create/Update/Delete → allow unauthenticated
|
||||||
|
└─ UserID == 0 → abort 401
|
||||||
↓
|
↓
|
||||||
BeforeRead Hook → calls provider.GetColumnSecurity() + GetRowSecurity()
|
BeforeRead Hook → calls provider.GetColumnSecurity() + GetRowSecurity()
|
||||||
↓
|
↓
|
||||||
@@ -693,15 +698,30 @@ http.Handle("/api/protected", authHandler)
|
|||||||
optionalHandler := security.NewOptionalAuthHandler(securityList, myHandler)
|
optionalHandler := security.NewOptionalAuthHandler(securityList, myHandler)
|
||||||
http.Handle("/home", optionalHandler)
|
http.Handle("/home", optionalHandler)
|
||||||
|
|
||||||
// Example handler
|
// NewOptionalAuthMiddleware - For spec routes; auth enforcement deferred to BeforeHandle
|
||||||
func myHandler(w http.ResponseWriter, r *http.Request) {
|
apiRouter.Use(security.NewOptionalAuthMiddleware(securityList))
|
||||||
userCtx, _ := security.GetUserContext(r.Context())
|
apiRouter.Use(security.SetSecurityMiddleware(securityList))
|
||||||
if userCtx.UserID == 0 {
|
restheadspec.RegisterSecurityHooks(handler, securityList) // includes BeforeHandle
|
||||||
// Guest user
|
```
|
||||||
} else {
|
|
||||||
// Authenticated user
|
---
|
||||||
}
|
|
||||||
}
|
## Model-Level Access Control
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Register model with rules (pkg/modelregistry)
|
||||||
|
modelregistry.RegisterModelWithRules("public.products", &Product{}, modelregistry.ModelRules{
|
||||||
|
SecurityDisabled: false, // skip all auth when true
|
||||||
|
CanPublicRead: true, // unauthenticated reads allowed
|
||||||
|
CanPublicCreate: false, // requires auth
|
||||||
|
CanPublicUpdate: false, // requires auth
|
||||||
|
CanPublicDelete: false, // requires auth
|
||||||
|
CanUpdate: true, // authenticated can update
|
||||||
|
CanDelete: false, // authenticated cannot delete (enforced in BeforeDelete)
|
||||||
|
})
|
||||||
|
|
||||||
|
// CheckModelAuthAllowed used automatically in BeforeHandle hook
|
||||||
|
// No code needed — call RegisterSecurityHooks and it's applied
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -751,14 +751,25 @@ resolvespec.RegisterSecurityHooks(resolveHandler, securityList)
|
|||||||
```
|
```
|
||||||
HTTP Request
|
HTTP Request
|
||||||
↓
|
↓
|
||||||
NewAuthMiddleware (security package)
|
NewOptionalAuthMiddleware (security package) ← recommended for spec routes
|
||||||
├─ Calls provider.Authenticate(request)
|
├─ Calls provider.Authenticate(request)
|
||||||
└─ Adds UserContext to context
|
├─ On success: adds authenticated UserContext to context
|
||||||
|
└─ On failure: adds guest UserContext (UserID=0) to context
|
||||||
↓
|
↓
|
||||||
SetSecurityMiddleware (security package)
|
SetSecurityMiddleware (security package)
|
||||||
└─ Adds SecurityList to context
|
└─ Adds SecurityList to context
|
||||||
↓
|
↓
|
||||||
Spec Handler (restheadspec/funcspec/resolvespec)
|
Spec Handler (restheadspec/funcspec/resolvespec/websocketspec/mqttspec)
|
||||||
|
└─ Resolves schema + entity + model from request
|
||||||
|
↓
|
||||||
|
BeforeHandle Hook (registered by spec via RegisterSecurityHooks)
|
||||||
|
├─ Adapts spec's HookContext → SecurityContext
|
||||||
|
├─ Calls security.CheckModelAuthAllowed(secCtx, operation)
|
||||||
|
│ ├─ Loads model rules from context or registry
|
||||||
|
│ ├─ SecurityDisabled → allow
|
||||||
|
│ ├─ CanPublicRead/Create/Update/Delete → allow unauthenticated
|
||||||
|
│ └─ UserID == 0 → 401 unauthorized
|
||||||
|
└─ On error: aborts with 401
|
||||||
↓
|
↓
|
||||||
BeforeRead Hook (registered by spec)
|
BeforeRead Hook (registered by spec)
|
||||||
├─ Adapts spec's HookContext → SecurityContext
|
├─ Adapts spec's HookContext → SecurityContext
|
||||||
@@ -784,7 +795,8 @@ HTTP Response (secured data)
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Key Points:**
|
**Key Points:**
|
||||||
- Security package is spec-agnostic and provides core logic
|
- `NewOptionalAuthMiddleware` never rejects — it sets guest context on auth failure; `BeforeHandle` enforces auth after model resolution
|
||||||
|
- `BeforeHandle` fires after model resolution, giving access to model rules and user context simultaneously
|
||||||
- Each spec registers its own hooks that adapt to SecurityContext
|
- Each spec registers its own hooks that adapt to SecurityContext
|
||||||
- Security rules are loaded once and cached for the request
|
- Security rules are loaded once and cached for the request
|
||||||
- Row security is applied to the query (database level)
|
- Row security is applied to the query (database level)
|
||||||
@@ -1002,15 +1014,49 @@ func (p *MyProvider) GetRowSecurity(ctx context.Context, userID int, schema, tab
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Model-Level Access Control
|
||||||
|
|
||||||
|
Use `ModelRules` (from `pkg/modelregistry`) to control per-entity auth behavior:
|
||||||
|
|
||||||
|
```go
|
||||||
|
modelregistry.RegisterModelWithRules("public.products", &Product{}, modelregistry.ModelRules{
|
||||||
|
SecurityDisabled: false, // true = skip all auth checks
|
||||||
|
CanPublicRead: true, // unauthenticated GET allowed
|
||||||
|
CanPublicCreate: false, // requires auth
|
||||||
|
CanPublicUpdate: false, // requires auth
|
||||||
|
CanPublicDelete: false, // requires auth
|
||||||
|
CanUpdate: true, // authenticated users can update
|
||||||
|
CanDelete: false, // authenticated users cannot delete
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`CheckModelAuthAllowed(secCtx, operation)` applies these rules in `BeforeHandle`:
|
||||||
|
1. `SecurityDisabled` → allow all
|
||||||
|
2. `CanPublicRead/Create/Update/Delete` → allow unauthenticated for that operation
|
||||||
|
3. Guest (UserID == 0) → return 401
|
||||||
|
4. Authenticated → allow (operation-specific `CanUpdate`/`CanDelete` checked in `BeforeUpdate`/`BeforeDelete`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Middleware and Handler API
|
## Middleware and Handler API
|
||||||
|
|
||||||
### NewAuthMiddleware
|
### NewAuthMiddleware
|
||||||
Standard middleware that authenticates all requests:
|
Standard middleware that authenticates all requests and returns 401 on failure:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
router.Use(security.NewAuthMiddleware(securityList))
|
router.Use(security.NewAuthMiddleware(securityList))
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### NewOptionalAuthMiddleware
|
||||||
|
Middleware for spec routes — always continues; sets guest context on auth failure:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Use with RegisterSecurityHooks — auth enforcement is deferred to BeforeHandle
|
||||||
|
apiRouter.Use(security.NewOptionalAuthMiddleware(securityList))
|
||||||
|
apiRouter.Use(security.SetSecurityMiddleware(securityList))
|
||||||
|
restheadspec.RegisterSecurityHooks(handler, securityList) // registers BeforeHandle
|
||||||
|
```
|
||||||
|
|
||||||
Routes can skip authentication using the `SkipAuth` helper:
|
Routes can skip authentication using the `SkipAuth` helper:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
|||||||
@@ -275,6 +275,64 @@ func checkModelDeleteAllowed(secCtx SecurityContext) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckModelAuthAllowed checks whether the requested operation is permitted based on
|
||||||
|
// model rules and the current user's authentication state. It is intended for use in
|
||||||
|
// a BeforeHandle hook, fired after model resolution.
|
||||||
|
//
|
||||||
|
// Logic:
|
||||||
|
// 1. Load model rules from context (set by NewModelAuthMiddleware) or fall back to registry.
|
||||||
|
// 2. SecurityDisabled → allow.
|
||||||
|
// 3. operation == "read" && CanPublicRead → allow.
|
||||||
|
// 4. operation == "create" && CanPublicCreate → allow.
|
||||||
|
// 5. operation == "update" && CanPublicUpdate → allow.
|
||||||
|
// 6. operation == "delete" && CanPublicDelete → allow.
|
||||||
|
// 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())
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rules.SecurityDisabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if operation == "read" && rules.CanPublicRead {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if operation == "create" && rules.CanPublicCreate {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if operation == "update" && rules.CanPublicUpdate {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if operation == "delete" && rules.CanPublicDelete {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, _ := secCtx.GetUserID()
|
||||||
|
if userID == 0 {
|
||||||
|
return fmt.Errorf("authentication required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// CheckModelUpdateAllowed is the public wrapper for checkModelUpdateAllowed.
|
// CheckModelUpdateAllowed is the public wrapper for checkModelUpdateAllowed.
|
||||||
func CheckModelUpdateAllowed(secCtx SecurityContext) error {
|
func CheckModelUpdateAllowed(secCtx SecurityContext) error {
|
||||||
return checkModelUpdateAllowed(secCtx)
|
return checkModelUpdateAllowed(secCtx)
|
||||||
|
|||||||
@@ -139,6 +139,31 @@ func NewOptionalAuthHandler(securityList *SecurityList, next http.Handler) http.
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewOptionalAuthMiddleware creates authentication middleware that always continues.
|
||||||
|
// On auth failure, a guest user context is set instead of returning 401.
|
||||||
|
// Intended for spec routes where auth enforcement is deferred to a BeforeHandle hook
|
||||||
|
// after model resolution.
|
||||||
|
func NewOptionalAuthMiddleware(securityList *SecurityList) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
provider := securityList.Provider()
|
||||||
|
if provider == nil {
|
||||||
|
http.Error(w, "Security provider not configured", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userCtx, err := provider.Authenticate(r)
|
||||||
|
if err != nil {
|
||||||
|
guestCtx := createGuestContext(r)
|
||||||
|
next.ServeHTTP(w, setUserContext(r, guestCtx))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, setUserContext(r, userCtx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewAuthMiddleware creates an authentication middleware with the given security list
|
// NewAuthMiddleware creates an authentication middleware with the given security list
|
||||||
// This middleware extracts user authentication from the request and adds it to context
|
// This middleware extracts user authentication from the request and adds it to context
|
||||||
// Routes can skip authentication by setting SkipAuthKey context value (use SkipAuth helper)
|
// Routes can skip authentication by setting SkipAuthKey context value (use SkipAuth helper)
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ Hooks allow you to intercept and modify operations at various points in the life
|
|||||||
|
|
||||||
### Available Hook Types
|
### Available Hook Types
|
||||||
|
|
||||||
|
- **BeforeHandle** — fires after model resolution, before operation dispatch (auth checks)
|
||||||
- **BeforeRead** / **AfterRead**
|
- **BeforeRead** / **AfterRead**
|
||||||
- **BeforeCreate** / **AfterCreate**
|
- **BeforeCreate** / **AfterCreate**
|
||||||
- **BeforeUpdate** / **AfterUpdate**
|
- **BeforeUpdate** / **AfterUpdate**
|
||||||
@@ -337,6 +338,8 @@ Hooks allow you to intercept and modify operations at various points in the life
|
|||||||
- **BeforeSubscribe** / **AfterSubscribe**
|
- **BeforeSubscribe** / **AfterSubscribe**
|
||||||
- **BeforeConnect** / **AfterConnect**
|
- **BeforeConnect** / **AfterConnect**
|
||||||
|
|
||||||
|
`HookContext` includes `Operation string` (`"read"`, `"create"`, `"update"`, `"delete"`) and `Abort bool`, `AbortMessage string`, `AbortCode int` for abort signaling.
|
||||||
|
|
||||||
### Hook Example
|
### Hook Example
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -599,7 +602,19 @@ asyncio.run(main())
|
|||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
Implement authentication using hooks:
|
Use `RegisterSecurityHooks` for integrated auth with model-rule support:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/bitechdev/ResolveSpec/pkg/security"
|
||||||
|
|
||||||
|
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||||
|
securityList := security.NewSecurityList(provider)
|
||||||
|
websocketspec.RegisterSecurityHooks(handler, securityList)
|
||||||
|
// Registers BeforeHandle (model auth), BeforeRead (load rules),
|
||||||
|
// AfterRead (column security + audit), BeforeUpdate, BeforeDelete
|
||||||
|
```
|
||||||
|
|
||||||
|
Or implement custom authentication using hooks directly:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
handler := websocketspec.NewHandlerWithGORM(db)
|
handler := websocketspec.NewHandlerWithGORM(db)
|
||||||
|
|||||||
@@ -177,6 +177,16 @@ func (h *Handler) handleRequest(conn *Connection, msg *Message) {
|
|||||||
Metadata: make(map[string]interface{}),
|
Metadata: make(map[string]interface{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute BeforeHandle hook - auth check fires here, after model resolution
|
||||||
|
hookCtx.Operation = string(msg.Operation)
|
||||||
|
if err := h.hooks.Execute(BeforeHandle, hookCtx); err != nil {
|
||||||
|
if hookCtx.Abort {
|
||||||
|
errResp := NewErrorResponse(msg.ID, "unauthorized", hookCtx.AbortMessage)
|
||||||
|
_ = conn.SendJSON(errResp)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Route to operation handler
|
// Route to operation handler
|
||||||
switch msg.Operation {
|
switch msg.Operation {
|
||||||
case OperationRead:
|
case OperationRead:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package websocketspec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/common"
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||||
)
|
)
|
||||||
@@ -10,6 +11,10 @@ import (
|
|||||||
type HookType string
|
type HookType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// BeforeHandle fires after model resolution, before operation dispatch.
|
||||||
|
// Use this for auth checks that need model rules and user context simultaneously.
|
||||||
|
BeforeHandle HookType = "before_handle"
|
||||||
|
|
||||||
// BeforeRead is called before a read operation
|
// BeforeRead is called before a read operation
|
||||||
BeforeRead HookType = "before_read"
|
BeforeRead HookType = "before_read"
|
||||||
// AfterRead is called after a read operation
|
// AfterRead is called after a read operation
|
||||||
@@ -83,6 +88,9 @@ type HookContext struct {
|
|||||||
// Options contains the parsed request options
|
// Options contains the parsed request options
|
||||||
Options *common.RequestOptions
|
Options *common.RequestOptions
|
||||||
|
|
||||||
|
// Operation being dispatched (e.g. "read", "create", "update", "delete")
|
||||||
|
Operation string
|
||||||
|
|
||||||
// ID is the record ID for single-record operations
|
// ID is the record ID for single-record operations
|
||||||
ID string
|
ID string
|
||||||
|
|
||||||
@@ -98,6 +106,11 @@ type HookContext struct {
|
|||||||
// Error is any error that occurred (for after hooks)
|
// Error is any error that occurred (for after hooks)
|
||||||
Error error
|
Error error
|
||||||
|
|
||||||
|
// Allow hooks to abort the operation
|
||||||
|
Abort bool // If set to true, the operation will be aborted
|
||||||
|
AbortMessage string // Message to return if aborted
|
||||||
|
AbortCode int // HTTP status code if aborted
|
||||||
|
|
||||||
// Metadata is additional context data
|
// Metadata is additional context data
|
||||||
Metadata map[string]interface{}
|
Metadata map[string]interface{}
|
||||||
}
|
}
|
||||||
@@ -171,6 +184,11 @@ func (hr *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
|
|||||||
if err := hook(ctx); err != nil {
|
if err := hook(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if hook requested abort
|
||||||
|
if ctx.Abort {
|
||||||
|
return fmt.Errorf("operation aborted by hook: %s", ctx.AbortMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package websocketspec
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||||
"github.com/bitechdev/ResolveSpec/pkg/security"
|
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||||
@@ -9,6 +10,17 @@ import (
|
|||||||
|
|
||||||
// RegisterSecurityHooks registers all security-related hooks with the handler
|
// RegisterSecurityHooks registers all security-related hooks with the handler
|
||||||
func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList) {
|
func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList) {
|
||||||
|
// Hook 0: BeforeHandle - enforce auth after model resolution
|
||||||
|
handler.Hooks().Register(BeforeHandle, func(hookCtx *HookContext) error {
|
||||||
|
if err := security.CheckModelAuthAllowed(newSecurityContext(hookCtx), hookCtx.Operation); err != nil {
|
||||||
|
hookCtx.Abort = true
|
||||||
|
hookCtx.AbortMessage = err.Error()
|
||||||
|
hookCtx.AbortCode = http.StatusUnauthorized
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
// Hook 1: BeforeRead - Load security rules
|
// Hook 1: BeforeRead - Load security rules
|
||||||
handler.Hooks().Register(BeforeRead, func(hookCtx *HookContext) error {
|
handler.Hooks().Register(BeforeRead, func(hookCtx *HookContext) error {
|
||||||
secCtx := newSecurityContext(hookCtx)
|
secCtx := newSecurityContext(hookCtx)
|
||||||
|
|||||||
Reference in New Issue
Block a user