mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-29 07:44:25 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93df33e274 | ||
|
|
abd045493a | ||
|
|
a61556d857 | ||
|
|
eaf1133575 | ||
|
|
8172c0495d | ||
|
|
7a3c368121 | ||
|
|
9c5c7689e9 |
@@ -47,8 +47,8 @@ func main() {
|
||||
handler.RegisterModel("public", modelNames[i], model)
|
||||
}
|
||||
|
||||
// Setup routes using new SetupMuxRoutes function
|
||||
resolvespec.SetupMuxRoutes(r, handler)
|
||||
// Setup routes using new SetupMuxRoutes function (without authentication)
|
||||
resolvespec.SetupMuxRoutes(r, handler, nil)
|
||||
|
||||
// Start server
|
||||
logger.Info("Starting server on :8080")
|
||||
|
||||
@@ -141,6 +141,12 @@ func (b *BunRouterRequest) AllHeaders() map[string]string {
|
||||
return headers
|
||||
}
|
||||
|
||||
// UnderlyingRequest returns the underlying *http.Request
|
||||
// This is useful when you need to pass the request to other handlers
|
||||
func (b *BunRouterRequest) UnderlyingRequest() *http.Request {
|
||||
return b.req.Request
|
||||
}
|
||||
|
||||
// StandardBunRouterAdapter creates routes compatible with standard bunrouter handlers
|
||||
type StandardBunRouterAdapter struct {
|
||||
*BunRouterAdapter
|
||||
|
||||
@@ -137,6 +137,12 @@ func (h *HTTPRequest) AllHeaders() map[string]string {
|
||||
return headers
|
||||
}
|
||||
|
||||
// UnderlyingRequest returns the underlying *http.Request
|
||||
// This is useful when you need to pass the request to other handlers
|
||||
func (h *HTTPRequest) UnderlyingRequest() *http.Request {
|
||||
return h.req
|
||||
}
|
||||
|
||||
// HTTPResponseWriter adapts our ResponseWriter interface to standard http.ResponseWriter
|
||||
type HTTPResponseWriter struct {
|
||||
resp http.ResponseWriter
|
||||
@@ -166,6 +172,12 @@ func (h *HTTPResponseWriter) WriteJSON(data interface{}) error {
|
||||
return json.NewEncoder(h.resp).Encode(data)
|
||||
}
|
||||
|
||||
// UnderlyingResponseWriter returns the underlying http.ResponseWriter
|
||||
// This is useful when you need to pass the response writer to other handlers
|
||||
func (h *HTTPResponseWriter) UnderlyingResponseWriter() http.ResponseWriter {
|
||||
return h.resp
|
||||
}
|
||||
|
||||
// StandardMuxAdapter creates routes compatible with standard http.HandlerFunc
|
||||
type StandardMuxAdapter struct {
|
||||
*MuxAdapter
|
||||
|
||||
97
pkg/common/handler_example.go
Normal file
97
pkg/common/handler_example.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package common
|
||||
|
||||
// Example showing how to use the common handler interfaces
|
||||
// This file demonstrates the handler interface hierarchy and usage patterns
|
||||
|
||||
// ProcessWithAnyHandler demonstrates using the base SpecHandler interface
|
||||
// which works with any handler type (resolvespec, restheadspec, or funcspec)
|
||||
func ProcessWithAnyHandler(handler SpecHandler) Database {
|
||||
// All handlers expose GetDatabase() through the SpecHandler interface
|
||||
return handler.GetDatabase()
|
||||
}
|
||||
|
||||
// ProcessCRUDRequest demonstrates using the CRUDHandler interface
|
||||
// which works with resolvespec.Handler and restheadspec.Handler
|
||||
func ProcessCRUDRequest(handler CRUDHandler, w ResponseWriter, r Request, params map[string]string) {
|
||||
// Both resolvespec and restheadspec handlers implement Handle()
|
||||
handler.Handle(w, r, params)
|
||||
}
|
||||
|
||||
// ProcessMetadataRequest demonstrates getting metadata from CRUD handlers
|
||||
func ProcessMetadataRequest(handler CRUDHandler, w ResponseWriter, r Request, params map[string]string) {
|
||||
// Both resolvespec and restheadspec handlers implement HandleGet()
|
||||
handler.HandleGet(w, r, params)
|
||||
}
|
||||
|
||||
// Example usage patterns (not executable, just for documentation):
|
||||
/*
|
||||
// Example 1: Using with resolvespec.Handler
|
||||
func ExampleResolveSpec() {
|
||||
db := // ... get database
|
||||
registry := // ... get registry
|
||||
|
||||
handler := resolvespec.NewHandler(db, registry)
|
||||
|
||||
// Can be used as SpecHandler
|
||||
var specHandler SpecHandler = handler
|
||||
database := specHandler.GetDatabase()
|
||||
|
||||
// Can be used as CRUDHandler
|
||||
var crudHandler CRUDHandler = handler
|
||||
crudHandler.Handle(w, r, params)
|
||||
crudHandler.HandleGet(w, r, params)
|
||||
}
|
||||
|
||||
// Example 2: Using with restheadspec.Handler
|
||||
func ExampleRestHeadSpec() {
|
||||
db := // ... get database
|
||||
registry := // ... get registry
|
||||
|
||||
handler := restheadspec.NewHandler(db, registry)
|
||||
|
||||
// Can be used as SpecHandler
|
||||
var specHandler SpecHandler = handler
|
||||
database := specHandler.GetDatabase()
|
||||
|
||||
// Can be used as CRUDHandler
|
||||
var crudHandler CRUDHandler = handler
|
||||
crudHandler.Handle(w, r, params)
|
||||
crudHandler.HandleGet(w, r, params)
|
||||
}
|
||||
|
||||
// Example 3: Using with funcspec.Handler
|
||||
func ExampleFuncSpec() {
|
||||
db := // ... get database
|
||||
|
||||
handler := funcspec.NewHandler(db)
|
||||
|
||||
// Can be used as SpecHandler
|
||||
var specHandler SpecHandler = handler
|
||||
database := specHandler.GetDatabase()
|
||||
|
||||
// Can be used as QueryHandler
|
||||
var queryHandler QueryHandler = handler
|
||||
// funcspec has different methods: SqlQueryList() and SqlQuery()
|
||||
// which return HTTP handler functions
|
||||
}
|
||||
|
||||
// Example 4: Polymorphic handler processing
|
||||
func ProcessHandlers(handlers []SpecHandler) {
|
||||
for _, handler := range handlers {
|
||||
// All handlers expose the database
|
||||
db := handler.GetDatabase()
|
||||
|
||||
// Type switch for specific handler types
|
||||
switch h := handler.(type) {
|
||||
case CRUDHandler:
|
||||
// This is resolvespec or restheadspec
|
||||
// Can call Handle() and HandleGet()
|
||||
_ = h
|
||||
case QueryHandler:
|
||||
// This is funcspec
|
||||
// Can call SqlQueryList() and SqlQuery()
|
||||
_ = h
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -122,6 +122,7 @@ type Request interface {
|
||||
PathParam(key string) string
|
||||
QueryParam(key string) string
|
||||
AllQueryParams() map[string]string // Get all query parameters as a map
|
||||
UnderlyingRequest() *http.Request // Get the underlying *http.Request for forwarding to other handlers
|
||||
}
|
||||
|
||||
// ResponseWriter interface abstracts HTTP response
|
||||
@@ -130,6 +131,7 @@ type ResponseWriter interface {
|
||||
WriteHeader(statusCode int)
|
||||
Write(data []byte) (int, error)
|
||||
WriteJSON(data interface{}) error
|
||||
UnderlyingResponseWriter() http.ResponseWriter // Get the underlying http.ResponseWriter for forwarding to other handlers
|
||||
}
|
||||
|
||||
// HTTPHandlerFunc type for HTTP handlers
|
||||
@@ -164,6 +166,10 @@ func (s *StandardResponseWriter) WriteJSON(data interface{}) error {
|
||||
return json.NewEncoder(s.w).Encode(data)
|
||||
}
|
||||
|
||||
func (s *StandardResponseWriter) UnderlyingResponseWriter() http.ResponseWriter {
|
||||
return s.w
|
||||
}
|
||||
|
||||
// StandardRequest adapts *http.Request to Request interface
|
||||
type StandardRequest struct {
|
||||
r *http.Request
|
||||
@@ -228,6 +234,10 @@ func (s *StandardRequest) AllQueryParams() map[string]string {
|
||||
return params
|
||||
}
|
||||
|
||||
func (s *StandardRequest) UnderlyingRequest() *http.Request {
|
||||
return s.r
|
||||
}
|
||||
|
||||
// TableNameProvider interface for models that provide table names
|
||||
type TableNameProvider interface {
|
||||
TableName() string
|
||||
@@ -246,3 +256,39 @@ type PrimaryKeyNameProvider interface {
|
||||
type SchemaProvider interface {
|
||||
SchemaName() string
|
||||
}
|
||||
|
||||
// SpecHandler interface represents common functionality across all spec handlers
|
||||
// This is the base interface implemented by:
|
||||
// - resolvespec.Handler: Handles CRUD operations via request body with explicit operation field
|
||||
// - restheadspec.Handler: Handles CRUD operations via HTTP methods (GET/POST/PUT/DELETE)
|
||||
// - funcspec.Handler: Handles custom SQL query execution with dynamic parameters
|
||||
//
|
||||
// The interface hierarchy is:
|
||||
//
|
||||
// SpecHandler (base)
|
||||
// ├── CRUDHandler (resolvespec, restheadspec)
|
||||
// └── QueryHandler (funcspec)
|
||||
type SpecHandler interface {
|
||||
// GetDatabase returns the underlying database connection
|
||||
GetDatabase() Database
|
||||
}
|
||||
|
||||
// CRUDHandler interface for handlers that support CRUD operations
|
||||
// This is implemented by resolvespec.Handler and restheadspec.Handler
|
||||
type CRUDHandler interface {
|
||||
SpecHandler
|
||||
|
||||
// Handle processes API requests through router-agnostic interface
|
||||
Handle(w ResponseWriter, r Request, params map[string]string)
|
||||
|
||||
// HandleGet processes GET requests for metadata
|
||||
HandleGet(w ResponseWriter, r Request, params map[string]string)
|
||||
}
|
||||
|
||||
// QueryHandler interface for handlers that execute SQL queries
|
||||
// This is implemented by funcspec.Handler
|
||||
// Note: funcspec uses standard http.ResponseWriter and *http.Request instead of common interfaces
|
||||
type QueryHandler interface {
|
||||
SpecHandler
|
||||
// Methods are defined in funcspec package due to different function signature requirements
|
||||
}
|
||||
|
||||
@@ -32,6 +32,12 @@ func NewHandler(db common.Database) *Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// GetDatabase returns the underlying database connection
|
||||
// Implements common.SpecHandler interface
|
||||
func (h *Handler) GetDatabase() common.Database {
|
||||
return h.db
|
||||
}
|
||||
|
||||
// Hooks returns the hook registry for this handler
|
||||
// Use this to register custom hooks for operations
|
||||
func (h *Handler) Hooks() *HookRegistry {
|
||||
|
||||
83
pkg/funcspec/security_adapter.go
Normal file
83
pkg/funcspec/security_adapter.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package funcspec
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||
)
|
||||
|
||||
// RegisterSecurityHooks registers security hooks for funcspec handlers
|
||||
// Note: funcspec operates on SQL queries directly, so row-level security is not directly applicable
|
||||
// We provide audit logging for data access tracking
|
||||
func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList) {
|
||||
// Hook 1: BeforeQueryList - Audit logging before query list execution
|
||||
handler.Hooks().Register(BeforeQueryList, func(hookCtx *HookContext) error {
|
||||
secCtx := newFuncSpecSecurityContext(hookCtx)
|
||||
return security.LogDataAccess(secCtx)
|
||||
})
|
||||
|
||||
// Hook 2: BeforeQuery - Audit logging before single query execution
|
||||
handler.Hooks().Register(BeforeQuery, func(hookCtx *HookContext) error {
|
||||
secCtx := newFuncSpecSecurityContext(hookCtx)
|
||||
return security.LogDataAccess(secCtx)
|
||||
})
|
||||
|
||||
// Note: Row-level security and column masking are challenging in funcspec
|
||||
// because the SQL query is fully user-defined. Security should be implemented
|
||||
// at the SQL function level or through database policies (RLS).
|
||||
}
|
||||
|
||||
// funcSpecSecurityContext adapts funcspec.HookContext to security.SecurityContext interface
|
||||
type funcSpecSecurityContext struct {
|
||||
ctx *HookContext
|
||||
}
|
||||
|
||||
func newFuncSpecSecurityContext(ctx *HookContext) security.SecurityContext {
|
||||
return &funcSpecSecurityContext{ctx: ctx}
|
||||
}
|
||||
|
||||
func (f *funcSpecSecurityContext) GetContext() context.Context {
|
||||
return f.ctx.Context
|
||||
}
|
||||
|
||||
func (f *funcSpecSecurityContext) GetUserID() (int, bool) {
|
||||
if f.ctx.UserContext == nil {
|
||||
return 0, false
|
||||
}
|
||||
return int(f.ctx.UserContext.UserID), true
|
||||
}
|
||||
|
||||
func (f *funcSpecSecurityContext) GetSchema() string {
|
||||
// funcspec doesn't have a schema concept, extract from SQL query or use default
|
||||
return "public"
|
||||
}
|
||||
|
||||
func (f *funcSpecSecurityContext) GetEntity() string {
|
||||
// funcspec doesn't have an entity concept, could parse from SQL or use a placeholder
|
||||
return "sql_query"
|
||||
}
|
||||
|
||||
func (f *funcSpecSecurityContext) GetModel() interface{} {
|
||||
// funcspec doesn't use models in the same way as restheadspec
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *funcSpecSecurityContext) GetQuery() interface{} {
|
||||
// In funcspec, the query is a string, not a query builder object
|
||||
return f.ctx.SQLQuery
|
||||
}
|
||||
|
||||
func (f *funcSpecSecurityContext) SetQuery(query interface{}) {
|
||||
// In funcspec, we could modify the SQL string, but this should be done cautiously
|
||||
if sqlQuery, ok := query.(string); ok {
|
||||
f.ctx.SQLQuery = sqlQuery
|
||||
}
|
||||
}
|
||||
|
||||
func (f *funcSpecSecurityContext) GetResult() interface{} {
|
||||
return f.ctx.Result
|
||||
}
|
||||
|
||||
func (f *funcSpecSecurityContext) SetResult(result interface{}) {
|
||||
f.ctx.Result = result
|
||||
}
|
||||
@@ -16,11 +16,17 @@ import (
|
||||
"github.com/bitechdev/ResolveSpec/pkg/reflection"
|
||||
)
|
||||
|
||||
// FallbackHandler is a function that handles requests when no model is found
|
||||
// It receives the same parameters as the Handle method
|
||||
type FallbackHandler func(w common.ResponseWriter, r common.Request, params map[string]string)
|
||||
|
||||
// Handler handles API requests using database and model abstractions
|
||||
type Handler struct {
|
||||
db common.Database
|
||||
registry common.ModelRegistry
|
||||
nestedProcessor *common.NestedCUDProcessor
|
||||
hooks *HookRegistry
|
||||
fallbackHandler FallbackHandler
|
||||
}
|
||||
|
||||
// NewHandler creates a new API handler with database and registry abstractions
|
||||
@@ -28,12 +34,31 @@ func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
|
||||
handler := &Handler{
|
||||
db: db,
|
||||
registry: registry,
|
||||
hooks: NewHookRegistry(),
|
||||
}
|
||||
// Initialize nested processor
|
||||
handler.nestedProcessor = common.NewNestedCUDProcessor(db, registry, handler)
|
||||
return handler
|
||||
}
|
||||
|
||||
// Hooks returns the hook registry for this handler
|
||||
// Use this to register custom hooks for operations
|
||||
func (h *Handler) Hooks() *HookRegistry {
|
||||
return h.hooks
|
||||
}
|
||||
|
||||
// SetFallbackHandler sets a fallback handler to be called when no model is found
|
||||
// If not set, the handler will simply return (pass through to next route)
|
||||
func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
|
||||
h.fallbackHandler = fallback
|
||||
}
|
||||
|
||||
// GetDatabase returns the underlying database connection
|
||||
// Implements common.SpecHandler interface
|
||||
func (h *Handler) GetDatabase() common.Database {
|
||||
return h.db
|
||||
}
|
||||
|
||||
// handlePanic is a helper function to handle panics with stack traces
|
||||
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
|
||||
stack := debug.Stack()
|
||||
@@ -75,8 +100,14 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
||||
// Get model and populate context with request-scoped data
|
||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
||||
if err != nil {
|
||||
logger.Error("Invalid entity: %v", err)
|
||||
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
||||
// Model not found - call fallback handler if set, otherwise pass through
|
||||
logger.Debug("Model not found for %s.%s", schema, entity)
|
||||
if h.fallbackHandler != nil {
|
||||
logger.Debug("Calling fallback handler for %s.%s", schema, entity)
|
||||
h.fallbackHandler(w, r, params)
|
||||
} else {
|
||||
logger.Debug("No fallback handler set, passing through to next route")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -142,8 +173,14 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma
|
||||
|
||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get model: %v", err)
|
||||
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
||||
// Model not found - call fallback handler if set, otherwise pass through
|
||||
logger.Debug("Model not found for %s.%s", schema, entity)
|
||||
if h.fallbackHandler != nil {
|
||||
logger.Debug("Calling fallback handler for %s.%s", schema, entity)
|
||||
h.fallbackHandler(w, r, params)
|
||||
} else {
|
||||
logger.Debug("No fallback handler set, passing through to next route")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
152
pkg/resolvespec/hooks.go
Normal file
152
pkg/resolvespec/hooks.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package resolvespec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||
)
|
||||
|
||||
// HookType defines the type of hook to execute
|
||||
type HookType string
|
||||
|
||||
const (
|
||||
// Read operation hooks
|
||||
BeforeRead HookType = "before_read"
|
||||
AfterRead HookType = "after_read"
|
||||
|
||||
// Create operation hooks
|
||||
BeforeCreate HookType = "before_create"
|
||||
AfterCreate HookType = "after_create"
|
||||
|
||||
// Update operation hooks
|
||||
BeforeUpdate HookType = "before_update"
|
||||
AfterUpdate HookType = "after_update"
|
||||
|
||||
// Delete operation hooks
|
||||
BeforeDelete HookType = "before_delete"
|
||||
AfterDelete HookType = "after_delete"
|
||||
|
||||
// Scan/Execute operation hooks (for query building)
|
||||
BeforeScan HookType = "before_scan"
|
||||
)
|
||||
|
||||
// HookContext contains all the data available to a hook
|
||||
type HookContext struct {
|
||||
Context context.Context
|
||||
Handler *Handler // Reference to the handler for accessing database, registry, etc.
|
||||
Schema string
|
||||
Entity string
|
||||
Model interface{}
|
||||
Options common.RequestOptions
|
||||
Writer common.ResponseWriter
|
||||
Request common.Request
|
||||
|
||||
// Operation-specific fields
|
||||
ID string
|
||||
Data interface{} // For create/update operations
|
||||
Result interface{} // For after hooks
|
||||
Error error // For after hooks
|
||||
|
||||
// Query chain - allows hooks to modify the query before execution
|
||||
Query common.SelectQuery
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// HookFunc is the signature for hook functions
|
||||
// It receives a HookContext and can modify it or return an error
|
||||
// If an error is returned, the operation will be aborted
|
||||
type HookFunc func(*HookContext) error
|
||||
|
||||
// HookRegistry manages all registered hooks
|
||||
type HookRegistry struct {
|
||||
hooks map[HookType][]HookFunc
|
||||
}
|
||||
|
||||
// NewHookRegistry creates a new hook registry
|
||||
func NewHookRegistry() *HookRegistry {
|
||||
return &HookRegistry{
|
||||
hooks: make(map[HookType][]HookFunc),
|
||||
}
|
||||
}
|
||||
|
||||
// Register adds a new hook for the specified hook type
|
||||
func (r *HookRegistry) Register(hookType HookType, hook HookFunc) {
|
||||
if r.hooks == nil {
|
||||
r.hooks = make(map[HookType][]HookFunc)
|
||||
}
|
||||
r.hooks[hookType] = append(r.hooks[hookType], hook)
|
||||
logger.Info("Registered resolvespec hook for %s (total: %d)", hookType, len(r.hooks[hookType]))
|
||||
}
|
||||
|
||||
// RegisterMultiple registers a hook for multiple hook types
|
||||
func (r *HookRegistry) RegisterMultiple(hookTypes []HookType, hook HookFunc) {
|
||||
for _, hookType := range hookTypes {
|
||||
r.Register(hookType, hook)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute runs all hooks for the specified type in order
|
||||
// If any hook returns an error, execution stops and the error is returned
|
||||
func (r *HookRegistry) Execute(hookType HookType, ctx *HookContext) error {
|
||||
hooks, exists := r.hooks[hookType]
|
||||
if !exists || len(hooks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debug("Executing %d resolvespec hook(s) for %s", len(hooks), hookType)
|
||||
|
||||
for i, hook := range hooks {
|
||||
if err := hook(ctx); err != nil {
|
||||
logger.Error("Resolvespec hook %d for %s failed: %v", i+1, hookType, err)
|
||||
return fmt.Errorf("hook execution failed: %w", err)
|
||||
}
|
||||
|
||||
// Check if hook requested abort
|
||||
if ctx.Abort {
|
||||
logger.Warn("Resolvespec hook %d for %s requested abort: %s", i+1, hookType, ctx.AbortMessage)
|
||||
return fmt.Errorf("operation aborted by hook: %s", ctx.AbortMessage)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear removes all hooks for the specified type
|
||||
func (r *HookRegistry) Clear(hookType HookType) {
|
||||
delete(r.hooks, hookType)
|
||||
logger.Info("Cleared all resolvespec hooks for %s", hookType)
|
||||
}
|
||||
|
||||
// ClearAll removes all registered hooks
|
||||
func (r *HookRegistry) ClearAll() {
|
||||
r.hooks = make(map[HookType][]HookFunc)
|
||||
logger.Info("Cleared all resolvespec hooks")
|
||||
}
|
||||
|
||||
// Count returns the number of hooks registered for a specific type
|
||||
func (r *HookRegistry) Count(hookType HookType) int {
|
||||
if hooks, exists := r.hooks[hookType]; exists {
|
||||
return len(hooks)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// HasHooks returns true if there are any hooks registered for the specified type
|
||||
func (r *HookRegistry) HasHooks(hookType HookType) bool {
|
||||
return r.Count(hookType) > 0
|
||||
}
|
||||
|
||||
// GetAllHookTypes returns all hook types that have registered hooks
|
||||
func (r *HookRegistry) GetAllHookTypes() []HookType {
|
||||
types := make([]HookType, 0, len(r.hooks))
|
||||
for hookType := range r.hooks {
|
||||
types = append(types, hookType)
|
||||
}
|
||||
return types
|
||||
}
|
||||
@@ -37,28 +37,46 @@ func NewStandardBunRouter() *router.StandardBunRouterAdapter {
|
||||
return router.NewStandardBunRouterAdapter()
|
||||
}
|
||||
|
||||
// MiddlewareFunc is a function that wraps an http.Handler with additional functionality
|
||||
type MiddlewareFunc func(http.Handler) http.Handler
|
||||
|
||||
// SetupMuxRoutes sets up routes for the ResolveSpec API with Mux
|
||||
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler) {
|
||||
muxRouter.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
||||
// authMiddleware is optional - if provided, routes will be protected with the middleware
|
||||
// Example: SetupMuxRoutes(router, handler, func(h http.Handler) http.Handler { return security.NewAuthHandler(securityList, h) })
|
||||
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware MiddlewareFunc) {
|
||||
// Create handler functions
|
||||
postEntityHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
reqAdapter := router.NewHTTPRequest(r)
|
||||
respAdapter := router.NewHTTPResponseWriter(w)
|
||||
handler.Handle(respAdapter, reqAdapter, vars)
|
||||
}).Methods("POST")
|
||||
})
|
||||
|
||||
muxRouter.HandleFunc("/{schema}/{entity}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
postEntityWithIDHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
reqAdapter := router.NewHTTPRequest(r)
|
||||
respAdapter := router.NewHTTPResponseWriter(w)
|
||||
handler.Handle(respAdapter, reqAdapter, vars)
|
||||
}).Methods("POST")
|
||||
})
|
||||
|
||||
muxRouter.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
||||
getEntityHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
reqAdapter := router.NewHTTPRequest(r)
|
||||
respAdapter := router.NewHTTPResponseWriter(w)
|
||||
handler.HandleGet(respAdapter, reqAdapter, vars)
|
||||
}).Methods("GET")
|
||||
})
|
||||
|
||||
// Apply authentication middleware if provided
|
||||
if authMiddleware != nil {
|
||||
postEntityHandler = authMiddleware(postEntityHandler).(http.HandlerFunc)
|
||||
postEntityWithIDHandler = authMiddleware(postEntityWithIDHandler).(http.HandlerFunc)
|
||||
getEntityHandler = authMiddleware(getEntityHandler).(http.HandlerFunc)
|
||||
}
|
||||
|
||||
// Register routes
|
||||
muxRouter.Handle("/{schema}/{entity}", postEntityHandler).Methods("POST")
|
||||
muxRouter.Handle("/{schema}/{entity}/{id}", postEntityWithIDHandler).Methods("POST")
|
||||
muxRouter.Handle("/{schema}/{entity}", getEntityHandler).Methods("GET")
|
||||
}
|
||||
|
||||
// Example usage functions for documentation:
|
||||
@@ -68,12 +86,20 @@ func ExampleWithGORM(db *gorm.DB) {
|
||||
// Create handler using GORM
|
||||
handler := NewHandlerWithGORM(db)
|
||||
|
||||
// Setup router
|
||||
// Setup router without authentication
|
||||
muxRouter := mux.NewRouter()
|
||||
SetupMuxRoutes(muxRouter, handler)
|
||||
SetupMuxRoutes(muxRouter, handler, nil)
|
||||
|
||||
// Register models
|
||||
// handler.RegisterModel("public", "users", &User{})
|
||||
|
||||
// To add authentication, pass a middleware function:
|
||||
// import "github.com/bitechdev/ResolveSpec/pkg/security"
|
||||
// secList := security.NewSecurityList(myProvider)
|
||||
// authMiddleware := func(h http.Handler) http.Handler {
|
||||
// return security.NewAuthHandler(secList, h)
|
||||
// }
|
||||
// SetupMuxRoutes(muxRouter, handler, authMiddleware)
|
||||
}
|
||||
|
||||
// ExampleWithBun shows how to switch to Bun ORM
|
||||
@@ -88,9 +114,9 @@ func ExampleWithBun(bunDB *bun.DB) {
|
||||
// Create handler
|
||||
handler := NewHandler(dbAdapter, registry)
|
||||
|
||||
// Setup routes
|
||||
// Setup routes without authentication
|
||||
muxRouter := mux.NewRouter()
|
||||
SetupMuxRoutes(muxRouter, handler)
|
||||
SetupMuxRoutes(muxRouter, handler, nil)
|
||||
}
|
||||
|
||||
// SetupBunRouterRoutes sets up bunrouter routes for the ResolveSpec API
|
||||
|
||||
85
pkg/resolvespec/security_hooks.go
Normal file
85
pkg/resolvespec/security_hooks.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package resolvespec
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||
)
|
||||
|
||||
// RegisterSecurityHooks registers all security-related hooks with the handler
|
||||
func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList) {
|
||||
// Hook 1: BeforeRead - Load security rules
|
||||
handler.Hooks().Register(BeforeRead, func(hookCtx *HookContext) error {
|
||||
secCtx := newSecurityContext(hookCtx)
|
||||
return security.LoadSecurityRules(secCtx, securityList)
|
||||
})
|
||||
|
||||
// Hook 2: BeforeScan - Apply row-level security filters
|
||||
handler.Hooks().Register(BeforeScan, func(hookCtx *HookContext) error {
|
||||
secCtx := newSecurityContext(hookCtx)
|
||||
return security.ApplyRowSecurity(secCtx, securityList)
|
||||
})
|
||||
|
||||
// Hook 3: AfterRead - Apply column-level security (masking)
|
||||
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
||||
secCtx := newSecurityContext(hookCtx)
|
||||
return security.ApplyColumnSecurity(secCtx, securityList)
|
||||
})
|
||||
|
||||
// Hook 4 (Optional): Audit logging
|
||||
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
||||
secCtx := newSecurityContext(hookCtx)
|
||||
return security.LogDataAccess(secCtx)
|
||||
})
|
||||
|
||||
logger.Info("Security hooks registered for resolvespec handler")
|
||||
}
|
||||
|
||||
// securityContext adapts resolvespec.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
|
||||
}
|
||||
|
||||
func (s *securityContext) GetQuery() interface{} {
|
||||
return s.ctx.Query
|
||||
}
|
||||
|
||||
func (s *securityContext) SetQuery(query interface{}) {
|
||||
if q, ok := query.(common.SelectQuery); ok {
|
||||
s.ctx.Query = q
|
||||
}
|
||||
}
|
||||
|
||||
func (s *securityContext) GetResult() interface{} {
|
||||
return s.ctx.Result
|
||||
}
|
||||
|
||||
func (s *securityContext) SetResult(result interface{}) {
|
||||
s.ctx.Result = result
|
||||
}
|
||||
@@ -17,6 +17,10 @@ import (
|
||||
"github.com/bitechdev/ResolveSpec/pkg/reflection"
|
||||
)
|
||||
|
||||
// FallbackHandler is a function that handles requests when no model is found
|
||||
// It receives the same parameters as the Handle method
|
||||
type FallbackHandler func(w common.ResponseWriter, r common.Request, params map[string]string)
|
||||
|
||||
// Handler handles API requests using database and model abstractions
|
||||
// This handler reads filters, columns, and options from HTTP headers
|
||||
type Handler struct {
|
||||
@@ -24,6 +28,7 @@ type Handler struct {
|
||||
registry common.ModelRegistry
|
||||
hooks *HookRegistry
|
||||
nestedProcessor *common.NestedCUDProcessor
|
||||
fallbackHandler FallbackHandler
|
||||
}
|
||||
|
||||
// NewHandler creates a new API handler with database and registry abstractions
|
||||
@@ -38,12 +43,24 @@ func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
|
||||
return handler
|
||||
}
|
||||
|
||||
// GetDatabase returns the underlying database connection
|
||||
// Implements common.SpecHandler interface
|
||||
func (h *Handler) GetDatabase() common.Database {
|
||||
return h.db
|
||||
}
|
||||
|
||||
// Hooks returns the hook registry for this handler
|
||||
// Use this to register custom hooks for operations
|
||||
func (h *Handler) Hooks() *HookRegistry {
|
||||
return h.hooks
|
||||
}
|
||||
|
||||
// SetFallbackHandler sets a fallback handler to be called when no model is found
|
||||
// If not set, the handler will simply return (pass through to next route)
|
||||
func (h *Handler) SetFallbackHandler(fallback FallbackHandler) {
|
||||
h.fallbackHandler = fallback
|
||||
}
|
||||
|
||||
// handlePanic is a helper function to handle panics with stack traces
|
||||
func (h *Handler) handlePanic(w common.ResponseWriter, method string, err interface{}) {
|
||||
stack := debug.Stack()
|
||||
@@ -75,8 +92,14 @@ func (h *Handler) Handle(w common.ResponseWriter, r common.Request, params map[s
|
||||
// Get model and populate context with request-scoped data
|
||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
||||
if err != nil {
|
||||
logger.Error("Invalid entity: %v", err)
|
||||
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
||||
// Model not found - call fallback handler if set, otherwise pass through
|
||||
logger.Debug("Model not found for %s.%s", schema, entity)
|
||||
if h.fallbackHandler != nil {
|
||||
logger.Debug("Calling fallback handler for %s.%s", schema, entity)
|
||||
h.fallbackHandler(w, r, params)
|
||||
} else {
|
||||
logger.Debug("No fallback handler set, passing through to next route")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -191,8 +214,14 @@ func (h *Handler) HandleGet(w common.ResponseWriter, r common.Request, params ma
|
||||
|
||||
model, err := h.registry.GetModelByEntity(schema, entity)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get model: %v", err)
|
||||
h.sendError(w, http.StatusBadRequest, "invalid_entity", "Invalid entity", err)
|
||||
// Model not found - call fallback handler if set, otherwise pass through
|
||||
logger.Debug("Model not found for %s.%s", schema, entity)
|
||||
if h.fallbackHandler != nil {
|
||||
logger.Debug("Calling fallback handler for %s.%s", schema, entity)
|
||||
h.fallbackHandler(w, r, params)
|
||||
} else {
|
||||
logger.Debug("No fallback handler set, passing through to next route")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package restheadspec
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -42,6 +43,12 @@ func (m *MockRequest) AllQueryParams() map[string]string {
|
||||
return m.queryParams
|
||||
}
|
||||
|
||||
func (m *MockRequest) UnderlyingRequest() *http.Request {
|
||||
// For testing purposes, return nil
|
||||
// In real scenarios, you might want to construct a proper http.Request
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestParseOptionsFromQueryParams(t *testing.T) {
|
||||
handler := NewHandler(nil, nil)
|
||||
|
||||
|
||||
@@ -90,31 +90,51 @@ func NewStandardBunRouter() *router.StandardBunRouterAdapter {
|
||||
return router.NewStandardBunRouterAdapter()
|
||||
}
|
||||
|
||||
// MiddlewareFunc is a function that wraps an http.Handler with additional functionality
|
||||
type MiddlewareFunc func(http.Handler) http.Handler
|
||||
|
||||
// SetupMuxRoutes sets up routes for the RestHeadSpec API with Mux
|
||||
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler) {
|
||||
// GET, POST, PUT, PATCH, DELETE for /{schema}/{entity}
|
||||
muxRouter.HandleFunc("/{schema}/{entity}", func(w http.ResponseWriter, r *http.Request) {
|
||||
// authMiddleware is optional - if provided, routes will be protected with the middleware
|
||||
// Example: SetupMuxRoutes(router, handler, func(h http.Handler) http.Handler { return security.NewAuthHandler(securityList, h) })
|
||||
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, authMiddleware MiddlewareFunc) {
|
||||
// Create handler functions
|
||||
entityHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
reqAdapter := router.NewHTTPRequest(r)
|
||||
respAdapter := router.NewHTTPResponseWriter(w)
|
||||
handler.Handle(respAdapter, reqAdapter, vars)
|
||||
}).Methods("GET", "POST")
|
||||
})
|
||||
|
||||
// GET, PUT, PATCH, DELETE for /{schema}/{entity}/{id}
|
||||
muxRouter.HandleFunc("/{schema}/{entity}/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
entityWithIDHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
reqAdapter := router.NewHTTPRequest(r)
|
||||
respAdapter := router.NewHTTPResponseWriter(w)
|
||||
handler.Handle(respAdapter, reqAdapter, vars)
|
||||
}).Methods("GET", "PUT", "PATCH", "DELETE", "POST")
|
||||
})
|
||||
|
||||
// GET for metadata (using HandleGet)
|
||||
muxRouter.HandleFunc("/{schema}/{entity}/metadata", func(w http.ResponseWriter, r *http.Request) {
|
||||
metadataHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
reqAdapter := router.NewHTTPRequest(r)
|
||||
respAdapter := router.NewHTTPResponseWriter(w)
|
||||
handler.HandleGet(respAdapter, reqAdapter, vars)
|
||||
}).Methods("GET")
|
||||
})
|
||||
|
||||
// Apply authentication middleware if provided
|
||||
if authMiddleware != nil {
|
||||
entityHandler = authMiddleware(entityHandler).(http.HandlerFunc)
|
||||
entityWithIDHandler = authMiddleware(entityWithIDHandler).(http.HandlerFunc)
|
||||
metadataHandler = authMiddleware(metadataHandler).(http.HandlerFunc)
|
||||
}
|
||||
|
||||
// Register routes
|
||||
// GET, POST for /{schema}/{entity}
|
||||
muxRouter.Handle("/{schema}/{entity}", entityHandler).Methods("GET", "POST")
|
||||
|
||||
// GET, PUT, PATCH, DELETE, POST for /{schema}/{entity}/{id}
|
||||
muxRouter.Handle("/{schema}/{entity}/{id}", entityWithIDHandler).Methods("GET", "PUT", "PATCH", "DELETE", "POST")
|
||||
|
||||
// GET for metadata (using HandleGet)
|
||||
muxRouter.Handle("/{schema}/{entity}/metadata", metadataHandler).Methods("GET")
|
||||
}
|
||||
|
||||
// Example usage functions for documentation:
|
||||
@@ -124,12 +144,20 @@ func ExampleWithGORM(db *gorm.DB) {
|
||||
// Create handler using GORM
|
||||
handler := NewHandlerWithGORM(db)
|
||||
|
||||
// Setup router
|
||||
// Setup router without authentication
|
||||
muxRouter := mux.NewRouter()
|
||||
SetupMuxRoutes(muxRouter, handler)
|
||||
SetupMuxRoutes(muxRouter, handler, nil)
|
||||
|
||||
// Register models
|
||||
// handler.registry.RegisterModel("public.users", &User{})
|
||||
|
||||
// To add authentication, pass a middleware function:
|
||||
// import "github.com/bitechdev/ResolveSpec/pkg/security"
|
||||
// secList := security.NewSecurityList(myProvider)
|
||||
// authMiddleware := func(h http.Handler) http.Handler {
|
||||
// return security.NewAuthHandler(secList, h)
|
||||
// }
|
||||
// SetupMuxRoutes(muxRouter, handler, authMiddleware)
|
||||
}
|
||||
|
||||
// ExampleWithBun shows how to switch to Bun ORM
|
||||
@@ -144,9 +172,9 @@ func ExampleWithBun(bunDB *bun.DB) {
|
||||
// Create handler
|
||||
handler := NewHandler(dbAdapter, registry)
|
||||
|
||||
// Setup routes
|
||||
// Setup routes without authentication
|
||||
muxRouter := mux.NewRouter()
|
||||
SetupMuxRoutes(muxRouter, handler)
|
||||
SetupMuxRoutes(muxRouter, handler, nil)
|
||||
}
|
||||
|
||||
// SetupBunRouterRoutes sets up bunrouter routes for the RestHeadSpec API
|
||||
|
||||
82
pkg/restheadspec/security_hooks.go
Normal file
82
pkg/restheadspec/security_hooks.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package restheadspec
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||
)
|
||||
|
||||
// RegisterSecurityHooks registers all security-related hooks with the handler
|
||||
func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList) {
|
||||
// Hook 1: BeforeRead - Load security rules
|
||||
handler.Hooks().Register(BeforeRead, func(hookCtx *HookContext) error {
|
||||
secCtx := newSecurityContext(hookCtx)
|
||||
return security.LoadSecurityRules(secCtx, securityList)
|
||||
})
|
||||
|
||||
// Hook 2: BeforeScan - Apply row-level security filters
|
||||
handler.Hooks().Register(BeforeScan, func(hookCtx *HookContext) error {
|
||||
secCtx := newSecurityContext(hookCtx)
|
||||
return security.ApplyRowSecurity(secCtx, securityList)
|
||||
})
|
||||
|
||||
// Hook 3: AfterRead - Apply column-level security (masking)
|
||||
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
||||
secCtx := newSecurityContext(hookCtx)
|
||||
return security.ApplyColumnSecurity(secCtx, securityList)
|
||||
})
|
||||
|
||||
// Hook 4 (Optional): Audit logging
|
||||
handler.Hooks().Register(AfterRead, func(hookCtx *HookContext) error {
|
||||
secCtx := newSecurityContext(hookCtx)
|
||||
return security.LogDataAccess(secCtx)
|
||||
})
|
||||
|
||||
logger.Info("Security hooks registered for restheadspec handler")
|
||||
}
|
||||
|
||||
// securityContext adapts restheadspec.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
|
||||
}
|
||||
|
||||
func (s *securityContext) GetQuery() interface{} {
|
||||
return s.ctx.Query
|
||||
}
|
||||
|
||||
func (s *securityContext) SetQuery(query interface{}) {
|
||||
s.ctx.Query = query
|
||||
}
|
||||
|
||||
func (s *securityContext) GetResult() interface{} {
|
||||
return s.ctx.Result
|
||||
}
|
||||
|
||||
func (s *securityContext) SetResult(result interface{}) {
|
||||
s.ctx.Result = result
|
||||
}
|
||||
@@ -56,9 +56,10 @@ rowSec := security.NewDatabaseRowSecurityProvider(db)
|
||||
// 2. Combine providers
|
||||
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
|
||||
// 3. Setup security
|
||||
// 3. Create handler and register security hooks
|
||||
handler := restheadspec.NewHandlerWithGORM(db)
|
||||
securityList := security.SetupSecurityProvider(handler, provider)
|
||||
securityList := security.NewSecurityList(provider)
|
||||
restheadspec.RegisterSecurityHooks(handler, securityList)
|
||||
|
||||
// 4. Apply middleware
|
||||
router := mux.NewRouter()
|
||||
@@ -69,6 +70,38 @@ router.Use(security.SetSecurityMiddleware(securityList))
|
||||
|
||||
## Architecture
|
||||
|
||||
### Spec-Agnostic Design
|
||||
|
||||
The security system is **completely spec-agnostic** - it doesn't depend on any specific spec implementation. Instead, each spec (restheadspec, funcspec, resolvespec) implements its own security integration by adapting to the `SecurityContext` interface.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Security Package (Generic) │
|
||||
│ - SecurityContext interface │
|
||||
│ - Security providers │
|
||||
│ - Core security logic │
|
||||
└─────────────────────────────────────┘
|
||||
▲ ▲ ▲
|
||||
│ │ │
|
||||
┌──────┘ │ └──────┐
|
||||
│ │ │
|
||||
┌───▼────┐ ┌────▼─────┐ ┌────▼──────┐
|
||||
│RestHead│ │ FuncSpec │ │ResolveSpec│
|
||||
│ Spec │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│Adapts │ │ Adapts │ │ Adapts │
|
||||
│to │ │ to │ │ to │
|
||||
│Security│ │ Security │ │ Security │
|
||||
│Context │ │ Context │ │ Context │
|
||||
└────────┘ └──────────┘ └───────────┘
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- ✅ No circular dependencies
|
||||
- ✅ Each spec can customize security integration
|
||||
- ✅ Easy to add new specs
|
||||
- ✅ Security logic is reusable across all specs
|
||||
|
||||
### Core Interfaces
|
||||
|
||||
The security system is built on three main interfaces:
|
||||
@@ -113,6 +146,28 @@ type SecurityProvider interface {
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. SecurityContext (Spec Integration Interface)
|
||||
Each spec implements this interface to integrate with the security system:
|
||||
|
||||
```go
|
||||
type SecurityContext interface {
|
||||
GetContext() context.Context
|
||||
GetUserID() (int, bool)
|
||||
GetSchema() string
|
||||
GetEntity() string
|
||||
GetModel() interface{}
|
||||
GetQuery() interface{}
|
||||
SetQuery(interface{})
|
||||
GetResult() interface{}
|
||||
SetResult(interface{})
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Examples:**
|
||||
- `restheadspec`: Adapts `restheadspec.HookContext` → `SecurityContext`
|
||||
- `funcspec`: Adapts `funcspec.HookContext` → `SecurityContext`
|
||||
- `resolvespec`: Adapts `resolvespec.HookContext` → `SecurityContext`
|
||||
|
||||
### UserContext
|
||||
Enhanced user context with complete user information:
|
||||
|
||||
@@ -197,7 +252,7 @@ rowSec := security.NewConfigRowSecurityProvider(templates, blocked)
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Example 1: Complete Database-Backed Security with Sessions
|
||||
### Example 1: Complete Database-Backed Security with Sessions (restheadspec)
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@@ -207,16 +262,20 @@ func main() {
|
||||
// db.Exec("CREATE TABLE users ...")
|
||||
// db.Exec("CREATE TABLE user_sessions ...")
|
||||
|
||||
// Create handler
|
||||
handler := restheadspec.NewHandlerWithGORM(db)
|
||||
|
||||
// Create providers
|
||||
// Create security providers
|
||||
auth := security.NewDatabaseAuthenticator(db) // Session-based auth
|
||||
colSec := security.NewDatabaseColumnSecurityProvider(db)
|
||||
rowSec := security.NewDatabaseRowSecurityProvider(db)
|
||||
|
||||
// Combine
|
||||
// Combine providers
|
||||
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
securityList := security.SetupSecurityProvider(handler, provider)
|
||||
securityList := security.NewSecurityList(provider)
|
||||
|
||||
// Register security hooks for this spec
|
||||
restheadspec.RegisterSecurityHooks(handler, securityList)
|
||||
|
||||
// Setup routes
|
||||
router := mux.NewRouter()
|
||||
@@ -309,14 +368,85 @@ func main() {
|
||||
colSec := security.NewConfigColumnSecurityProvider(columnRules)
|
||||
rowSec := security.NewConfigRowSecurityProvider(rowTemplates, nil)
|
||||
|
||||
// Combine providers and register hooks
|
||||
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
securityList := security.SetupSecurityProvider(handler, provider)
|
||||
securityList := security.NewSecurityList(provider)
|
||||
restheadspec.RegisterSecurityHooks(handler, securityList)
|
||||
|
||||
// Setup routes...
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Custom Provider
|
||||
### Example 3: FuncSpec Security (SQL Query API)
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/bitechdev/ResolveSpec/pkg/funcspec"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db := setupDatabase()
|
||||
|
||||
// Create funcspec handler
|
||||
handler := funcspec.NewHandler(db)
|
||||
|
||||
// Create security providers
|
||||
auth := security.NewJWTAuthenticator("secret-key", db)
|
||||
colSec := security.NewDatabaseColumnSecurityProvider(db)
|
||||
rowSec := security.NewDatabaseRowSecurityProvider(db)
|
||||
|
||||
// Combine providers
|
||||
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
securityList := security.NewSecurityList(provider)
|
||||
|
||||
// Register security hooks (audit logging)
|
||||
funcspec.RegisterSecurityHooks(handler, securityList)
|
||||
|
||||
// Note: funcspec operates on raw SQL queries, so row/column
|
||||
// security is limited. Security should be enforced at the
|
||||
// SQL function level or via database policies.
|
||||
|
||||
// Setup routes...
|
||||
}
|
||||
```
|
||||
|
||||
### Example 4: ResolveSpec Security (REST API)
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/bitechdev/ResolveSpec/pkg/resolvespec"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db := setupDatabase()
|
||||
registry := common.NewModelRegistry()
|
||||
|
||||
// Register models
|
||||
registry.RegisterModel("public.users", &User{})
|
||||
registry.RegisterModel("public.orders", &Order{})
|
||||
|
||||
// Create resolvespec handler
|
||||
handler := resolvespec.NewHandler(db, registry)
|
||||
|
||||
// Create security providers
|
||||
auth := security.NewDatabaseAuthenticator(db)
|
||||
colSec := security.NewDatabaseColumnSecurityProvider(db)
|
||||
rowSec := security.NewDatabaseRowSecurityProvider(db)
|
||||
|
||||
// Combine providers
|
||||
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
securityList := security.NewSecurityList(provider)
|
||||
|
||||
// Register security hooks for resolvespec
|
||||
resolvespec.RegisterSecurityHooks(handler, securityList)
|
||||
|
||||
// Setup routes...
|
||||
}
|
||||
```
|
||||
|
||||
### Example 5: Custom Provider
|
||||
|
||||
Implement your own provider for complete control:
|
||||
|
||||
@@ -345,9 +475,18 @@ func (p *MySecurityProvider) GetRowSecurity(ctx context.Context, userID int, sch
|
||||
// Your custom row security logic
|
||||
}
|
||||
|
||||
// Use it
|
||||
// Use it with any spec
|
||||
provider := &MySecurityProvider{db: db}
|
||||
securityList := security.SetupSecurityProvider(handler, provider)
|
||||
securityList := security.NewSecurityList(provider)
|
||||
|
||||
// Register with restheadspec
|
||||
restheadspec.RegisterSecurityHooks(restHandler, securityList)
|
||||
|
||||
// Or with funcspec
|
||||
funcspec.RegisterSecurityHooks(funcHandler, securityList)
|
||||
|
||||
// Or with resolvespec
|
||||
resolvespec.RegisterSecurityHooks(resolveHandler, securityList)
|
||||
```
|
||||
|
||||
## Security Features
|
||||
@@ -419,30 +558,45 @@ securityList := security.SetupSecurityProvider(handler, provider)
|
||||
```
|
||||
HTTP Request
|
||||
↓
|
||||
NewAuthMiddleware
|
||||
NewAuthMiddleware (security package)
|
||||
├─ Calls provider.Authenticate(request)
|
||||
└─ Adds UserContext to context
|
||||
↓
|
||||
SetSecurityMiddleware
|
||||
SetSecurityMiddleware (security package)
|
||||
└─ Adds SecurityList to context
|
||||
↓
|
||||
Handler.Handle()
|
||||
Spec Handler (restheadspec/funcspec/resolvespec)
|
||||
↓
|
||||
BeforeRead Hook
|
||||
├─ Calls provider.GetColumnSecurity()
|
||||
└─ Calls provider.GetRowSecurity()
|
||||
BeforeRead Hook (registered by spec)
|
||||
├─ Adapts spec's HookContext → SecurityContext
|
||||
├─ Calls security.LoadSecurityRules(secCtx, securityList)
|
||||
│ ├─ Calls provider.GetColumnSecurity()
|
||||
│ └─ Calls provider.GetRowSecurity()
|
||||
└─ Caches security rules
|
||||
↓
|
||||
BeforeScan Hook
|
||||
└─ Applies row security (adds WHERE clause)
|
||||
BeforeScan Hook (registered by spec)
|
||||
├─ Adapts spec's HookContext → SecurityContext
|
||||
├─ Calls security.ApplyRowSecurity(secCtx, securityList)
|
||||
└─ Applies row security (adds WHERE clause to query)
|
||||
↓
|
||||
Database Query (with security filters)
|
||||
↓
|
||||
AfterRead Hook
|
||||
└─ Applies column security (masks/hides fields)
|
||||
AfterRead Hook (registered by spec)
|
||||
├─ Adapts spec's HookContext → SecurityContext
|
||||
├─ Calls security.ApplyColumnSecurity(secCtx, securityList)
|
||||
├─ Applies column security (masks/hides fields)
|
||||
└─ Calls security.LogDataAccess(secCtx)
|
||||
↓
|
||||
HTTP Response (secured data)
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Security package is spec-agnostic and provides core logic
|
||||
- Each spec registers its own hooks that adapt to SecurityContext
|
||||
- Security rules are loaded once and cached for the request
|
||||
- Row security is applied to the query (database level)
|
||||
- Column security is applied to results (application level)
|
||||
|
||||
## Testing
|
||||
|
||||
The interface-based design makes testing straightforward:
|
||||
@@ -475,7 +629,9 @@ func TestMyHandler(t *testing.T) {
|
||||
}
|
||||
```
|
||||
|
||||
## Migration from Callbacks
|
||||
## Migration Guide
|
||||
|
||||
### From Old Callback System
|
||||
|
||||
If you're upgrading from the old callback-based system:
|
||||
|
||||
@@ -489,7 +645,7 @@ security.SetupSecurityProvider(handler, &security.GlobalSecurity)
|
||||
|
||||
**New:**
|
||||
```go
|
||||
// Wrap your functions in a provider
|
||||
// 1. Wrap your functions in a provider
|
||||
type MyProvider struct{}
|
||||
|
||||
func (p *MyProvider) Authenticate(r *http.Request) (*security.UserContext, error) {
|
||||
@@ -513,11 +669,34 @@ func (p *MyProvider) Logout(ctx context.Context, req security.LogoutRequest) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use it
|
||||
// 2. Create security list and register hooks
|
||||
provider := &MyProvider{}
|
||||
securityList := security.NewSecurityList(provider)
|
||||
|
||||
// 3. Register with your spec
|
||||
restheadspec.RegisterSecurityHooks(handler, securityList)
|
||||
```
|
||||
|
||||
### From Old SetupSecurityProvider API
|
||||
|
||||
If you're upgrading from the previous interface-based system:
|
||||
|
||||
**Old:**
|
||||
```go
|
||||
securityList := security.SetupSecurityProvider(handler, provider)
|
||||
```
|
||||
|
||||
**New:**
|
||||
```go
|
||||
securityList := security.NewSecurityList(provider)
|
||||
restheadspec.RegisterSecurityHooks(handler, securityList) // or funcspec/resolvespec
|
||||
```
|
||||
|
||||
The main changes:
|
||||
1. Security package no longer knows about specific spec types
|
||||
2. Each spec registers its own security hooks
|
||||
3. More flexible - same security provider works with all specs
|
||||
|
||||
## Documentation
|
||||
|
||||
| File | Description |
|
||||
|
||||
@@ -1,51 +1,43 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
|
||||
)
|
||||
|
||||
// RegisterSecurityHooks registers all security-related hooks with the handler
|
||||
func RegisterSecurityHooks(handler *restheadspec.Handler, securityList *SecurityList) {
|
||||
|
||||
// Hook 1: BeforeRead - Load security rules
|
||||
handler.Hooks().Register(restheadspec.BeforeRead, func(hookCtx *restheadspec.HookContext) error {
|
||||
return LoadSecurityRules(hookCtx, securityList)
|
||||
})
|
||||
|
||||
// Hook 2: BeforeScan - Apply row-level security filters
|
||||
handler.Hooks().Register(restheadspec.BeforeScan, func(hookCtx *restheadspec.HookContext) error {
|
||||
return ApplyRowSecurity(hookCtx, securityList)
|
||||
})
|
||||
|
||||
// Hook 3: AfterRead - Apply column-level security (masking)
|
||||
handler.Hooks().Register(restheadspec.AfterRead, func(hookCtx *restheadspec.HookContext) error {
|
||||
return ApplyColumnSecurity(hookCtx, securityList)
|
||||
})
|
||||
|
||||
// Hook 4 (Optional): Audit logging
|
||||
handler.Hooks().Register(restheadspec.AfterRead, LogDataAccess)
|
||||
// SecurityContext is a generic interface that any spec can implement to integrate with security features
|
||||
// This interface abstracts the common security context needs across different specs
|
||||
type SecurityContext interface {
|
||||
GetContext() context.Context
|
||||
GetUserID() (int, bool)
|
||||
GetSchema() string
|
||||
GetEntity() string
|
||||
GetModel() interface{}
|
||||
GetQuery() interface{}
|
||||
SetQuery(interface{})
|
||||
GetResult() interface{}
|
||||
SetResult(interface{})
|
||||
}
|
||||
|
||||
// LoadSecurityRules loads security configuration for the user and entity
|
||||
func LoadSecurityRules(hookCtx *restheadspec.HookContext, securityList *SecurityList) error {
|
||||
// loadSecurityRules loads security configuration for the user and entity (generic version)
|
||||
func loadSecurityRules(secCtx SecurityContext, securityList *SecurityList) error {
|
||||
// Extract user ID from context
|
||||
userID, ok := GetUserID(hookCtx.Context)
|
||||
userID, ok := secCtx.GetUserID()
|
||||
if !ok {
|
||||
logger.Warn("No user ID in context for security check")
|
||||
return fmt.Errorf("authentication required")
|
||||
return nil
|
||||
}
|
||||
|
||||
schema := hookCtx.Schema
|
||||
tablename := hookCtx.Entity
|
||||
schema := secCtx.GetSchema()
|
||||
tablename := secCtx.GetEntity()
|
||||
|
||||
logger.Debug("Loading security rules for user=%d, schema=%s, table=%s", userID, schema, tablename)
|
||||
|
||||
// Load column security rules using the provider
|
||||
err := securityList.LoadColumnSecurity(hookCtx.Context, userID, schema, tablename, false)
|
||||
err := securityList.LoadColumnSecurity(secCtx.GetContext(), userID, schema, tablename, false)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to load column security: %v", err)
|
||||
// Don't fail the request if no security rules exist
|
||||
@@ -53,7 +45,7 @@ func LoadSecurityRules(hookCtx *restheadspec.HookContext, securityList *Security
|
||||
}
|
||||
|
||||
// Load row security rules using the provider
|
||||
_, err = securityList.LoadRowSecurity(hookCtx.Context, userID, schema, tablename, false)
|
||||
_, err = securityList.LoadRowSecurity(secCtx.GetContext(), userID, schema, tablename, false)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to load row security: %v", err)
|
||||
// Don't fail the request if no security rules exist
|
||||
@@ -63,15 +55,15 @@ func LoadSecurityRules(hookCtx *restheadspec.HookContext, securityList *Security
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyRowSecurity applies row-level security filters to the query
|
||||
func ApplyRowSecurity(hookCtx *restheadspec.HookContext, securityList *SecurityList) error {
|
||||
userID, ok := GetUserID(hookCtx.Context)
|
||||
// applyRowSecurity applies row-level security filters to the query (generic version)
|
||||
func applyRowSecurity(secCtx SecurityContext, securityList *SecurityList) error {
|
||||
userID, ok := secCtx.GetUserID()
|
||||
if !ok {
|
||||
return nil // No user context, skip
|
||||
}
|
||||
|
||||
schema := hookCtx.Schema
|
||||
tablename := hookCtx.Entity
|
||||
schema := secCtx.GetSchema()
|
||||
tablename := secCtx.GetEntity()
|
||||
|
||||
// Get row security template
|
||||
rowSec, err := securityList.GetRowSecurityTemplate(userID, schema, tablename)
|
||||
@@ -89,8 +81,14 @@ func ApplyRowSecurity(hookCtx *restheadspec.HookContext, securityList *SecurityL
|
||||
|
||||
// If there's a security template, apply it as a WHERE clause
|
||||
if rowSec.Template != "" {
|
||||
model := secCtx.GetModel()
|
||||
if model == nil {
|
||||
logger.Debug("No model available for row security on %s.%s", schema, tablename)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get primary key name from model
|
||||
modelType := reflect.TypeOf(hookCtx.Model)
|
||||
modelType := reflect.TypeOf(model)
|
||||
if modelType.Kind() == reflect.Ptr {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
@@ -117,39 +115,45 @@ func ApplyRowSecurity(hookCtx *restheadspec.HookContext, securityList *SecurityL
|
||||
userID, schema, tablename, whereClause)
|
||||
|
||||
// Apply the WHERE clause to the query
|
||||
// The query is in hookCtx.Query
|
||||
if selectQuery, ok := hookCtx.Query.(interface {
|
||||
query := secCtx.GetQuery()
|
||||
if selectQuery, ok := query.(interface {
|
||||
Where(string, ...interface{}) interface{}
|
||||
}); ok {
|
||||
hookCtx.Query = selectQuery.Where(whereClause)
|
||||
secCtx.SetQuery(selectQuery.Where(whereClause))
|
||||
} else {
|
||||
logger.Error("Unable to apply WHERE clause - query doesn't support Where method")
|
||||
logger.Debug("Query doesn't support Where method, skipping row security")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyColumnSecurity applies column-level security (masking/hiding) to results
|
||||
func ApplyColumnSecurity(hookCtx *restheadspec.HookContext, securityList *SecurityList) error {
|
||||
userID, ok := GetUserID(hookCtx.Context)
|
||||
// applyColumnSecurity applies column-level security (masking/hiding) to results (generic version)
|
||||
func applyColumnSecurity(secCtx SecurityContext, securityList *SecurityList) error {
|
||||
userID, ok := secCtx.GetUserID()
|
||||
if !ok {
|
||||
return nil // No user context, skip
|
||||
}
|
||||
|
||||
schema := hookCtx.Schema
|
||||
tablename := hookCtx.Entity
|
||||
schema := secCtx.GetSchema()
|
||||
tablename := secCtx.GetEntity()
|
||||
|
||||
// Get result data
|
||||
result := hookCtx.Result
|
||||
result := secCtx.GetResult()
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Debug("Applying column security for user=%d, schema=%s, table=%s", userID, schema, tablename)
|
||||
|
||||
model := secCtx.GetModel()
|
||||
if model == nil {
|
||||
logger.Debug("No model available for column security on %s.%s", schema, tablename)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get model type
|
||||
modelType := reflect.TypeOf(hookCtx.Model)
|
||||
modelType := reflect.TypeOf(model)
|
||||
if modelType.Kind() == reflect.Ptr {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
@@ -169,37 +173,59 @@ func ApplyColumnSecurity(hookCtx *restheadspec.HookContext, securityList *Securi
|
||||
|
||||
// Update the result with masked data
|
||||
if maskedResult.IsValid() && maskedResult.CanInterface() {
|
||||
hookCtx.Result = maskedResult.Interface()
|
||||
secCtx.SetResult(maskedResult.Interface())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogDataAccess logs all data access for audit purposes
|
||||
func LogDataAccess(hookCtx *restheadspec.HookContext) error {
|
||||
userID, _ := GetUserID(hookCtx.Context)
|
||||
// logDataAccess logs all data access for audit purposes (generic version)
|
||||
func logDataAccess(secCtx SecurityContext) error {
|
||||
userID, _ := secCtx.GetUserID()
|
||||
|
||||
logger.Info("AUDIT: User %d accessed %s.%s with filters: %+v",
|
||||
logger.Info("AUDIT: User %d accessed %s.%s",
|
||||
userID,
|
||||
hookCtx.Schema,
|
||||
hookCtx.Entity,
|
||||
hookCtx.Options.Filters,
|
||||
secCtx.GetSchema(),
|
||||
secCtx.GetEntity(),
|
||||
)
|
||||
|
||||
// TODO: Write to audit log table or external audit service
|
||||
// auditLog := AuditLog{
|
||||
// UserID: userID,
|
||||
// Schema: hookCtx.Schema,
|
||||
// Entity: hookCtx.Entity,
|
||||
// Schema: secCtx.GetSchema(),
|
||||
// Entity: secCtx.GetEntity(),
|
||||
// Action: "READ",
|
||||
// Timestamp: time.Now(),
|
||||
// Filters: hookCtx.Options.Filters,
|
||||
// }
|
||||
// db.Create(&auditLog)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogDataAccess is a public wrapper for logDataAccess that accepts a SecurityContext
|
||||
// This allows other packages to use the audit logging functionality
|
||||
func LogDataAccess(secCtx SecurityContext) error {
|
||||
return logDataAccess(secCtx)
|
||||
}
|
||||
|
||||
// LoadSecurityRules is a public wrapper for loadSecurityRules that accepts a SecurityContext
|
||||
// This allows other packages to load security rules using the generic interface
|
||||
func LoadSecurityRules(secCtx SecurityContext, securityList *SecurityList) error {
|
||||
return loadSecurityRules(secCtx, securityList)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return applyRowSecurity(secCtx, securityList)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return applyColumnSecurity(secCtx, securityList)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
|
||||
@@ -243,3 +243,158 @@ func GetUserMeta(ctx context.Context) (map[string]any, bool) {
|
||||
meta, ok := ctx.Value(UserMetaKey).(map[string]any)
|
||||
return meta, ok
|
||||
}
|
||||
|
||||
// // Handler adapters for resolvespec/restheadspec compatibility
|
||||
// // These functions allow using NewAuthHandler and NewOptionalAuthHandler with custom handler abstractions
|
||||
|
||||
// // SpecHandlerAdapter is an interface for handler adapters that need authentication
|
||||
// // Implement this interface to create adapters for custom handler types
|
||||
// type SpecHandlerAdapter interface {
|
||||
// // AdaptToHTTPHandler converts the custom handler to a standard http.Handler
|
||||
// AdaptToHTTPHandler() http.Handler
|
||||
// }
|
||||
|
||||
// // ResolveSpecHandlerAdapter adapts a resolvespec/restheadspec handler method to http.Handler
|
||||
// type ResolveSpecHandlerAdapter struct {
|
||||
// // HandlerMethod is the method to call (e.g., handler.Handle, handler.HandleGet)
|
||||
// HandlerMethod func(w any, r any, params map[string]string)
|
||||
// // Params are the route parameters (e.g., {"schema": "public", "entity": "users"})
|
||||
// Params map[string]string
|
||||
// // RequestAdapter converts *http.Request to the custom Request interface
|
||||
// // Use router.NewHTTPRequest from pkg/common/adapters/router
|
||||
// RequestAdapter func(*http.Request) any
|
||||
// // ResponseAdapter converts http.ResponseWriter to the custom ResponseWriter interface
|
||||
// // Use router.NewHTTPResponseWriter from pkg/common/adapters/router
|
||||
// ResponseAdapter func(http.ResponseWriter) any
|
||||
// }
|
||||
|
||||
// // AdaptToHTTPHandler implements SpecHandlerAdapter
|
||||
// func (a *ResolveSpecHandlerAdapter) AdaptToHTTPHandler() http.Handler {
|
||||
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// req := a.RequestAdapter(r)
|
||||
// resp := a.ResponseAdapter(w)
|
||||
// a.HandlerMethod(resp, req, a.Params)
|
||||
// })
|
||||
// }
|
||||
|
||||
// // WrapSpecHandler wraps a spec handler adapter with authentication
|
||||
// // Use this to apply NewAuthHandler or NewOptionalAuthHandler to resolvespec/restheadspec handlers
|
||||
// //
|
||||
// // Example with required auth:
|
||||
// //
|
||||
// // adapter := &security.ResolveSpecHandlerAdapter{
|
||||
// // HandlerMethod: handler.Handle,
|
||||
// // Params: map[string]string{"schema": "public", "entity": "users"},
|
||||
// // RequestAdapter: func(r *http.Request) any { return router.NewHTTPRequest(r) },
|
||||
// // ResponseAdapter: func(w http.ResponseWriter) any { return router.NewHTTPResponseWriter(w) },
|
||||
// // }
|
||||
// // authHandler := security.WrapSpecHandler(securityList, adapter, false)
|
||||
// // muxRouter.Handle("/api/users", authHandler)
|
||||
// func WrapSpecHandler(securityList *SecurityList, adapter SpecHandlerAdapter, optional bool) http.Handler {
|
||||
// httpHandler := adapter.AdaptToHTTPHandler()
|
||||
// if optional {
|
||||
// return NewOptionalAuthHandler(securityList, httpHandler)
|
||||
// }
|
||||
// return NewAuthHandler(securityList, httpHandler)
|
||||
// }
|
||||
|
||||
// // MuxRouteBuilder helps build authenticated routes with Gorilla Mux
|
||||
// type MuxRouteBuilder struct {
|
||||
// securityList *SecurityList
|
||||
// requestAdapter func(*http.Request) any
|
||||
// responseAdapter func(http.ResponseWriter) any
|
||||
// paramExtractor func(*http.Request) map[string]string
|
||||
// }
|
||||
|
||||
// // NewMuxRouteBuilder creates a route builder for Gorilla Mux with standard router adapters
|
||||
// // Usage:
|
||||
// //
|
||||
// // builder := security.NewMuxRouteBuilder(securityList, router.NewHTTPRequest, router.NewHTTPResponseWriter)
|
||||
// func NewMuxRouteBuilder(
|
||||
// securityList *SecurityList,
|
||||
// requestAdapter func(*http.Request) any,
|
||||
// responseAdapter func(http.ResponseWriter) any,
|
||||
// ) *MuxRouteBuilder {
|
||||
// return &MuxRouteBuilder{
|
||||
// securityList: securityList,
|
||||
// requestAdapter: requestAdapter,
|
||||
// responseAdapter: responseAdapter,
|
||||
// paramExtractor: nil, // Will be set per route using mux.Vars
|
||||
// }
|
||||
// }
|
||||
|
||||
// // HandleAuth creates an authenticated route handler
|
||||
// // pattern: the route pattern (e.g., "/{schema}/{entity}")
|
||||
// // handler: the handler method to call (e.g., handler.Handle)
|
||||
// // optional: true for optional auth (guest fallback), false for required auth (401 on failure)
|
||||
// // methods: HTTP methods (e.g., "GET", "POST")
|
||||
// //
|
||||
// // Usage:
|
||||
// //
|
||||
// // builder.HandleAuth(router, "/{schema}/{entity}", handler.Handle, false, "POST")
|
||||
// func (b *MuxRouteBuilder) HandleAuth(
|
||||
// router interface {
|
||||
// HandleFunc(pattern string, f func(http.ResponseWriter, *http.Request)) interface{ Methods(...string) interface{} }
|
||||
// },
|
||||
// pattern string,
|
||||
// handlerMethod func(w any, r any, params map[string]string),
|
||||
// optional bool,
|
||||
// methods ...string,
|
||||
// ) {
|
||||
// router.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
|
||||
// // Extract params using the registered extractor or default to empty map
|
||||
// var params map[string]string
|
||||
// if b.paramExtractor != nil {
|
||||
// params = b.paramExtractor(r)
|
||||
// } else {
|
||||
// params = make(map[string]string)
|
||||
// }
|
||||
|
||||
// adapter := &ResolveSpecHandlerAdapter{
|
||||
// HandlerMethod: handlerMethod,
|
||||
// Params: params,
|
||||
// RequestAdapter: b.requestAdapter,
|
||||
// ResponseAdapter: b.responseAdapter,
|
||||
// }
|
||||
// authHandler := WrapSpecHandler(b.securityList, adapter, optional)
|
||||
// authHandler.ServeHTTP(w, r)
|
||||
// }).Methods(methods...)
|
||||
// }
|
||||
|
||||
// // SetParamExtractor sets a custom parameter extractor function
|
||||
// // For Gorilla Mux, you would use: builder.SetParamExtractor(mux.Vars)
|
||||
// func (b *MuxRouteBuilder) SetParamExtractor(extractor func(*http.Request) map[string]string) {
|
||||
// b.paramExtractor = extractor
|
||||
// }
|
||||
|
||||
// // SetupAuthenticatedSpecRoutes sets up all standard resolvespec/restheadspec routes with authentication
|
||||
// // This is a convenience function that sets up the common route patterns
|
||||
// //
|
||||
// // Usage:
|
||||
// //
|
||||
// // security.SetupAuthenticatedSpecRoutes(router, handler, securityList, router.NewHTTPRequest, router.NewHTTPResponseWriter, mux.Vars)
|
||||
// func SetupAuthenticatedSpecRoutes(
|
||||
// router interface {
|
||||
// HandleFunc(pattern string, f func(http.ResponseWriter, *http.Request)) interface{ Methods(...string) interface{} }
|
||||
// },
|
||||
// handler interface {
|
||||
// Handle(w any, r any, params map[string]string)
|
||||
// HandleGet(w any, r any, params map[string]string)
|
||||
// },
|
||||
// securityList *SecurityList,
|
||||
// requestAdapter func(*http.Request) any,
|
||||
// responseAdapter func(http.ResponseWriter) any,
|
||||
// paramExtractor func(*http.Request) map[string]string,
|
||||
// ) {
|
||||
// builder := NewMuxRouteBuilder(securityList, requestAdapter, responseAdapter)
|
||||
// builder.SetParamExtractor(paramExtractor)
|
||||
|
||||
// // POST /{schema}/{entity}
|
||||
// builder.HandleAuth(router, "/{schema}/{entity}", handler.Handle, false, "POST")
|
||||
|
||||
// // POST /{schema}/{entity}/{id}
|
||||
// builder.HandleAuth(router, "/{schema}/{entity}/{id}", handler.Handle, false, "POST")
|
||||
|
||||
// // GET /{schema}/{entity}
|
||||
// builder.HandleAuth(router, "/{schema}/{entity}", handler.HandleGet, false, "GET")
|
||||
// }
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
|
||||
)
|
||||
|
||||
// SetupSecurityProvider initializes and configures the security provider
|
||||
// This function creates a SecurityList with the given provider and registers hooks
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// // Create your security provider (use composite or single provider)
|
||||
// auth := security.NewJWTAuthenticator("your-secret-key", db)
|
||||
// colSec := security.NewDatabaseColumnSecurityProvider(db)
|
||||
// rowSec := security.NewDatabaseRowSecurityProvider(db)
|
||||
// provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
//
|
||||
// // Setup security with the provider
|
||||
// handler := restheadspec.NewHandlerWithGORM(db)
|
||||
// securityList := security.SetupSecurityProvider(handler, provider)
|
||||
//
|
||||
// // Apply middleware
|
||||
// router.Use(security.NewAuthMiddleware(securityList))
|
||||
// router.Use(security.SetSecurityMiddleware(securityList))
|
||||
func SetupSecurityProvider(handler *restheadspec.Handler, provider SecurityProvider) *SecurityList {
|
||||
if provider == nil {
|
||||
panic("security provider cannot be nil")
|
||||
}
|
||||
|
||||
// Create security list with the provider
|
||||
securityList := NewSecurityList(provider)
|
||||
|
||||
// Register all security hooks
|
||||
RegisterSecurityHooks(handler, securityList)
|
||||
|
||||
return securityList
|
||||
}
|
||||
|
||||
// Example 1: Complete Setup with Composite Provider and Database-Backed Security
|
||||
// ===============================================================================
|
||||
// Note: Security providers use *sql.DB, but restheadspec.Handler may use *gorm.DB
|
||||
// You can get *sql.DB from gorm.DB using: sqlDB, _ := gormDB.DB()
|
||||
|
||||
func ExampleDatabaseSecurity(gormDB interface{}, sqlDB *sql.DB) (http.Handler, error) {
|
||||
// Step 1: Create the ResolveSpec handler
|
||||
// handler := restheadspec.NewHandlerWithGORM(gormDB.(*gorm.DB))
|
||||
handler := &restheadspec.Handler{} // Placeholder - use your handler initialization
|
||||
|
||||
// Step 2: Register your models
|
||||
// handler.RegisterModel("public", "users", User{})
|
||||
// handler.RegisterModel("public", "orders", Order{})
|
||||
|
||||
// Step 3: Create security provider components (using sql.DB)
|
||||
auth := NewJWTAuthenticator("your-secret-key", sqlDB)
|
||||
colSec := NewDatabaseColumnSecurityProvider(sqlDB)
|
||||
rowSec := NewDatabaseRowSecurityProvider(sqlDB)
|
||||
|
||||
// Step 4: Combine into composite provider
|
||||
provider := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
|
||||
// Step 5: Setup security
|
||||
securityList := SetupSecurityProvider(handler, provider)
|
||||
|
||||
// Step 6: Create router and setup routes
|
||||
router := mux.NewRouter()
|
||||
restheadspec.SetupMuxRoutes(router, handler)
|
||||
|
||||
// Step 7: Apply middleware in correct order
|
||||
router.Use(NewAuthMiddleware(securityList))
|
||||
router.Use(SetSecurityMiddleware(securityList))
|
||||
|
||||
return router, nil
|
||||
}
|
||||
|
||||
// Example 2: Simple Header-Based Authentication
|
||||
// ==============================================
|
||||
|
||||
func ExampleHeaderAuthentication(gormDB interface{}, sqlDB *sql.DB) (*mux.Router, error) {
|
||||
// handler := restheadspec.NewHandlerWithGORM(gormDB.(*gorm.DB))
|
||||
handler := &restheadspec.Handler{} // Placeholder - use your handler initialization
|
||||
|
||||
// Use header-based auth with database security providers
|
||||
auth := NewHeaderAuthenticatorExample()
|
||||
colSec := NewDatabaseColumnSecurityProvider(sqlDB)
|
||||
rowSec := NewDatabaseRowSecurityProvider(sqlDB)
|
||||
|
||||
provider := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
securityList := SetupSecurityProvider(handler, provider)
|
||||
|
||||
router := mux.NewRouter()
|
||||
restheadspec.SetupMuxRoutes(router, handler)
|
||||
|
||||
router.Use(NewAuthMiddleware(securityList))
|
||||
router.Use(SetSecurityMiddleware(securityList))
|
||||
|
||||
return router, nil
|
||||
}
|
||||
|
||||
// Example 3: Config-Based Security (No Database for Security)
|
||||
// ===========================================================
|
||||
|
||||
func ExampleConfigSecurity(gormDB interface{}) (*mux.Router, error) {
|
||||
// handler := restheadspec.NewHandlerWithGORM(gormDB.(*gorm.DB))
|
||||
handler := &restheadspec.Handler{} // Placeholder - use your handler initialization
|
||||
|
||||
// Define column security rules in code
|
||||
columnRules := map[string][]ColumnSecurity{
|
||||
"public.employees": {
|
||||
{
|
||||
Schema: "public",
|
||||
Tablename: "employees",
|
||||
Path: []string{"ssn"},
|
||||
Accesstype: "mask",
|
||||
MaskStart: 5,
|
||||
MaskChar: "*",
|
||||
},
|
||||
{
|
||||
Schema: "public",
|
||||
Tablename: "employees",
|
||||
Path: []string{"salary"},
|
||||
Accesstype: "hide",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Define row security templates
|
||||
rowTemplates := map[string]string{
|
||||
"public.orders": "user_id = {UserID}",
|
||||
"public.documents": "user_id = {UserID} OR is_public = true",
|
||||
}
|
||||
|
||||
// Define blocked tables
|
||||
blockedTables := map[string]bool{
|
||||
"public.admin_logs": true,
|
||||
}
|
||||
|
||||
// Create providers
|
||||
auth := NewHeaderAuthenticatorExample()
|
||||
colSec := NewConfigColumnSecurityProvider(columnRules)
|
||||
rowSec := NewConfigRowSecurityProvider(rowTemplates, blockedTables)
|
||||
|
||||
provider := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
securityList := SetupSecurityProvider(handler, provider)
|
||||
|
||||
router := mux.NewRouter()
|
||||
restheadspec.SetupMuxRoutes(router, handler)
|
||||
|
||||
router.Use(NewAuthMiddleware(securityList))
|
||||
router.Use(SetSecurityMiddleware(securityList))
|
||||
|
||||
return router, nil
|
||||
}
|
||||
|
||||
// Example 4: Custom Security Provider
|
||||
// ====================================
|
||||
|
||||
// You can implement your own SecurityProvider by implementing all three interfaces
|
||||
type CustomSecurityProvider struct {
|
||||
// Your custom fields
|
||||
}
|
||||
|
||||
func (p *CustomSecurityProvider) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
|
||||
// Your custom login logic
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (p *CustomSecurityProvider) Logout(ctx context.Context, req LogoutRequest) error {
|
||||
// Your custom logout logic
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *CustomSecurityProvider) Authenticate(r *http.Request) (*UserContext, error) {
|
||||
// Your custom authentication logic
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (p *CustomSecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]ColumnSecurity, error) {
|
||||
// Your custom column security logic
|
||||
return []ColumnSecurity{}, nil
|
||||
}
|
||||
|
||||
func (p *CustomSecurityProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error) {
|
||||
// Your custom row security logic
|
||||
return RowSecurity{
|
||||
Schema: schema,
|
||||
Tablename: table,
|
||||
UserID: userID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Example 5: Adding Login/Logout Endpoints
|
||||
// =========================================
|
||||
|
||||
func SetupAuthRoutes(router *mux.Router, securityList *SecurityList) {
|
||||
// Login endpoint
|
||||
router.HandleFunc("/auth/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse login request
|
||||
var loginReq LoginRequest
|
||||
// json.NewDecoder(r.Body).Decode(&loginReq)
|
||||
|
||||
// Call provider's Login method
|
||||
resp, err := securityList.Provider().Login(r.Context(), loginReq)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Return token
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// json.NewEncoder(w).Encode(resp)
|
||||
fmt.Fprintf(w, `{"token": "%s", "expires_in": %d}`, resp.Token, resp.ExpiresIn)
|
||||
}).Methods("POST")
|
||||
|
||||
// Logout endpoint
|
||||
router.HandleFunc("/auth/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Extract token from header
|
||||
token := r.Header.Get("Authorization")
|
||||
|
||||
// Get user ID from context (if authenticated)
|
||||
userID, _ := GetUserID(r.Context())
|
||||
|
||||
// Call provider's Logout method
|
||||
err := securityList.Provider().Logout(r.Context(), LogoutRequest{
|
||||
Token: token,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, `{"success": true}`)
|
||||
}).Methods("POST")
|
||||
|
||||
// Optional: Token refresh endpoint
|
||||
router.HandleFunc("/auth/refresh", func(w http.ResponseWriter, r *http.Request) {
|
||||
refreshToken := r.Header.Get("X-Refresh-Token")
|
||||
|
||||
// Check if provider supports refresh
|
||||
if refreshable, ok := securityList.Provider().(Refreshable); ok {
|
||||
resp, err := refreshable.RefreshToken(r.Context(), refreshToken)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"token": "%s", "expires_in": %d}`, resp.Token, resp.ExpiresIn)
|
||||
} else {
|
||||
http.Error(w, "Token refresh not supported", http.StatusNotImplemented)
|
||||
}
|
||||
}).Methods("POST")
|
||||
}
|
||||
|
||||
// Example 6: Complete Server Setup
|
||||
// =================================
|
||||
|
||||
func CompleteServerExample(gormDB interface{}, sqlDB *sql.DB) http.Handler {
|
||||
// Create handler and register models
|
||||
// handler := restheadspec.NewHandlerWithGORM(gormDB.(*gorm.DB))
|
||||
handler := &restheadspec.Handler{} // Placeholder - use your handler initialization
|
||||
// handler.RegisterModel("public", "users", User{})
|
||||
|
||||
// Setup security (using sql.DB for security providers)
|
||||
auth := NewJWTAuthenticator("secret-key", sqlDB)
|
||||
colSec := NewDatabaseColumnSecurityProvider(sqlDB)
|
||||
rowSec := NewDatabaseRowSecurityProvider(sqlDB)
|
||||
provider := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
securityList := SetupSecurityProvider(handler, provider)
|
||||
|
||||
// Create router
|
||||
router := mux.NewRouter()
|
||||
|
||||
// Add auth routes (login/logout)
|
||||
SetupAuthRoutes(router, securityList)
|
||||
|
||||
// Add API routes with security middleware
|
||||
apiRouter := router.PathPrefix("/api").Subrouter()
|
||||
restheadspec.SetupMuxRoutes(apiRouter, handler)
|
||||
apiRouter.Use(NewAuthMiddleware(securityList))
|
||||
apiRouter.Use(SetSecurityMiddleware(securityList))
|
||||
|
||||
return router
|
||||
}
|
||||
Reference in New Issue
Block a user