ResolveSpec/pkg/security/CALLBACKS_GUIDE.md

17 KiB

Security Provider Callbacks Guide

Overview

The ResolveSpec security provider uses a callback-based architecture that requires you to implement three functions:

  1. AuthenticateCallback - Extract user credentials from HTTP requests
  2. LoadColumnSecurityCallback - Load column security rules for masking/hiding
  3. LoadRowSecurityCallback - Load row security filters (WHERE clauses)

This design allows you to integrate the security provider with any authentication system and database schema.


Why Callbacks?

The callback-based design provides:

Flexibility - Works with any auth system (JWT, session, OAuth, custom) Database Agnostic - No assumptions about your security table schema Testability - Easy to mock for unit tests Extensibility - Add custom logic without modifying core code


Quick Start

Step 1: Implement the Three Callbacks

package main

import (
    "fmt"
    "net/http"
    "github.com/bitechdev/ResolveSpec/pkg/security"
)

// 1. Authentication: Extract user from request
func myAuthFunction(r *http.Request) (userID int, roles string, err error) {
    // Your auth logic here (JWT, session, header, etc.)
    token := r.Header.Get("Authorization")
    userID, roles, err = validateToken(token)
    return userID, roles, err
}

// 2. Column Security: Load column masking rules
func myLoadColumnSecurity(userID int, schema, tablename string) ([]security.ColumnSecurity, error) {
    // Your database query or config lookup here
    return loadColumnRulesFromDatabase(userID, schema, tablename)
}

// 3. Row Security: Load row filtering rules
func myLoadRowSecurity(userID int, schema, tablename string) (security.RowSecurity, error) {
    // Your database query or config lookup here
    return loadRowRulesFromDatabase(userID, schema, tablename)
}

Step 2: Configure the Callbacks

func main() {
    db := setupDatabase()
    handler := restheadspec.NewHandlerWithGORM(db)

    // Configure callbacks BEFORE SetupSecurityProvider
    security.GlobalSecurity.AuthenticateCallback = myAuthFunction
    security.GlobalSecurity.LoadColumnSecurityCallback = myLoadColumnSecurity
    security.GlobalSecurity.LoadRowSecurityCallback = myLoadRowSecurity

    // Setup security provider (validates callbacks are set)
    if err := security.SetupSecurityProvider(handler, &security.GlobalSecurity); err != nil {
        log.Fatal(err) // Fails if callbacks not configured
    }

    // Apply middleware
    router := mux.NewRouter()
    restheadspec.SetupMuxRoutes(router, handler)
    router.Use(mux.MiddlewareFunc(security.AuthMiddleware))
    router.Use(mux.MiddlewareFunc(security.SetSecurityMiddleware))

    http.ListenAndServe(":8080", router)
}

Callback 1: AuthenticateCallback

Function Signature

func(r *http.Request) (userID int, roles string, err error)

Parameters

  • r *http.Request - The incoming HTTP request

Returns

  • userID int - The authenticated user's ID
  • roles string - User's roles (comma-separated, e.g., "admin,manager")
  • err error - Return error to reject the request (HTTP 401)

Example Implementations

Simple Header-Based Auth

func authenticateFromHeader(r *http.Request) (int, string, error) {
    userIDStr := r.Header.Get("X-User-ID")
    if userIDStr == "" {
        return 0, "", fmt.Errorf("X-User-ID header required")
    }

    userID, err := strconv.Atoi(userIDStr)
    if err != nil {
        return 0, "", fmt.Errorf("invalid user ID")
    }

    roles := r.Header.Get("X-User-Roles") // Optional
    return userID, roles, nil
}

JWT Token Auth

import "github.com/golang-jwt/jwt/v5"

func authenticateFromJWT(r *http.Request) (int, string, error) {
    authHeader := r.Header.Get("Authorization")
    tokenString := strings.TrimPrefix(authHeader, "Bearer ")

    token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        return []byte(os.Getenv("JWT_SECRET")), nil
    })

    if err != nil || !token.Valid {
        return 0, "", fmt.Errorf("invalid token")
    }

    claims := token.Claims.(jwt.MapClaims)
    userID := int(claims["user_id"].(float64))
    roles := claims["roles"].(string)

    return userID, roles, nil
}
func authenticateFromSession(r *http.Request) (int, string, error) {
    cookie, err := r.Cookie("session_id")
    if err != nil {
        return 0, "", fmt.Errorf("no session cookie")
    }

    session, err := sessionStore.Get(cookie.Value)
    if err != nil {
        return 0, "", fmt.Errorf("invalid session")
    }

    return session.UserID, session.Roles, nil
}

