Compare commits

...

2 Commits

Author SHA1 Message Date
Hein
9c5c7689e9 More common handler interface 2025-12-02 15:45:24 +02:00
Hein
08050c960d Optional Authentication 2025-12-02 14:14:38 +02:00
16 changed files with 826 additions and 111 deletions

View File

@@ -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")

View 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
}
}
}
*/

View File

@@ -246,3 +246,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
}

View File

@@ -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 {

View File

@@ -23,6 +23,15 @@ func Init(dev bool) {
}
func UpdateLoggerPath(path string, dev bool) {
defaultConfig := zap.NewProductionConfig()
if dev {
defaultConfig = zap.NewDevelopmentConfig()
}
defaultConfig.OutputPaths = []string{path}
UpdateLogger(&defaultConfig)
}
func UpdateLogger(config *zap.Config) {
defaultConfig := zap.NewProductionConfig()
defaultConfig.OutputPaths = []string{"resolvespec.log"}

View File

@@ -34,6 +34,12 @@ 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
}
// 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()

View File

@@ -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

View File

@@ -38,6 +38,12 @@ 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 {

View File

@@ -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

View File

@@ -91,7 +91,8 @@ security.UserContext{
RemoteID: "remote_xyz", // Remote system ID
Roles: []string{"admin"}, // User roles
Email: "john@example.com", // User email
Claims: map[string]any{}, // Additional metadata
Claims: map[string]any{}, // Additional authentication claims
Meta: map[string]any{}, // Additional metadata (JSON-serializable)
}
```
@@ -621,6 +622,67 @@ func main() {
---
## Authentication Modes
```go
// Required authentication (default)
// Authentication must succeed or returns 401
router.Use(security.NewAuthMiddleware(securityList))
// Skip authentication for specific routes
// Always sets guest user context
func PublicRoute(w http.ResponseWriter, r *http.Request) {
ctx := security.SkipAuth(r.Context())
r = r.WithContext(ctx)
// Guest context will be set
}
// Optional authentication for specific routes
// Tries to authenticate, falls back to guest if it fails
func HomeRoute(w http.ResponseWriter, r *http.Request) {
ctx := security.OptionalAuth(r.Context())
r = r.WithContext(ctx)
userCtx, _ := security.GetUserContext(r.Context())
if userCtx.UserID == 0 {
// Guest user
} else {
// Authenticated user
}
}
```
**Comparison:**
- **Required**: Auth must succeed or return 401 (default)
- **SkipAuth**: Never tries to authenticate, always guest
- **OptionalAuth**: Tries to authenticate, guest on failure
---
## Standalone Handlers
```go
// NewAuthHandler - Required authentication (returns 401 on failure)
authHandler := security.NewAuthHandler(securityList, myHandler)
http.Handle("/api/protected", authHandler)
// NewOptionalAuthHandler - Optional authentication (guest on failure)
optionalHandler := security.NewOptionalAuthHandler(securityList, myHandler)
http.Handle("/home", optionalHandler)
// Example handler
func myHandler(w http.ResponseWriter, r *http.Request) {
userCtx, _ := security.GetUserContext(r.Context())
if userCtx.UserID == 0 {
// Guest user
} else {
// Authenticated user
}
}
```
---
## Context Helpers
```go
@@ -635,6 +697,7 @@ sessionID, ok := security.GetSessionID(ctx)
remoteID, ok := security.GetRemoteID(ctx)
roles, ok := security.GetUserRoles(ctx)
email, ok := security.GetUserEmail(ctx)
meta, ok := security.GetUserMeta(ctx)
```
---

View File

@@ -118,14 +118,15 @@ Enhanced user context with complete user information:
```go
type UserContext struct {
UserID int // User's unique ID
UserName string // Username
UserLevel int // User privilege level
SessionID string // Current session ID
RemoteID string // Remote system ID
Roles []string // User roles
Email string // User email
Claims map[string]any // Additional metadata
UserID int // User's unique ID
UserName string // Username
UserLevel int // User privilege level
SessionID string // Current session ID
RemoteID string // Remote system ID
Roles []string // User roles
Email string // User email
Claims map[string]any // Additional authentication claims
Meta map[string]any // Additional metadata (can hold any JSON-serializable values)
}
```
@@ -629,6 +630,142 @@ func (p *MyProvider) GetRowSecurity(ctx context.Context, userID int, schema, tab
}
```
## Middleware and Handler API
### NewAuthMiddleware
Standard middleware that authenticates all requests:
```go
router.Use(security.NewAuthMiddleware(securityList))
```
Routes can skip authentication using the `SkipAuth` helper:
```go
func PublicHandler(w http.ResponseWriter, r *http.Request) {
ctx := security.SkipAuth(r.Context())
// This route will bypass authentication
// A guest user context will be set instead
}
router.Handle("/public", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := security.SkipAuth(r.Context())
PublicHandler(w, r.WithContext(ctx))
}))
```
When authentication is skipped, a guest user context is automatically set:
- UserID: 0
- UserName: "guest"
- Roles: ["guest"]
- RemoteID: Request's remote address
Routes can use optional authentication with the `OptionalAuth` helper:
```go
func OptionalAuthHandler(w http.ResponseWriter, r *http.Request) {
ctx := security.OptionalAuth(r.Context())
r = r.WithContext(ctx)
// This route will try to authenticate
// If authentication succeeds, authenticated user context is set
// If authentication fails, guest user context is set instead
userCtx, _ := security.GetUserContext(r.Context())
if userCtx.UserID == 0 {
// Guest user
fmt.Fprintf(w, "Welcome, guest!")
} else {
// Authenticated user
fmt.Fprintf(w, "Welcome back, %s!", userCtx.UserName)
}
}
router.Handle("/home", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := security.OptionalAuth(r.Context())
OptionalAuthHandler(w, r.WithContext(ctx))
}))
```
**Authentication Modes Summary:**
- **Required (default)**: Authentication must succeed or returns 401
- **SkipAuth**: Bypasses authentication entirely, always sets guest context
- **OptionalAuth**: Tries authentication, falls back to guest context if it fails
### NewAuthHandler
Standalone authentication handler (without middleware wrapping):
```go
// Use when you need authentication logic without middleware
authHandler := security.NewAuthHandler(securityList, myHandler)
http.Handle("/api/protected", authHandler)
```
### NewOptionalAuthHandler
Standalone optional authentication handler that tries to authenticate but falls back to guest:
```go
// Use for routes that should work for both authenticated and guest users
optionalHandler := security.NewOptionalAuthHandler(securityList, myHandler)
http.Handle("/home", optionalHandler)
// Example handler that checks user context
func myHandler(w http.ResponseWriter, r *http.Request) {
userCtx, _ := security.GetUserContext(r.Context())
if userCtx.UserID == 0 {
fmt.Fprintf(w, "Welcome, guest!")
} else {
fmt.Fprintf(w, "Welcome back, %s!", userCtx.UserName)
}
}
```
### Helper Functions
Extract user information from context:
```go
// Get full user context
userCtx, ok := security.GetUserContext(ctx)
// Get specific fields
userID, ok := security.GetUserID(ctx)
userName, ok := security.GetUserName(ctx)
userLevel, ok := security.GetUserLevel(ctx)
sessionID, ok := security.GetSessionID(ctx)
remoteID, ok := security.GetRemoteID(ctx)
roles, ok := security.GetUserRoles(ctx)
email, ok := security.GetUserEmail(ctx)
meta, ok := security.GetUserMeta(ctx)
```
### Metadata Support
The `Meta` field in `UserContext` can hold any JSON-serializable values:
```go
// Set metadata during login
loginReq := security.LoginRequest{
Username: "user@example.com",
Password: "password",
Meta: map[string]any{
"department": "engineering",
"location": "US",
"preferences": map[string]any{
"theme": "dark",
},
},
}
// Access metadata in handlers
meta, ok := security.GetUserMeta(ctx)
if ok {
department := meta["department"].(string)
}
```
## License
Part of the ResolveSpec project.

View File

@@ -54,6 +54,8 @@ func (a *HeaderAuthenticatorExample) Authenticate(r *http.Request) (*UserContext
RemoteID: r.Header.Get("X-Remote-ID"),
Email: r.Header.Get("X-User-Email"),
Roles: parseRoles(r.Header.Get("X-User-Roles")),
Claims: make(map[string]any),
Meta: make(map[string]any),
}, nil
}
@@ -125,6 +127,8 @@ func (a *JWTAuthenticatorExample) Login(ctx context.Context, req LoginRequest) (
Email: user.Email,
UserLevel: user.UserLevel,
Roles: parseRoles(user.Roles),
Claims: req.Claims,
Meta: req.Meta,
},
ExpiresIn: int64(24 * time.Hour.Seconds()),
}, nil
@@ -242,6 +246,9 @@ func (a *DatabaseAuthenticatorExample) Login(ctx context.Context, req LoginReque
Email: user.Email,
UserLevel: user.UserLevel,
Roles: parseRoles(user.Roles),
SessionID: sessionToken,
Claims: req.Claims,
Meta: req.Meta,
},
ExpiresIn: int64(24 * time.Hour.Seconds()),
}, nil
@@ -320,6 +327,8 @@ func (a *DatabaseAuthenticatorExample) Authenticate(r *http.Request) (*UserConte
UserLevel: session.UserLevel,
SessionID: sessionToken,
Roles: parseRoles(session.Roles),
Claims: make(map[string]any),
Meta: make(map[string]any),
}, nil
}
@@ -370,9 +379,12 @@ func (a *DatabaseAuthenticatorExample) RefreshToken(ctx context.Context, refresh
return &LoginResponse{
Token: newSessionToken,
User: &UserContext{
UserID: session.UserID,
UserName: session.Username,
Email: session.Email,
UserID: session.UserID,
UserName: session.Username,
Email: session.Email,
SessionID: newSessionToken,
Claims: make(map[string]any),
Meta: make(map[string]any),
},
ExpiresIn: int64(24 * time.Hour.Seconds()),
}, nil

View File

@@ -7,35 +7,37 @@ import (
// UserContext holds authenticated user information
type UserContext struct {
UserID int
UserName string
UserLevel int
SessionID string
RemoteID string
Roles []string
Email string
Claims map[string]any
UserID int `json:"user_id"`
UserName string `json:"user_name"`
UserLevel int `json:"user_level"`
SessionID string `json:"session_id"`
RemoteID string `json:"remote_id"`
Roles []string `json:"roles"`
Email string `json:"email"`
Claims map[string]any `json:"claims"`
Meta map[string]any `json:"meta"` // Additional metadata that can hold any JSON-serializable values
}
// LoginRequest contains credentials for login
type LoginRequest struct {
Username string
Password string
Claims map[string]any // Additional login data
Username string `json:"username"`
Password string `json:"password"`
Claims map[string]any `json:"claims"` // Additional login data
Meta map[string]any `json:"meta"` // Additional metadata to be set on user context
}
// LoginResponse contains the result of a login attempt
type LoginResponse struct {
Token string
RefreshToken string
User *UserContext
ExpiresIn int64 // Token expiration in seconds
Token string `json:"token"`
RefreshToken string `json:"refresh_token"`
User *UserContext `json:"user"`
ExpiresIn int64 `json:"expires_in"` // Token expiration in seconds
}
// LogoutRequest contains information for logout
type LogoutRequest struct {
Token string
UserID int
Token string `json:"token"`
UserID int `json:"user_id"`
}
// Authenticator handles user authentication operations

View File

@@ -10,21 +10,145 @@ type contextKey string
const (
// Context keys for user information
UserIDKey contextKey = "user_id"
UserNameKey contextKey = "user_name"
UserLevelKey contextKey = "user_level"
SessionIDKey contextKey = "session_id"
RemoteIDKey contextKey = "remote_id"
UserRolesKey contextKey = "user_roles"
UserEmailKey contextKey = "user_email"
UserContextKey contextKey = "user_context"
UserIDKey contextKey = "user_id"
UserNameKey contextKey = "user_name"
UserLevelKey contextKey = "user_level"
SessionIDKey contextKey = "session_id"
RemoteIDKey contextKey = "remote_id"
UserRolesKey contextKey = "user_roles"
UserEmailKey contextKey = "user_email"
UserContextKey contextKey = "user_context"
UserMetaKey contextKey = "user_meta"
SkipAuthKey contextKey = "skip_auth"
OptionalAuthKey contextKey = "optional_auth"
)
// SkipAuth returns a context with skip auth flag set to true
// Use this to mark routes that should bypass authentication middleware
func SkipAuth(ctx context.Context) context.Context {
return context.WithValue(ctx, SkipAuthKey, true)
}
// OptionalAuth returns a context with optional auth flag set to true
// Use this to mark routes that should try to authenticate, but fall back to guest if authentication fails
func OptionalAuth(ctx context.Context) context.Context {
return context.WithValue(ctx, OptionalAuthKey, true)
}
// createGuestContext creates a guest user context for unauthenticated requests
func createGuestContext(r *http.Request) *UserContext {
return &UserContext{
UserID: 0,
UserName: "guest",
UserLevel: 0,
SessionID: "",
RemoteID: r.RemoteAddr,
Roles: []string{"guest"},
Email: "",
Claims: map[string]any{},
Meta: map[string]any{},
}
}
// setUserContext adds a user context to the request context
func setUserContext(r *http.Request, userCtx *UserContext) *http.Request {
ctx := r.Context()
ctx = context.WithValue(ctx, UserContextKey, userCtx)
ctx = context.WithValue(ctx, UserIDKey, userCtx.UserID)
ctx = context.WithValue(ctx, UserNameKey, userCtx.UserName)
ctx = context.WithValue(ctx, UserLevelKey, userCtx.UserLevel)
ctx = context.WithValue(ctx, SessionIDKey, userCtx.SessionID)
ctx = context.WithValue(ctx, RemoteIDKey, userCtx.RemoteID)
ctx = context.WithValue(ctx, UserRolesKey, userCtx.Roles)
if userCtx.Email != "" {
ctx = context.WithValue(ctx, UserEmailKey, userCtx.Email)
}
if len(userCtx.Meta) > 0 {
ctx = context.WithValue(ctx, UserMetaKey, userCtx.Meta)
}
return r.WithContext(ctx)
}
// authenticateRequest performs authentication and adds user context to the request
// This is the shared authentication logic used by both handler and middleware
func authenticateRequest(w http.ResponseWriter, r *http.Request, provider SecurityProvider) (*http.Request, bool) {
// Call the provider's Authenticate method
userCtx, err := provider.Authenticate(r)
if err != nil {
http.Error(w, "Authentication failed: "+err.Error(), http.StatusUnauthorized)
return nil, false
}
return setUserContext(r, userCtx), true
}
// NewAuthHandler creates an authentication handler that can be used standalone
// This handler performs authentication and returns 401 if authentication fails
// Use this when you need authentication logic without middleware wrapping
func NewAuthHandler(securityList *SecurityList, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get the security provider
provider := securityList.Provider()
if provider == nil {
http.Error(w, "Security provider not configured", http.StatusInternalServerError)
return
}
// Authenticate the request
authenticatedReq, ok := authenticateRequest(w, r, provider)
if !ok {
return // authenticateRequest already wrote the error response
}
// Continue with authenticated context
next.ServeHTTP(w, authenticatedReq)
})
}
// NewOptionalAuthHandler creates an optional authentication handler that can be used standalone
// This handler tries to authenticate but falls back to guest context if authentication fails
// Use this for routes that should show personalized content for authenticated users but still work for guests
func NewOptionalAuthHandler(securityList *SecurityList, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get the security provider
provider := securityList.Provider()
if provider == nil {
http.Error(w, "Security provider not configured", http.StatusInternalServerError)
return
}
// Try to authenticate
userCtx, err := provider.Authenticate(r)
if err != nil {
// Authentication failed - set guest context and continue
guestCtx := createGuestContext(r)
next.ServeHTTP(w, setUserContext(r, guestCtx))
return
}
// Authentication succeeded - set user context
next.ServeHTTP(w, setUserContext(r, userCtx))
})
}
// NewAuthMiddleware creates an authentication middleware with the given security list
// 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 use optional authentication by setting OptionalAuthKey context value (use OptionalAuth helper)
// When authentication is skipped or fails with optional auth, a guest user context is set instead
func NewAuthMiddleware(securityList *SecurityList) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if this route should skip authentication
if skip, ok := r.Context().Value(SkipAuthKey).(bool); ok && skip {
// Set guest user context for skipped routes
guestCtx := createGuestContext(r)
next.ServeHTTP(w, setUserContext(r, guestCtx))
return
}
// Get the security provider
provider := securityList.Provider()
if provider == nil {
@@ -32,31 +156,25 @@ func NewAuthMiddleware(securityList *SecurityList) func(http.Handler) http.Handl
return
}
// Call the provider's Authenticate method
// Check if this route has optional authentication
optional, _ := r.Context().Value(OptionalAuthKey).(bool)
// Try to authenticate
userCtx, err := provider.Authenticate(r)
if err != nil {
if optional {
// Optional auth failed - set guest context and continue
guestCtx := createGuestContext(r)
next.ServeHTTP(w, setUserContext(r, guestCtx))
return
}
// Required auth failed - return error
http.Error(w, "Authentication failed: "+err.Error(), http.StatusUnauthorized)
return
}
// Add user information to context
ctx := r.Context()
ctx = context.WithValue(ctx, UserContextKey, userCtx)
ctx = context.WithValue(ctx, UserIDKey, userCtx.UserID)
ctx = context.WithValue(ctx, UserNameKey, userCtx.UserName)
ctx = context.WithValue(ctx, UserLevelKey, userCtx.UserLevel)
ctx = context.WithValue(ctx, SessionIDKey, userCtx.SessionID)
ctx = context.WithValue(ctx, RemoteIDKey, userCtx.RemoteID)
if len(userCtx.Roles) > 0 {
ctx = context.WithValue(ctx, UserRolesKey, userCtx.Roles)
}
if userCtx.Email != "" {
ctx = context.WithValue(ctx, UserEmailKey, userCtx.Email)
}
// Continue with authenticated context
next.ServeHTTP(w, r.WithContext(ctx))
// Authentication succeeded - set user context
next.ServeHTTP(w, setUserContext(r, userCtx))
})
}
}
@@ -119,3 +237,164 @@ func GetUserEmail(ctx context.Context) (string, bool) {
email, ok := ctx.Value(UserEmailKey).(string)
return email, ok
}
// GetUserMeta extracts user metadata from context
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")
// }

