| .. | ||
| composite.go | ||
| database_schema.sql | ||
| examples.go | ||
| hooks.go | ||
| interfaces.go | ||
| middleware.go | ||
| provider.go | ||
| providers.go | ||
| QUICK_REFERENCE.md | ||
| README.md | ||
| setup_example.go | ||
ResolveSpec Security Provider
Type-safe, composable security system for ResolveSpec with support for authentication, column-level security (masking), and row-level security (filtering).
Features
- ✅ Interface-Based - Type-safe providers instead of callbacks
- ✅ Login/Logout Support - Built-in authentication lifecycle
- ✅ Composable - Mix and match different providers
- ✅ No Global State - Each handler has its own security configuration
- ✅ Testable - Easy to mock and test
- ✅ Extensible - Implement custom providers for your needs
- ✅ Stored Procedures - All database operations use PostgreSQL stored procedures for security and maintainability
Stored Procedure Architecture
All database-backed security providers use PostgreSQL stored procedures exclusively. No raw SQL queries are executed from Go code.
Benefits
- Security: Database logic is centralized and protected
- Maintainability: Update database logic without recompiling Go code
- Performance: Stored procedures are pre-compiled and optimized
- Testability: Test database logic independently
- Consistency: Standardized
resolvespec_*naming convention
Available Stored Procedures
| Procedure | Purpose | Used By |
|---|---|---|
resolvespec_login |
Session-based login | DatabaseAuthenticator |
resolvespec_logout |
Session invalidation | DatabaseAuthenticator |
resolvespec_session |
Session validation | DatabaseAuthenticator |
resolvespec_session_update |
Update session activity | DatabaseAuthenticator |
resolvespec_refresh_token |
Token refresh | DatabaseAuthenticator |
resolvespec_jwt_login |
JWT user validation | JWTAuthenticator |
resolvespec_jwt_logout |
JWT token blacklist | JWTAuthenticator |
resolvespec_column_security |
Load column rules | DatabaseColumnSecurityProvider |
resolvespec_row_security |
Load row templates | DatabaseRowSecurityProvider |
See database_schema.sql for complete stored procedure definitions and examples.
Quick Start
import (
"github.com/bitechdev/ResolveSpec/pkg/security"
"github.com/bitechdev/ResolveSpec/pkg/restheadspec"
)
// 1. Create security providers
auth := security.NewJWTAuthenticator("your-secret-key", db)
colSec := security.NewDatabaseColumnSecurityProvider(db)
rowSec := security.NewDatabaseRowSecurityProvider(db)
// 2. Combine providers
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
// 3. Setup security
handler := restheadspec.NewHandlerWithGORM(db)
securityList := security.SetupSecurityProvider(handler, provider)
// 4. Apply middleware
router := mux.NewRouter()
restheadspec.SetupMuxRoutes(router, handler)
router.Use(security.NewAuthMiddleware(securityList))
router.Use(security.SetSecurityMiddleware(securityList))
Architecture
Core Interfaces
The security system is built on three main interfaces:
1. Authenticator
Handles user authentication lifecycle:
type Authenticator interface {
Login(ctx context.Context, req LoginRequest) (*LoginResponse, error)
Logout(ctx context.Context, req LogoutRequest) error
Authenticate(r *http.Request) (*UserContext, error)
}
2. ColumnSecurityProvider
Manages column-level security (masking/hiding):
type ColumnSecurityProvider interface {
GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]ColumnSecurity, error)
}
3. RowSecurityProvider
Manages row-level security (WHERE clause filtering):
type RowSecurityProvider interface {
GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error)
}
SecurityProvider
The main interface that combines all three:
type SecurityProvider interface {
Authenticator
ColumnSecurityProvider
RowSecurityProvider
}
UserContext
Enhanced user context with complete user information:
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
}
Available Implementations
Authenticators
HeaderAuthenticator - Simple header-based authentication:
auth := security.NewHeaderAuthenticator()
// Expects: X-User-ID, X-User-Name, X-User-Level, etc.
DatabaseAuthenticator - Database session-based authentication (Recommended):
auth := security.NewDatabaseAuthenticator(db)
// Supports: Login, Logout, Session management, Token refresh
// All operations use stored procedures: resolvespec_login, resolvespec_logout,
// resolvespec_session, resolvespec_session_update, resolvespec_refresh_token
// Requires: users and user_sessions tables + stored procedures (see database_schema.sql)
JWTAuthenticator - JWT token authentication with login/logout:
auth := security.NewJWTAuthenticator("secret-key", db)
// Supports: Login, Logout, JWT token validation
// All operations use stored procedures: resolvespec_jwt_login, resolvespec_jwt_logout
// Note: Requires JWT library installation for token signing/verification
Column Security Providers
DatabaseColumnSecurityProvider - Loads rules from database:
colSec := security.NewDatabaseColumnSecurityProvider(db)
// Uses stored procedure: resolvespec_column_security
// Queries core.secaccess and core.hub_link tables
ConfigColumnSecurityProvider - Static configuration:
rules := map[string][]security.ColumnSecurity{
"public.employees": {
{Path: []string{"ssn"}, Accesstype: "mask", MaskStart: 5},
},
}
colSec := security.NewConfigColumnSecurityProvider(rules)
Row Security Providers
DatabaseRowSecurityProvider - Loads filters from database:
rowSec := security.NewDatabaseRowSecurityProvider(db)
// Uses stored procedure: resolvespec_row_security
ConfigRowSecurityProvider - Static templates:
templates := map[string]string{
"public.orders": "user_id = {UserID}",
}
blocked := map[string]bool{
"public.admin_logs": true,
}
rowSec := security.NewConfigRowSecurityProvider(templates, blocked)
Usage Examples
Example 1: Complete Database-Backed Security with Sessions
func main() {
db := setupDatabase()
// Run migrations (see database_schema.sql)
// db.Exec("CREATE TABLE users ...")
// db.Exec("CREATE TABLE user_sessions ...")
handler := restheadspec.NewHandlerWithGORM(db)
// Create providers
auth := security.NewDatabaseAuthenticator(db) // Session-based auth
colSec := security.NewDatabaseColumnSecurityProvider(db)
rowSec := security.NewDatabaseRowSecurityProvider(db)
// Combine
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
securityList := security.SetupSecurityProvider(handler, provider)
// Setup routes
router := mux.NewRouter()
// Add auth endpoints
router.HandleFunc("/auth/login", handleLogin(securityList)).Methods("POST")
router.HandleFunc("/auth/logout", handleLogout(securityList)).Methods("POST")
router.HandleFunc("/auth/refresh", handleRefresh(securityList)).Methods("POST")
// Setup API with security
apiRouter := router.PathPrefix("/api").Subrouter()
restheadspec.SetupMuxRoutes(apiRouter, handler)
apiRouter.Use(security.NewAuthMiddleware(securityList))
apiRouter.Use(security.SetSecurityMiddleware(securityList))
http.ListenAndServe(":8080", router)
}
func handleLogin(securityList *security.SecurityList) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req security.LoginRequest
json.NewDecoder(r.Body).Decode(&req)
// Add client info to claims
req.Claims = map[string]any{
"ip_address": r.RemoteAddr,
"user_agent": r.UserAgent(),
}
resp, err := securityList.Provider().Login(r.Context(), req)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
// Set session cookie (optional)
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: resp.Token,
Expires: time.Now().Add(24 * time.Hour),
HttpOnly: true,
Secure: true, // Use in production with HTTPS
SameSite: http.SameSiteStrictMode,
})
json.NewEncoder(w).Encode(resp)
}
}
func handleRefresh(securityList *security.SecurityList) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Refresh-Token")
if refreshable, ok := securityList.Provider().(security.Refreshable); ok {
resp, err := refreshable.RefreshToken(r.Context(), token)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
json.NewEncoder(w).Encode(resp)
} else {
http.Error(w, "Refresh not supported", http.StatusNotImplemented)
}
}
}
Example 2: Config-Based Security (No Database)
func main() {
db := setupDatabase()
handler := restheadspec.NewHandlerWithGORM(db)
// Static column security rules
columnRules := map[string][]security.ColumnSecurity{
"public.employees": {
{Path: []string{"ssn"}, Accesstype: "mask", MaskStart: 5},
{Path: []string{"salary"}, Accesstype: "hide"},
},
}
// Static row security templates
rowTemplates := map[string]string{
"public.orders": "user_id = {UserID}",
}
// Create providers
auth := security.NewHeaderAuthenticator()
colSec := security.NewConfigColumnSecurityProvider(columnRules)
rowSec := security.NewConfigRowSecurityProvider(rowTemplates, nil)
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
securityList := security.SetupSecurityProvider(handler, provider)
// Setup routes...
}
Example 3: Custom Provider
Implement your own provider for complete control:
type MySecurityProvider struct {
db *gorm.DB
}
func (p *MySecurityProvider) Login(ctx context.Context, req security.LoginRequest) (*security.LoginResponse, error) {
// Your custom login logic
}
func (p *MySecurityProvider) Logout(ctx context.Context, req security.LogoutRequest) error {
// Your custom logout logic
}
func (p *MySecurityProvider) Authenticate(r *http.Request) (*security.UserContext, error) {
// Your custom authentication logic
}
func (p *MySecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
// Your custom column security logic
}
func (p *MySecurityProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (security.RowSecurity, error) {
// Your custom row security logic
}
// Use it
provider := &MySecurityProvider{db: db}
securityList := security.SetupSecurityProvider(handler, provider)
Security Features
Column Security (Masking/Hiding)
Mask SSN (show last 4 digits):
{
Path: []string{"ssn"},
Accesstype: "mask",
MaskStart: 5,
MaskChar: "*",
}
// "123-45-6789" → "*****6789"
Hide entire field:
{
Path: []string{"salary"},
Accesstype: "hide",
}
// Field returns 0 or empty
Nested JSON field masking:
{
Path: []string{"address", "street"},
Accesstype: "mask",
MaskStart: 10,
}
Row Security (Filtering)
User isolation:
{
Template: "user_id = {UserID}",
}
// Users only see their own records
Tenant isolation:
{
Template: "tenant_id = {TenantID} AND user_id = {UserID}",
}
Block all access:
{
HasBlock: true,
}
// Completely blocks access to the table
Template variables:
{UserID}- Current user's ID{PrimaryKeyName}- Primary key column{TableName}- Table name{SchemaName}- Schema name
Request Flow
HTTP Request
↓
NewAuthMiddleware
├─ Calls provider.Authenticate(request)
└─ Adds UserContext to context
↓
SetSecurityMiddleware
└─ Adds SecurityList to context
↓
Handler.Handle()
↓
BeforeRead Hook
├─ Calls provider.GetColumnSecurity()
└─ Calls provider.GetRowSecurity()
↓
BeforeScan Hook
└─ Applies row security (adds WHERE clause)
↓
Database Query (with security filters)
↓
AfterRead Hook
└─ Applies column security (masks/hides fields)
↓
HTTP Response (secured data)
Testing
The interface-based design makes testing straightforward:
// Mock authenticator for tests
type MockAuthenticator struct {
UserToReturn *security.UserContext
ErrorToReturn error
}
func (m *MockAuthenticator) Authenticate(r *http.Request) (*security.UserContext, error) {
return m.UserToReturn, m.ErrorToReturn
}
// Use in tests
func TestMyHandler(t *testing.T) {
mockAuth := &MockAuthenticator{
UserToReturn: &security.UserContext{UserID: 123},
}
provider := security.NewCompositeSecurityProvider(
mockAuth,
&MockColumnSecurity{},
&MockRowSecurity{},
)
securityList := security.SetupSecurityProvider(handler, provider)
// ... test your handler
}
Migration from Callbacks
If you're upgrading from the old callback-based system:
Old:
security.GlobalSecurity.AuthenticateCallback = myAuthFunc
security.GlobalSecurity.LoadColumnSecurityCallback = myColSecFunc
security.GlobalSecurity.LoadRowSecurityCallback = myRowSecFunc
security.SetupSecurityProvider(handler, &security.GlobalSecurity)
New:
// Wrap your functions in a provider
type MyProvider struct{}
func (p *MyProvider) Authenticate(r *http.Request) (*security.UserContext, error) {
userID, roles, err := myAuthFunc(r)
return &security.UserContext{UserID: userID, Roles: strings.Split(roles, ",")}, err
}
func (p *MyProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
return myColSecFunc(userID, schema, table)
}
func (p *MyProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (security.RowSecurity, error) {
return myRowSecFunc(userID, schema, table)
}
func (p *MyProvider) Login(ctx context.Context, req security.LoginRequest) (*security.LoginResponse, error) {
return nil, fmt.Errorf("not implemented")
}
func (p *MyProvider) Logout(ctx context.Context, req security.LogoutRequest) error {
return nil
}
// Use it
provider := &MyProvider{}
securityList := security.SetupSecurityProvider(handler, provider)
Documentation
| File | Description |
|---|---|
| QUICK_REFERENCE.md | Quick reference guide with examples |
| INTERFACE_GUIDE.md | Complete implementation guide |
| examples.go | Working provider implementations |
| setup_example.go | 6 complete integration examples |
API Reference
Context Helpers
Get user information from request context:
userCtx, ok := security.GetUserContext(ctx)
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)
Optional Interfaces
Implement these for additional features:
Refreshable - Token refresh support:
type Refreshable interface {
RefreshToken(ctx context.Context, refreshToken string) (*LoginResponse, error)
}
Validatable - Token validation:
type Validatable interface {
ValidateToken(ctx context.Context, token string) (bool, error)
}
Cacheable - Cache management:
type Cacheable interface {
ClearCache(ctx context.Context, userID int, schema, table string) error
}
Benefits Over Callbacks
| Feature | Old (Callbacks) | New (Interfaces) |
|---|---|---|
| Type Safety | ❌ Callbacks can be nil | ✅ Compile-time verification |
| Global State | ❌ GlobalSecurity variable | ✅ Dependency injection |
| Testability | ⚠️ Need to set globals | ✅ Easy to mock |
| Composability | ❌ Single provider only | ✅ Mix and match |
| Login/Logout | ❌ Not supported | ✅ Built-in |
| Extensibility | ⚠️ Limited | ✅ Optional interfaces |
Common Patterns
Caching Security Rules
type CachedProvider struct {
inner security.ColumnSecurityProvider
cache *cache.Cache
}
func (p *CachedProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
key := fmt.Sprintf("%d:%s.%s", userID, schema, table)
if cached, found := p.cache.Get(key); found {
return cached.([]security.ColumnSecurity), nil
}
rules, err := p.inner.GetColumnSecurity(ctx, userID, schema, table)
if err == nil {
p.cache.Set(key, rules, cache.DefaultExpiration)
}
return rules, err
}
Role-Based Security
func (p *MyProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
userCtx, _ := security.GetUserContext(ctx)
if contains(userCtx.Roles, "admin") {
return []security.ColumnSecurity{}, nil // No restrictions
}
return loadRestrictionsForUser(userID, schema, table), nil
}
Multi-Tenant Isolation
func (p *MyProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (security.RowSecurity, error) {
tenantID := getUserTenant(userID)
return security.RowSecurity{
Template: fmt.Sprintf("tenant_id = %d AND user_id = {UserID}", tenantID),
}, nil
}
License
Part of the ResolveSpec project.