Callback 2: LoadColumnSecurityCallback

Function Signature

func(pUserID int, pSchema, pTablename string) ([]ColumnSecurity, error)

Parameters

  • pUserID int - The authenticated user's ID
  • pSchema string - Database schema (e.g., "public")
  • pTablename string - Table name (e.g., "employees")

Returns

  • []ColumnSecurity - List of column security rules
  • error - Return error if loading fails

ColumnSecurity Structure

type ColumnSecurity struct {
    Schema       string   // "public"
    Tablename    string   // "employees"
    Path         []string // ["ssn"] or ["address", "street"]
    Accesstype   string   // "mask" or "hide"

    // Masking configuration (for Accesstype = "mask")
    MaskStart    int      // Mask first N characters
    MaskEnd      int      // Mask last N characters
    MaskInvert   bool     // true = mask middle, false = mask edges
    MaskChar     string   // Character to use for masking (default "*")

    // Optional fields
    ExtraFilters map[string]string
    Control      string
    ID           int
    UserID       int
}

Example Implementations

Load from Database

func loadColumnSecurityFromDB(userID int, schema, tablename string) ([]security.ColumnSecurity, error) {
    var rules []security.ColumnSecurity

    query := `
        SELECT control, accesstype, jsonvalue
        FROM core.secacces
        WHERE rid_hub IN (
            SELECT rid_hub_parent FROM core.hub_link
            WHERE rid_hub_child = ? AND parent_hubtype = 'secgroup'
        )
        AND control ILIKE ?
    `

    rows, err := db.Query(query, userID, fmt.Sprintf("%s.%s%%", schema, tablename))
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    for rows.Next() {
        var control, accesstype, jsonValue string
        rows.Scan(&control, &accesstype, &jsonValue)

        // Parse control: "schema.table.column"
        parts := strings.Split(control, ".")
        if len(parts) < 3 {
            continue
        }

        rule := security.ColumnSecurity{
            Schema:     schema,
            Tablename:  tablename,
            Path:       parts[2:],
            Accesstype: accesstype,
        }

        // Parse JSON configuration
        var config map[string]interface{}
        json.Unmarshal([]byte(jsonValue), &config)
        if start, ok := config["start"].(float64); ok {
            rule.MaskStart = int(start)
        }
        if end, ok := config["end"].(float64); ok {
            rule.MaskEnd = int(end)
        }
        if char, ok := config["char"].(string); ok {
            rule.MaskChar = char
        }

        rules = append(rules, rule)
    }

    return rules, nil
}

Load from Static Config

func loadColumnSecurityFromConfig(userID int, schema, tablename string) ([]security.ColumnSecurity, error) {
    // Define security rules in code
    allRules := map[string][]security.ColumnSecurity{
        "public.employees": {
            {
                Schema:     "public",
                Tablename:  "employees",
                Path:       []string{"ssn"},
                Accesstype: "mask",
                MaskStart:  5,
                MaskChar:   "*",
            },
            {
                Schema:     "public",
                Tablename:  "employees",
                Path:       []string{"salary"},
                Accesstype: "hide",
            },
        },
    }

    key := fmt.Sprintf("%s.%s", schema, tablename)
    rules, ok := allRules[key]
    if !ok {
        return []security.ColumnSecurity{}, nil // No rules
    }

    return rules, nil
}

Column Security Examples

Mask SSN (show last 4 digits):

ColumnSecurity{
    Path:       []string{"ssn"},
    Accesstype: "mask",
    MaskStart:  5,      // Mask first 5 characters
    MaskEnd:    0,      // Keep last 4 visible
    MaskChar:   "*",
}
// Result: "123-45-6789" → "*****6789"

Hide entire field:

ColumnSecurity{
    Path:       []string{"salary"},
    Accesstype: "hide",
}
// Result: salary field returns 0 or empty

