ResolveSpec/pkg/security/README.md
2025-11-24 17:00:05 +02:00

18 KiB

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.