mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-01-18 00:44:25 +00:00
Updated the security package
This commit is contained in:
@@ -3,35 +3,96 @@
|
||||
## 3-Step Setup
|
||||
|
||||
```go
|
||||
// Step 1: Implement callbacks
|
||||
func myAuth(r *http.Request) (int, string, error) { /* ... */ }
|
||||
func myColSec(userID int, schema, table string) ([]security.ColumnSecurity, error) { /* ... */ }
|
||||
func myRowSec(userID int, schema, table string) (security.RowSecurity, error) { /* ... */ }
|
||||
// Step 1: Create security providers
|
||||
auth := security.NewDatabaseAuthenticator(db) // Session-based (recommended)
|
||||
// OR: auth := security.NewJWTAuthenticator("secret-key", db)
|
||||
// OR: auth := security.NewHeaderAuthenticator()
|
||||
|
||||
// Step 2: Configure callbacks
|
||||
security.GlobalSecurity.AuthenticateCallback = myAuth
|
||||
security.GlobalSecurity.LoadColumnSecurityCallback = myColSec
|
||||
security.GlobalSecurity.LoadRowSecurityCallback = myRowSec
|
||||
colSec := security.NewDatabaseColumnSecurityProvider(db)
|
||||
rowSec := security.NewDatabaseRowSecurityProvider(db)
|
||||
|
||||
// Step 2: Combine providers
|
||||
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
|
||||
// Step 3: Setup and apply middleware
|
||||
security.SetupSecurityProvider(handler, &security.GlobalSecurity)
|
||||
router.Use(mux.MiddlewareFunc(security.AuthMiddleware))
|
||||
router.Use(mux.MiddlewareFunc(security.SetSecurityMiddleware))
|
||||
securityList := security.SetupSecurityProvider(handler, provider)
|
||||
router.Use(security.NewAuthMiddleware(securityList))
|
||||
router.Use(security.SetSecurityMiddleware(securityList))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Callback Signatures
|
||||
## Stored Procedures
|
||||
|
||||
**All database operations use PostgreSQL stored procedures** with `resolvespec_*` naming:
|
||||
|
||||
### Database Authenticators
|
||||
```go
|
||||
// DatabaseAuthenticator uses these stored procedures:
|
||||
resolvespec_login(jsonb) // Login with credentials
|
||||
resolvespec_logout(jsonb) // Invalidate session
|
||||
resolvespec_session(text, text) // Validate session token
|
||||
resolvespec_session_update(text, jsonb) // Update activity timestamp
|
||||
resolvespec_refresh_token(text, jsonb) // Generate new session
|
||||
|
||||
// JWTAuthenticator uses these stored procedures:
|
||||
resolvespec_jwt_login(text, text) // Validate credentials
|
||||
resolvespec_jwt_logout(text, int) // Blacklist token
|
||||
```
|
||||
|
||||
### Security Providers
|
||||
```go
|
||||
// DatabaseColumnSecurityProvider:
|
||||
resolvespec_column_security(int, text, text) // Load column rules
|
||||
|
||||
// DatabaseRowSecurityProvider:
|
||||
resolvespec_row_security(text, text, int) // Load row template
|
||||
```
|
||||
|
||||
All stored procedures return structured results:
|
||||
- Session/Login: `(p_success bool, p_error text, p_data jsonb)`
|
||||
- Security: `(p_success bool, p_error text, p_rules jsonb)`
|
||||
|
||||
See `database_schema.sql` for complete definitions.
|
||||
|
||||
---
|
||||
|
||||
## Interface Signatures
|
||||
|
||||
```go
|
||||
// 1. Authentication
|
||||
func(r *http.Request) (userID int, roles string, err error)
|
||||
// Authenticator interface
|
||||
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. Column Security
|
||||
func(userID int, schema, tablename string) ([]ColumnSecurity, error)
|
||||
// ColumnSecurityProvider interface
|
||||
type ColumnSecurityProvider interface {
|
||||
GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]ColumnSecurity, error)
|
||||
}
|
||||
|
||||
// 3. Row Security
|
||||
func(userID int, schema, tablename string) (RowSecurity, error)
|
||||
// RowSecurityProvider interface
|
||||
type RowSecurityProvider interface {
|
||||
GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UserContext Structure
|
||||
|
||||
```go
|
||||
security.UserContext{
|
||||
UserID: 123, // User's unique ID
|
||||
UserName: "john_doe", // Username
|
||||
UserLevel: 5, // User privilege level
|
||||
SessionID: "sess_abc123", // Current session ID
|
||||
RemoteID: "remote_xyz", // Remote system ID
|
||||
Roles: []string{"admin"}, // User roles
|
||||
Email: "john@example.com", // User email
|
||||
Claims: map[string]any{}, // Additional metadata
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -109,70 +170,204 @@ HasBlock: true
|
||||
|
||||
## Example Implementations
|
||||
|
||||
### Simple Header Auth
|
||||
### Database Session Authenticator (Recommended)
|
||||
|
||||
```go
|
||||
func authFromHeader(r *http.Request) (int, string, error) {
|
||||
// Create authenticator
|
||||
auth := security.NewDatabaseAuthenticator(db)
|
||||
|
||||
// Requires these tables:
|
||||
// - users (id, username, email, password, user_level, roles, is_active)
|
||||
// - user_sessions (session_token, user_id, expires_at, created_at, last_activity_at)
|
||||
// See database_schema.sql for full schema
|
||||
|
||||
// Features:
|
||||
// - Login with username/password
|
||||
// - Session management in database
|
||||
// - Token refresh support (implements Refreshable)
|
||||
// - Automatic session expiration
|
||||
// - Tracks IP address and user agent
|
||||
// - Works with Authorization header or cookie
|
||||
```
|
||||
|
||||
### Simple Header Authenticator
|
||||
|
||||
```go
|
||||
type HeaderAuthenticator struct{}
|
||||
|
||||
func NewHeaderAuthenticator() *HeaderAuthenticator {
|
||||
return &HeaderAuthenticator{}
|
||||
}
|
||||
|
||||
func (a *HeaderAuthenticator) Login(ctx context.Context, req security.LoginRequest) (*security.LoginResponse, error) {
|
||||
return nil, fmt.Errorf("not supported")
|
||||
}
|
||||
|
||||
func (a *HeaderAuthenticator) Logout(ctx context.Context, req security.LogoutRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *HeaderAuthenticator) Authenticate(r *http.Request) (*security.UserContext, error) {
|
||||
userIDStr := r.Header.Get("X-User-ID")
|
||||
if userIDStr == "" {
|
||||
return 0, "", fmt.Errorf("X-User-ID required")
|
||||
return nil, fmt.Errorf("X-User-ID required")
|
||||
}
|
||||
userID, err := strconv.Atoi(userIDStr)
|
||||
return userID, "", err
|
||||
userID, _ := strconv.Atoi(userIDStr)
|
||||
return &security.UserContext{
|
||||
UserID: userID,
|
||||
UserName: r.Header.Get("X-User-Name"),
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### JWT Auth
|
||||
### JWT Authenticator
|
||||
|
||||
```go
|
||||
func authFromJWT(r *http.Request) (int, string, error) {
|
||||
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
claims, err := jwt.Parse(token, secret)
|
||||
type JWTAuthenticator struct {
|
||||
secretKey []byte
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewJWTAuthenticator(secret string, db *gorm.DB) *JWTAuthenticator {
|
||||
return &JWTAuthenticator{secretKey: []byte(secret), db: db}
|
||||
}
|
||||
|
||||
func (a *JWTAuthenticator) Login(ctx context.Context, req security.LoginRequest) (*security.LoginResponse, error) {
|
||||
// Validate credentials against database
|
||||
var user User
|
||||
err := a.db.WithContext(ctx).Where("username = ?", req.Username).First(&user).Error
|
||||
if err != nil {
|
||||
return 0, "", err
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
return claims.UserID, claims.Roles, nil
|
||||
|
||||
// Generate JWT token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.ID,
|
||||
"exp": time.Now().Add(24 * time.Hour).Unix(),
|
||||
})
|
||||
tokenString, _ := token.SignedString(a.secretKey)
|
||||
|
||||
return &security.LoginResponse{
|
||||
Token: tokenString,
|
||||
User: &security.UserContext{UserID: user.ID},
|
||||
ExpiresIn: 86400,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *JWTAuthenticator) Logout(ctx context.Context, req security.LogoutRequest) error {
|
||||
// Add to blacklist
|
||||
return a.db.WithContext(ctx).Table("token_blacklist").Create(map[string]any{
|
||||
"token": req.Token,
|
||||
"user_id": req.UserID,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (a *JWTAuthenticator) Authenticate(r *http.Request) (*security.UserContext, error) {
|
||||
tokenString := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (any, error) {
|
||||
return a.secretKey, nil
|
||||
})
|
||||
if err != nil || !token.Valid {
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
return &security.UserContext{
|
||||
UserID: int(claims["user_id"].(float64)),
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Static Column Security
|
||||
|
||||
```go
|
||||
func loadColSec(userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||
if table == "employees" {
|
||||
return []security.ColumnSecurity{
|
||||
{Path: []string{"ssn"}, Accesstype: "mask", MaskStart: 5},
|
||||
{Path: []string{"salary"}, Accesstype: "hide"},
|
||||
}, nil
|
||||
}
|
||||
return []security.ColumnSecurity{}, nil
|
||||
type ConfigColumnSecurityProvider struct {
|
||||
rules map[string][]security.ColumnSecurity
|
||||
}
|
||||
|
||||
func NewConfigColumnSecurityProvider(rules map[string][]security.ColumnSecurity) *ConfigColumnSecurityProvider {
|
||||
return &ConfigColumnSecurityProvider{rules: rules}
|
||||
}
|
||||
|
||||
func (p *ConfigColumnSecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||
key := fmt.Sprintf("%s.%s", schema, table)
|
||||
return p.rules[key], nil
|
||||
}
|
||||
```
|
||||
|
||||
### Database Column Security
|
||||
|
||||
```go
|
||||
func loadColSec(userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||
rows, err := db.Query(`
|
||||
type DatabaseColumnSecurityProvider struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewDatabaseColumnSecurityProvider(db *gorm.DB) *DatabaseColumnSecurityProvider {
|
||||
return &DatabaseColumnSecurityProvider{db: db}
|
||||
}
|
||||
|
||||
func (p *DatabaseColumnSecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||
var records []struct {
|
||||
Control string
|
||||
Accesstype string
|
||||
JSONValue string
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT control, accesstype, jsonvalue
|
||||
FROM core.secacces
|
||||
WHERE rid_hub IN (...)
|
||||
FROM core.secaccess
|
||||
WHERE rid_hub IN (
|
||||
SELECT rid_hub_parent FROM core.hub_link
|
||||
WHERE rid_hub_child = ? AND parent_hubtype = 'secgroup'
|
||||
)
|
||||
AND control ILIKE ?
|
||||
`, fmt.Sprintf("%s.%s%%", schema, table))
|
||||
// ... parse and return
|
||||
`
|
||||
|
||||
err := p.db.WithContext(ctx).Raw(query, userID, fmt.Sprintf("%s.%s%%", schema, table)).Scan(&records).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rules []security.ColumnSecurity
|
||||
for _, rec := range records {
|
||||
parts := strings.Split(rec.Control, ".")
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
rules = append(rules, security.ColumnSecurity{
|
||||
Schema: schema,
|
||||
Tablename: table,
|
||||
Path: parts[2:],
|
||||
Accesstype: rec.Accesstype,
|
||||
})
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Static Row Security
|
||||
|
||||
```go
|
||||
func loadRowSec(userID int, schema, table string) (security.RowSecurity, error) {
|
||||
templates := map[string]string{
|
||||
"orders": "user_id = {UserID}",
|
||||
"documents": "user_id = {UserID} OR is_public = true",
|
||||
type ConfigRowSecurityProvider struct {
|
||||
templates map[string]string
|
||||
blocked map[string]bool
|
||||
}
|
||||
|
||||
func NewConfigRowSecurityProvider(templates map[string]string, blocked map[string]bool) *ConfigRowSecurityProvider {
|
||||
return &ConfigRowSecurityProvider{templates: templates, blocked: blocked}
|
||||
}
|
||||
|
||||
func (p *ConfigRowSecurityProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (security.RowSecurity, error) {
|
||||
key := fmt.Sprintf("%s.%s", schema, table)
|
||||
|
||||
if p.blocked[key] {
|
||||
return security.RowSecurity{HasBlock: true}, nil
|
||||
}
|
||||
|
||||
return security.RowSecurity{
|
||||
Template: templates[table],
|
||||
Schema: schema,
|
||||
Tablename: table,
|
||||
UserID: userID,
|
||||
Template: p.templates[key],
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
@@ -182,19 +377,22 @@ func loadRowSec(userID int, schema, table string) (security.RowSecurity, error)
|
||||
## Testing
|
||||
|
||||
```go
|
||||
// Test auth callback
|
||||
// Test Authenticator
|
||||
auth := security.NewHeaderAuthenticator()
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("X-User-ID", "123")
|
||||
userID, roles, err := myAuth(req)
|
||||
assert.Equal(t, 123, userID)
|
||||
userCtx, err := auth.Authenticate(req)
|
||||
assert.Equal(t, 123, userCtx.UserID)
|
||||
|
||||
// Test column security callback
|
||||
rules, err := myColSec(123, "public", "employees")
|
||||
assert.Equal(t, "mask", rules[0].Accesstype)
|
||||
// Test ColumnSecurityProvider
|
||||
colSec := security.NewConfigColumnSecurityProvider(rules)
|
||||
cols, err := colSec.GetColumnSecurity(context.Background(), 123, "public", "employees")
|
||||
assert.Equal(t, "mask", cols[0].Accesstype)
|
||||
|
||||
// Test row security callback
|
||||
rowSec, err := myRowSec(123, "public", "orders")
|
||||
assert.Equal(t, "user_id = {UserID}", rowSec.Template)
|
||||
// Test RowSecurityProvider
|
||||
rowSec := security.NewConfigRowSecurityProvider(templates, blocked)
|
||||
row, err := rowSec.GetRowSecurity(context.Background(), 123, "public", "orders")
|
||||
assert.Equal(t, "user_id = {UserID}", row.Template)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -204,13 +402,13 @@ assert.Equal(t, "user_id = {UserID}", rowSec.Template)
|
||||
```
|
||||
HTTP Request
|
||||
↓
|
||||
AuthMiddleware → calls AuthenticateCallback
|
||||
↓ (adds userID to context)
|
||||
SetSecurityMiddleware → adds GlobalSecurity to context
|
||||
NewAuthMiddleware → calls provider.Authenticate()
|
||||
↓ (adds UserContext to context)
|
||||
SetSecurityMiddleware → adds SecurityList to context
|
||||
↓
|
||||
Handler.Handle()
|
||||
↓
|
||||
BeforeRead Hook → calls LoadColumnSecurityCallback + LoadRowSecurityCallback
|
||||
BeforeRead Hook → calls provider.GetColumnSecurity() + GetRowSecurity()
|
||||
↓
|
||||
BeforeScan Hook → applies row security (WHERE clause)
|
||||
↓
|
||||
@@ -228,10 +426,13 @@ HTTP Response
|
||||
### Role-Based Security
|
||||
|
||||
```go
|
||||
func loadColSec(userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||
if isAdmin(userID) {
|
||||
func (p *MyColumnSecurityProvider) 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 loadRestrictions(userID, schema, table), nil
|
||||
}
|
||||
```
|
||||
@@ -239,7 +440,7 @@ func loadColSec(userID int, schema, table string) ([]security.ColumnSecurity, er
|
||||
### Tenant Isolation
|
||||
|
||||
```go
|
||||
func loadRowSec(userID int, schema, table string) (security.RowSecurity, error) {
|
||||
func (p *MyRowSecurityProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (security.RowSecurity, error) {
|
||||
tenantID := getUserTenant(userID)
|
||||
return security.RowSecurity{
|
||||
Template: fmt.Sprintf("tenant_id = %d", tenantID),
|
||||
@@ -247,19 +448,26 @@ func loadRowSec(userID int, schema, table string) (security.RowSecurity, error)
|
||||
}
|
||||
```
|
||||
|
||||
### Caching
|
||||
### Caching with Decorator
|
||||
|
||||
```go
|
||||
var cache = make(map[string][]security.ColumnSecurity)
|
||||
type CachedColumnSecurityProvider struct {
|
||||
inner security.ColumnSecurityProvider
|
||||
cache *cache.Cache
|
||||
}
|
||||
|
||||
func loadColSec(userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||
func (p *CachedColumnSecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||
key := fmt.Sprintf("%d:%s.%s", userID, schema, table)
|
||||
if cached, ok := cache[key]; ok {
|
||||
return cached, nil
|
||||
|
||||
if cached, found := p.cache.Get(key); found {
|
||||
return cached.([]security.ColumnSecurity), nil
|
||||
}
|
||||
rules := loadFromDB(userID, schema, table)
|
||||
cache[key] = rules
|
||||
return rules, nil
|
||||
|
||||
rules, err := p.inner.GetColumnSecurity(ctx, userID, schema, table)
|
||||
if err == nil {
|
||||
p.cache.Set(key, rules, cache.DefaultExpiration)
|
||||
}
|
||||
return rules, err
|
||||
}
|
||||
```
|
||||
|
||||
@@ -268,21 +476,20 @@ func loadColSec(userID int, schema, table string) ([]security.ColumnSecurity, er
|
||||
## Error Handling
|
||||
|
||||
```go
|
||||
// Setup will fail if callbacks not configured
|
||||
if err := security.SetupSecurityProvider(handler, &security.GlobalSecurity); err != nil {
|
||||
log.Fatal("Security setup failed:", err)
|
||||
}
|
||||
// Panic if provider is nil
|
||||
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
// panics if any parameter is nil
|
||||
|
||||
// Auth middleware rejects if callback returns error
|
||||
func myAuth(r *http.Request) (int, string, error) {
|
||||
// Auth middleware returns 401 if Authenticate fails
|
||||
func (a *MyAuthenticator) Authenticate(r *http.Request) (*security.UserContext, error) {
|
||||
if invalid {
|
||||
return 0, "", fmt.Errorf("invalid credentials") // Returns HTTP 401
|
||||
return nil, fmt.Errorf("invalid credentials") // Returns HTTP 401
|
||||
}
|
||||
return userID, roles, nil
|
||||
return &security.UserContext{UserID: userID}, nil
|
||||
}
|
||||
|
||||
// Security loading can fail gracefully
|
||||
func loadColSec(userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||
func (p *MyProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||
rules, err := db.Load(...)
|
||||
if err != nil {
|
||||
log.Printf("Failed to load security: %v", err)
|
||||
@@ -294,6 +501,45 @@ func loadColSec(userID int, schema, table string) ([]security.ColumnSecurity, er
|
||||
|
||||
---
|
||||
|
||||
## Login/Logout Endpoints
|
||||
|
||||
```go
|
||||
func SetupAuthRoutes(router *mux.Router, securityList *security.SecurityList) {
|
||||
// Login
|
||||
router.HandleFunc("/auth/login", func(w http.ResponseWriter, r *http.Request) {
|
||||
var req security.LoginRequest
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
resp, err := securityList.Provider().Login(r.Context(), req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}).Methods("POST")
|
||||
|
||||
// Logout
|
||||
router.HandleFunc("/auth/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.Header.Get("Authorization")
|
||||
userID, _ := security.GetUserID(r.Context())
|
||||
|
||||
err := securityList.Provider().Logout(r.Context(), security.LogoutRequest{
|
||||
Token: token,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}).Methods("POST")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
```go
|
||||
@@ -301,15 +547,15 @@ func loadColSec(userID int, schema, table string) ([]security.ColumnSecurity, er
|
||||
import "github.com/bitechdev/GoCore/pkg/cfg"
|
||||
cfg.SetLogLevel("DEBUG")
|
||||
|
||||
// Log in callbacks
|
||||
func myAuth(r *http.Request) (int, string, error) {
|
||||
// Log in provider methods
|
||||
func (a *MyAuthenticator) Authenticate(r *http.Request) (*security.UserContext, error) {
|
||||
token := r.Header.Get("Authorization")
|
||||
log.Printf("Auth: token=%s", token)
|
||||
// ...
|
||||
}
|
||||
|
||||
// Check if callbacks are called
|
||||
func loadColSec(userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||
// Check if methods are called
|
||||
func (p *MyColumnSecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||
log.Printf("Loading column security: user=%d, schema=%s, table=%s", userID, schema, table)
|
||||
// ...
|
||||
}
|
||||
@@ -323,6 +569,7 @@ func loadColSec(userID int, schema, table string) ([]security.ColumnSecurity, er
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -331,29 +578,42 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// Simple all-in-one provider
|
||||
type SimpleProvider struct{}
|
||||
|
||||
func (p *SimpleProvider) Login(ctx context.Context, req security.LoginRequest) (*security.LoginResponse, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (p *SimpleProvider) Logout(ctx context.Context, req security.LogoutRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SimpleProvider) Authenticate(r *http.Request) (*security.UserContext, error) {
|
||||
id, _ := strconv.Atoi(r.Header.Get("X-User-ID"))
|
||||
return &security.UserContext{UserID: id}, nil
|
||||
}
|
||||
|
||||
func (p *SimpleProvider) GetColumnSecurity(ctx context.Context, u int, s, t string) ([]security.ColumnSecurity, error) {
|
||||
return []security.ColumnSecurity{}, nil
|
||||
}
|
||||
|
||||
func (p *SimpleProvider) GetRowSecurity(ctx context.Context, u int, s, t string) (security.RowSecurity, error) {
|
||||
return security.RowSecurity{Template: fmt.Sprintf("user_id = %d", u)}, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
handler := restheadspec.NewHandlerWithGORM(db)
|
||||
|
||||
// Configure callbacks
|
||||
security.GlobalSecurity.AuthenticateCallback = func(r *http.Request) (int, string, error) {
|
||||
id, _ := strconv.Atoi(r.Header.Get("X-User-ID"))
|
||||
return id, "", nil
|
||||
}
|
||||
security.GlobalSecurity.LoadColumnSecurityCallback = func(u int, s, t string) ([]security.ColumnSecurity, error) {
|
||||
return []security.ColumnSecurity{}, nil
|
||||
}
|
||||
security.GlobalSecurity.LoadRowSecurityCallback = func(u int, s, t string) (security.RowSecurity, error) {
|
||||
return security.RowSecurity{Template: fmt.Sprintf("user_id = %d", u)}, nil
|
||||
}
|
||||
// Setup security
|
||||
provider := &SimpleProvider{}
|
||||
securityList := security.SetupSecurityProvider(handler, provider)
|
||||
|
||||
// Setup
|
||||
security.SetupSecurityProvider(handler, &security.GlobalSecurity)
|
||||
|
||||
// Middleware
|
||||
// Apply middleware
|
||||
router := mux.NewRouter()
|
||||
restheadspec.SetupMuxRoutes(router, handler)
|
||||
router.Use(mux.MiddlewareFunc(security.AuthMiddleware))
|
||||
router.Use(mux.MiddlewareFunc(security.SetSecurityMiddleware))
|
||||
router.Use(security.NewAuthMiddleware(securityList))
|
||||
router.Use(security.SetSecurityMiddleware(securityList))
|
||||
|
||||
http.ListenAndServe(":8080", router)
|
||||
}
|
||||
@@ -361,15 +621,32 @@ func main() {
|
||||
|
||||
---
|
||||
|
||||
## Context Helpers
|
||||
|
||||
```go
|
||||
// Get full user context
|
||||
userCtx, ok := security.GetUserContext(ctx)
|
||||
|
||||
// Get individual 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)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `CALLBACKS_GUIDE.md` | **Start here** - Complete implementation guide |
|
||||
| `callbacks_example.go` | 7 working examples to copy |
|
||||
| `CALLBACKS_SUMMARY.md` | Architecture overview |
|
||||
| `README.md` | Full documentation |
|
||||
| `setup_example.go` | Integration examples |
|
||||
| `INTERFACE_GUIDE.md` | **Start here** - Complete implementation guide |
|
||||
| `examples.go` | Working provider implementations to copy |
|
||||
| `setup_example.go` | 6 complete integration examples |
|
||||
| `README.md` | Architecture overview and migration guide |
|
||||
|
||||
---
|
||||
|
||||
@@ -377,22 +654,22 @@ func main() {
|
||||
|
||||
```go
|
||||
// ===== REQUIRED SETUP =====
|
||||
security.GlobalSecurity.AuthenticateCallback = myAuthFunc
|
||||
security.GlobalSecurity.LoadColumnSecurityCallback = myColFunc
|
||||
security.GlobalSecurity.LoadRowSecurityCallback = myRowFunc
|
||||
security.SetupSecurityProvider(handler, &security.GlobalSecurity)
|
||||
auth := security.NewJWTAuthenticator("secret", db)
|
||||
colSec := security.NewDatabaseColumnSecurityProvider(db)
|
||||
rowSec := security.NewDatabaseRowSecurityProvider(db)
|
||||
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
securityList := security.SetupSecurityProvider(handler, provider)
|
||||
|
||||
// ===== CALLBACK SIGNATURES =====
|
||||
func(r *http.Request) (int, string, error) // Auth
|
||||
func(int, string, string) ([]security.ColumnSecurity, error) // Column
|
||||
func(int, string, string) (security.RowSecurity, error) // Row
|
||||
// ===== INTERFACE METHODS =====
|
||||
Authenticate(r *http.Request) (*UserContext, error)
|
||||
Login(ctx context.Context, req LoginRequest) (*LoginResponse, error)
|
||||
Logout(ctx context.Context, req LogoutRequest) error
|
||||
GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]ColumnSecurity, error)
|
||||
GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error)
|
||||
|
||||
// ===== QUICK EXAMPLES =====
|
||||
// Header auth
|
||||
func(r *http.Request) (int, string, error) {
|
||||
id, _ := strconv.Atoi(r.Header.Get("X-User-ID"))
|
||||
return id, "", nil
|
||||
}
|
||||
&UserContext{UserID: 123, UserName: "john"}
|
||||
|
||||
// Mask SSN
|
||||
{Path: []string{"ssn"}, Accesstype: "mask", MaskStart: 5}
|
||||
|
||||
Reference in New Issue
Block a user