Mask credit card (show last 4 digits):

ColumnSecurity{
    Path:       []string{"credit_card"},
    Accesstype: "mask",
    MaskStart:  12,
    MaskChar:   "*",
}
// Result: "1234-5678-9012-3456" → "************3456"

Callback 3: LoadRowSecurityCallback

Function Signature

func(pUserID int, pSchema, pTablename string) (RowSecurity, error)

Parameters

  • pUserID int - The authenticated user's ID
  • pSchema string - Database schema
  • pTablename string - Table name

Returns

  • RowSecurity - Row security configuration
  • error - Return error if loading fails

RowSecurity Structure

type RowSecurity struct {
    Schema    string // "public"
    Tablename string // "orders"
    UserID    int    // Current user ID
    Template  string // WHERE clause template (e.g., "user_id = {UserID}")
    HasBlock  bool   // If true, block ALL access to this table
}

Template Variables

You can use these placeholders in the Template string:

  • {UserID} - Current user's ID
  • {PrimaryKeyName} - Primary key column name
  • {TableName} - Table name
  • {SchemaName} - Schema name

Example Implementations

Load from Database Function

func loadRowSecurityFromDB(userID int, schema, tablename string) (security.RowSecurity, error) {
    var record security.RowSecurity

    query := `
        SELECT p_template, p_block
        FROM core.api_sec_rowtemplate(?, ?, ?)
    `

    row := db.QueryRow(query, schema, tablename, userID)
    err := row.Scan(&record.Template, &record.HasBlock)
    if err != nil {
        return security.RowSecurity{}, err
    }

    record.Schema = schema
    record.Tablename = tablename
    record.UserID = userID

    return record, nil
}

Load from Static Config

func loadRowSecurityFromConfig(userID int, schema, tablename string) (security.RowSecurity, error) {
    key := fmt.Sprintf("%s.%s", schema, tablename)

    // Define templates for each table
    templates := map[string]string{
        "public.orders":    "user_id = {UserID}",
        "public.documents": "user_id = {UserID} OR is_public = true",
    }

    // Define blocked tables
    blocked := map[string]bool{
        "public.admin_logs": true,
    }

    if blocked[key] {
        return security.RowSecurity{
            Schema:    schema,
            Tablename: tablename,
            UserID:    userID,
            HasBlock:  true,
        }, nil
    }

    template, ok := templates[key]
    if !ok {
        // No row security - allow all rows
        return security.RowSecurity{
            Schema:    schema,
            Tablename: tablename,
            UserID:    userID,
            Template:  "",
            HasBlock:  false,
        }, nil
    }

    return security.RowSecurity{
        Schema:    schema,
        Tablename: tablename,
        UserID:    userID,
        Template:  template,
        HasBlock:  false,
    }, nil
}

Row Security Examples

Users see only their own records:

RowSecurity{
    Template: "user_id = {UserID}",
}
// Query: SELECT * FROM orders WHERE user_id = 123

Users see their records OR public records:

RowSecurity{
    Template: "user_id = {UserID} OR is_public = true",
}

Complex filter with subquery:

RowSecurity{
    Template: "department_id IN (SELECT department_id FROM user_departments WHERE user_id = {UserID})",
}

Block all access:

RowSecurity{
    HasBlock: true,
}
// All queries to this table will be rejected

Complete Integration Example

package main

import (
    "fmt"
    "log"
    "net/http"
    "strconv"

    "github.com/bitechdev/ResolveSpec/pkg/restheadspec"
    "github.com/bitechdev/ResolveSpec/pkg/security"
    "github.com/gorilla/mux"
    "gorm.io/gorm"
)

func main() {
    db := setupDatabase()
    handler := restheadspec.NewHandlerWithGORM(db)
    handler.RegisterModel("public", "orders", Order{})

    // ===== CONFIGURE CALLBACKS =====
    security.GlobalSecurity.AuthenticateCallback = authenticateUser
    security.GlobalSecurity.LoadColumnSecurityCallback = loadColumnSec
    security.GlobalSecurity.LoadRowSecurityCallback = loadRowSec

    // ===== SETUP SECURITY =====
    if err := security.SetupSecurityProvider(handler, &security.GlobalSecurity); err != nil {
        log.Fatal("Security setup failed:", err)
    }

    // ===== SETUP ROUTES =====
    router := mux.NewRouter()
    restheadspec.SetupMuxRoutes(router, handler)
    router.Use(mux.MiddlewareFunc(security.AuthMiddleware))
    router.Use(mux.MiddlewareFunc(security.SetSecurityMiddleware))

    log.Println("Server starting on :8080")
    http.ListenAndServe(":8080", router)
}