View File

@@ -15,26 +15,26 @@ import (
)
type ColumnSecurity struct {
Schema string
Tablename string
Path []string
ExtraFilters map[string]string
UserID int
Accesstype string `json:"accesstype"`
MaskStart int
MaskEnd int
MaskInvert bool
MaskChar string
Control string `json:"control"`
ID int `json:"id"`
Schema string `json:"schema"`
Tablename string `json:"tablename"`
Path []string `json:"path"`
ExtraFilters map[string]string `json:"extra_filters"`
UserID int `json:"user_id"`
Accesstype string `json:"accesstype"`
MaskStart int `json:"mask_start"`
MaskEnd int `json:"mask_end"`
MaskInvert bool `json:"mask_invert"`
MaskChar string `json:"mask_char"`
Control string `json:"control"`
ID int `json:"id"`
}
type RowSecurity struct {
Schema string
Tablename string
Template string
HasBlock bool
UserID int
Schema string `json:"schema"`
Tablename string `json:"tablename"`
Template string `json:"template"`
HasBlock bool `json:"has_block"`
UserID int `json:"user_id"`
}
func (m *RowSecurity) GetTemplate(pPrimaryKeyName string, pModelType reflect.Type) string {

View File

@@ -68,12 +68,14 @@ func ExampleDatabaseSecurity(gormDB interface{}, sqlDB *sql.DB) (http.Handler, e
// Step 5: Setup security
securityList := SetupSecurityProvider(handler, provider)
// Step 6: Create router and setup routes
// Step 6: Create router and setup routes with authentication
router := mux.NewRouter()
restheadspec.SetupMuxRoutes(router, handler)
authMiddleware := func(h http.Handler) http.Handler {
return NewAuthHandler(securityList, h)
}
restheadspec.SetupMuxRoutes(router, handler, authMiddleware)
// Step 7: Apply middleware in correct order
router.Use(NewAuthMiddleware(securityList))
// Step 7: Apply additional security middleware
router.Use(SetSecurityMiddleware(securityList))
return router, nil
@@ -95,9 +97,11 @@ func ExampleHeaderAuthentication(gormDB interface{}, sqlDB *sql.DB) (*mux.Router
securityList := SetupSecurityProvider(handler, provider)
router := mux.NewRouter()
restheadspec.SetupMuxRoutes(router, handler)
authMiddleware := func(h http.Handler) http.Handler {
return NewAuthHandler(securityList, h)
}
restheadspec.SetupMuxRoutes(router, handler, authMiddleware)
router.Use(NewAuthMiddleware(securityList))
router.Use(SetSecurityMiddleware(securityList))
return router, nil
@@ -150,9 +154,11 @@ func ExampleConfigSecurity(gormDB interface{}) (*mux.Router, error) {
securityList := SetupSecurityProvider(handler, provider)
router := mux.NewRouter()
restheadspec.SetupMuxRoutes(router, handler)
authMiddleware := func(h http.Handler) http.Handler {
return NewAuthHandler(securityList, h)
}
restheadspec.SetupMuxRoutes(router, handler, authMiddleware)
router.Use(NewAuthMiddleware(securityList))
router.Use(SetSecurityMiddleware(securityList))
return router, nil
@@ -282,10 +288,12 @@ func CompleteServerExample(gormDB interface{}, sqlDB *sql.DB) http.Handler {
// Add auth routes (login/logout)
SetupAuthRoutes(router, securityList)
// Add API routes with security middleware
// Add API routes with authentication
apiRouter := router.PathPrefix("/api").Subrouter()
restheadspec.SetupMuxRoutes(apiRouter, handler)
apiRouter.Use(NewAuthMiddleware(securityList))
authMiddleware := func(h http.Handler) http.Handler {
return NewAuthHandler(securityList, h)
}
restheadspec.SetupMuxRoutes(apiRouter, handler, authMiddleware)
apiRouter.Use(SetSecurityMiddleware(securityList))
return router