// Callback implementations
func authenticateUser(r *http.Request) (int, string, error) {
    userIDStr := r.Header.Get("X-User-ID")
    if userIDStr == "" {
        return 0, "", fmt.Errorf("authentication required")
    }
    userID, err := strconv.Atoi(userIDStr)
    return userID, "", err
}

func loadColumnSec(userID int, schema, table string) ([]security.ColumnSecurity, error) {
    // Your implementation here
    return []security.ColumnSecurity{}, nil
}

func loadRowSec(userID int, schema, table string) (security.RowSecurity, error) {
    return security.RowSecurity{
        Schema:    schema,
        Tablename: table,
        UserID:    userID,
        Template:  "user_id = " + strconv.Itoa(userID),
    }, nil
}

Testing Your Callbacks

Unit Test Example

func TestAuthCallback(t *testing.T) {
    req := httptest.NewRequest("GET", "/api/orders", nil)
    req.Header.Set("X-User-ID", "123")

    userID, roles, err := myAuthFunction(req)

    assert.Nil(t, err)
    assert.Equal(t, 123, userID)
}

func TestColumnSecurityCallback(t *testing.T) {
    rules, err := myLoadColumnSecurity(123, "public", "employees")

    assert.Nil(t, err)
    assert.Greater(t, len(rules), 0)
    assert.Equal(t, "mask", rules[0].Accesstype)
}

Common Patterns

Pattern 1: Role-Based Security

func loadColumnSec(userID int, schema, table string) ([]security.ColumnSecurity, error) {
    roles := getUserRoles(userID)

    if contains(roles, "admin") {
        // Admins see everything
        return []security.ColumnSecurity{}, nil
    }

    // Non-admins have restrictions
    return []security.ColumnSecurity{
        {Path: []string{"ssn"}, Accesstype: "mask"},
    }, nil
}

Pattern 2: Tenant Isolation

func loadRowSec(userID int, schema, table string) (security.RowSecurity, error) {
    tenantID := getUserTenant(userID)

    return security.RowSecurity{
        Template: fmt.Sprintf("tenant_id = %d", tenantID),
    }, nil
}

Pattern 3: Caching Security Rules

var securityCache = cache.New(5*time.Minute, 10*time.Minute)

func loadColumnSec(userID int, schema, table string) ([]security.ColumnSecurity, error) {
    cacheKey := fmt.Sprintf("%d:%s.%s", userID, schema, table)

    if cached, found := securityCache.Get(cacheKey); found {
        return cached.([]security.ColumnSecurity), nil
    }

    rules := loadFromDatabase(userID, schema, table)
    securityCache.Set(cacheKey, rules, cache.DefaultExpiration)

    return rules, nil
}

Troubleshooting

Error: "AuthenticateCallback not set"

Solution: Configure all three callbacks before calling SetupSecurityProvider:

security.GlobalSecurity.AuthenticateCallback = myAuthFunc
security.GlobalSecurity.LoadColumnSecurityCallback = myColSecFunc
security.GlobalSecurity.LoadRowSecurityCallback = myRowSecFunc

Error: "Authentication failed"

Solution: Check your AuthenticateCallback implementation. Ensure it returns valid user ID or proper error.

Security rules not applying

Solution:

  1. Check callbacks are returning data
  2. Enable debug logging
  3. Verify database queries return results
  4. Check user has security groups assigned

Next Steps

  1. Implement the three callbacks for your system
  2. Configure GlobalSecurity with your callbacks
  3. Call SetupSecurityProvider
  4. Test with different users and verify isolation
  5. Review callbacks_example.go for more examples

For complete working examples, see:

  • pkg/security/callbacks_example.go - 7 example implementations
  • examples/secure_server/main.go - Full server example
  • pkg/security/README.md - Comprehensive documentation