mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-06 22:36:23 +00:00
951 lines
28 KiB
Markdown
951 lines
28 KiB
Markdown
# 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
|
|
|
|
```go
|
|
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. Create handler and register security hooks
|
|
handler := restheadspec.NewHandlerWithGORM(db)
|
|
securityList := security.NewSecurityList(provider)
|
|
restheadspec.RegisterSecurityHooks(handler, securityList)
|
|
|
|
// 4. Apply middleware
|
|
router := mux.NewRouter()
|
|
restheadspec.SetupMuxRoutes(router, handler)
|
|
router.Use(security.NewAuthMiddleware(securityList))
|
|
router.Use(security.SetSecurityMiddleware(securityList))
|
|
```
|
|
|
|
## Architecture
|
|
|
|
### Spec-Agnostic Design
|
|
|
|
The security system is **completely spec-agnostic** - it doesn't depend on any specific spec implementation. Instead, each spec (restheadspec, funcspec, resolvespec) implements its own security integration by adapting to the `SecurityContext` interface.
|
|
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ Security Package (Generic) │
|
|
│ - SecurityContext interface │
|
|
│ - Security providers │
|
|
│ - Core security logic │
|
|
└─────────────────────────────────────┘
|
|
▲ ▲ ▲
|
|
│ │ │
|
|
┌──────┘ │ └──────┐
|
|
│ │ │
|
|
┌───▼────┐ ┌────▼─────┐ ┌────▼──────┐
|
|
│RestHead│ │ FuncSpec │ │ResolveSpec│
|
|
│ Spec │ │ │ │ │
|
|
│ │ │ │ │ │
|
|
│Adapts │ │ Adapts │ │ Adapts │
|
|
│to │ │ to │ │ to │
|
|
│Security│ │ Security │ │ Security │
|
|
│Context │ │ Context │ │ Context │
|
|
└────────┘ └──────────┘ └───────────┘
|
|
```
|
|
|
|
**Benefits:**
|
|
- ✅ No circular dependencies
|
|
- ✅ Each spec can customize security integration
|
|
- ✅ Easy to add new specs
|
|
- ✅ Security logic is reusable across all specs
|
|
|
|
### Core Interfaces
|
|
|
|
The security system is built on three main interfaces:
|
|
|
|
#### 1. Authenticator
|
|
Handles user authentication lifecycle:
|
|
|
|
```go
|
|
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):
|
|
|
|
```go
|
|
type ColumnSecurityProvider interface {
|
|
GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]ColumnSecurity, error)
|
|
}
|
|
```
|
|
|
|
#### 3. RowSecurityProvider
|
|
Manages row-level security (WHERE clause filtering):
|
|
|
|
```go
|
|
type RowSecurityProvider interface {
|
|
GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error)
|
|
}
|
|
```
|
|
|
|
### SecurityProvider
|
|
The main interface that combines all three:
|
|
|
|
```go
|
|
type SecurityProvider interface {
|
|
Authenticator
|
|
ColumnSecurityProvider
|
|
RowSecurityProvider
|
|
}
|
|
```
|
|
|
|
#### 4. SecurityContext (Spec Integration Interface)
|
|
Each spec implements this interface to integrate with the security system:
|
|
|
|
```go
|
|
type SecurityContext interface {
|
|
GetContext() context.Context
|
|
GetUserID() (int, bool)
|
|
GetSchema() string
|
|
GetEntity() string
|
|
GetModel() interface{}
|
|
GetQuery() interface{}
|
|
SetQuery(interface{})
|
|
GetResult() interface{}
|
|
SetResult(interface{})
|
|
}
|
|
```
|
|
|
|
**Implementation Examples:**
|
|
- `restheadspec`: Adapts `restheadspec.HookContext` → `SecurityContext`
|
|
- `funcspec`: Adapts `funcspec.HookContext` → `SecurityContext`
|
|
- `resolvespec`: Adapts `resolvespec.HookContext` → `SecurityContext`
|
|
|
|
### UserContext
|
|
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 authentication claims
|
|
Meta map[string]any // Additional metadata (can hold any JSON-serializable values)
|
|
}
|
|
```
|
|
|
|
## Available Implementations
|
|
|
|
### Authenticators
|
|
|
|
**HeaderAuthenticator** - Simple header-based authentication:
|
|
```go
|
|
auth := security.NewHeaderAuthenticator()
|
|
// Expects: X-User-ID, X-User-Name, X-User-Level, etc.
|
|
```
|
|
|
|
**DatabaseAuthenticator** - Database session-based authentication (Recommended):
|
|
```go
|
|
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:
|
|
```go
|
|
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:
|
|
```go
|
|
colSec := security.NewDatabaseColumnSecurityProvider(db)
|
|
// Uses stored procedure: resolvespec_column_security
|
|
// Queries core.secaccess and core.hub_link tables
|
|
```
|
|
|
|
**ConfigColumnSecurityProvider** - Static configuration:
|
|
```go
|
|
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:
|
|
```go
|
|
rowSec := security.NewDatabaseRowSecurityProvider(db)
|
|
// Uses stored procedure: resolvespec_row_security
|
|
```
|
|
|
|
**ConfigRowSecurityProvider** - Static templates:
|
|
```go
|
|
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 (restheadspec)
|
|
|
|
```go
|
|
func main() {
|
|
db := setupDatabase()
|
|
|
|
// Run migrations (see database_schema.sql)
|
|
// db.Exec("CREATE TABLE users ...")
|
|
// db.Exec("CREATE TABLE user_sessions ...")
|
|
|
|
// Create handler
|
|
handler := restheadspec.NewHandlerWithGORM(db)
|
|
|
|
// Create security providers
|
|
auth := security.NewDatabaseAuthenticator(db) // Session-based auth
|
|
colSec := security.NewDatabaseColumnSecurityProvider(db)
|
|
rowSec := security.NewDatabaseRowSecurityProvider(db)
|
|
|
|
// Combine providers
|
|
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
securityList := security.NewSecurityList(provider)
|
|
|
|
// Register security hooks for this spec
|
|
restheadspec.RegisterSecurityHooks(handler, securityList)
|
|
|
|
// 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)
|
|
|
|
```go
|
|
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)
|
|
|
|
// Combine providers and register hooks
|
|
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
securityList := security.NewSecurityList(provider)
|
|
restheadspec.RegisterSecurityHooks(handler, securityList)
|
|
|
|
// Setup routes...
|
|
}
|
|
```
|
|
|
|
### Example 3: FuncSpec Security (SQL Query API)
|
|
|
|
```go
|
|
import (
|
|
"github.com/bitechdev/ResolveSpec/pkg/funcspec"
|
|
"github.com/bitechdev/ResolveSpec/pkg/security"
|
|
)
|
|
|
|
func main() {
|
|
db := setupDatabase()
|
|
|
|
// Create funcspec handler
|
|
handler := funcspec.NewHandler(db)
|
|
|
|
// Create security providers
|
|
auth := security.NewJWTAuthenticator("secret-key", db)
|
|
colSec := security.NewDatabaseColumnSecurityProvider(db)
|
|
rowSec := security.NewDatabaseRowSecurityProvider(db)
|
|
|
|
// Combine providers
|
|
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
securityList := security.NewSecurityList(provider)
|
|
|
|
// Register security hooks (audit logging)
|
|
funcspec.RegisterSecurityHooks(handler, securityList)
|
|
|
|
// Note: funcspec operates on raw SQL queries, so row/column
|
|
// security is limited. Security should be enforced at the
|
|
// SQL function level or via database policies.
|
|
|
|
// Setup routes...
|
|
}
|
|
```
|
|
|
|
### Example 4: ResolveSpec Security (REST API)
|
|
|
|
```go
|
|
import (
|
|
"github.com/bitechdev/ResolveSpec/pkg/resolvespec"
|
|
"github.com/bitechdev/ResolveSpec/pkg/security"
|
|
)
|
|
|
|
func main() {
|
|
db := setupDatabase()
|
|
registry := common.NewModelRegistry()
|
|
|
|
// Register models
|
|
registry.RegisterModel("public.users", &User{})
|
|
registry.RegisterModel("public.orders", &Order{})
|
|
|
|
// Create resolvespec handler
|
|
handler := resolvespec.NewHandler(db, registry)
|
|
|
|
// Create security providers
|
|
auth := security.NewDatabaseAuthenticator(db)
|
|
colSec := security.NewDatabaseColumnSecurityProvider(db)
|
|
rowSec := security.NewDatabaseRowSecurityProvider(db)
|
|
|
|
// Combine providers
|
|
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
securityList := security.NewSecurityList(provider)
|
|
|
|
// Register security hooks for resolvespec
|
|
resolvespec.RegisterSecurityHooks(handler, securityList)
|
|
|
|
// Setup routes...
|
|
}
|
|
```
|
|
|
|
### Example 5: Custom Provider
|
|
|
|
Implement your own provider for complete control:
|
|
|
|
```go
|
|
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 with any spec
|
|
provider := &MySecurityProvider{db: db}
|
|
securityList := security.NewSecurityList(provider)
|
|
|
|
// Register with restheadspec
|
|
restheadspec.RegisterSecurityHooks(restHandler, securityList)
|
|
|
|
// Or with funcspec
|
|
funcspec.RegisterSecurityHooks(funcHandler, securityList)
|
|
|
|
// Or with resolvespec
|
|
resolvespec.RegisterSecurityHooks(resolveHandler, securityList)
|
|
```
|
|
|
|
## Security Features
|
|
|
|
### Column Security (Masking/Hiding)
|
|
|
|
**Mask SSN (show last 4 digits):**
|
|
```go
|
|
{
|
|
Path: []string{"ssn"},
|
|
Accesstype: "mask",
|
|
MaskStart: 5,
|
|
MaskChar: "*",
|
|
}
|
|
// "123-45-6789" → "*****6789"
|
|
```
|
|
|
|
**Hide entire field:**
|
|
```go
|
|
{
|
|
Path: []string{"salary"},
|
|
Accesstype: "hide",
|
|
}
|
|
// Field returns 0 or empty
|
|
```
|
|
|
|
**Nested JSON field masking:**
|
|
```go
|
|
{
|
|
Path: []string{"address", "street"},
|
|
Accesstype: "mask",
|
|
MaskStart: 10,
|
|
}
|
|
```
|
|
|
|
### Row Security (Filtering)
|
|
|
|
**User isolation:**
|
|
```go
|
|
{
|
|
Template: "user_id = {UserID}",
|
|
}
|
|
// Users only see their own records
|
|
```
|
|
|
|
**Tenant isolation:**
|
|
```go
|
|
{
|
|
Template: "tenant_id = {TenantID} AND user_id = {UserID}",
|
|
}
|
|
```
|
|
|
|
**Block all access:**
|
|
```go
|
|
{
|
|
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 (security package)
|
|
├─ Calls provider.Authenticate(request)
|
|
└─ Adds UserContext to context
|
|
↓
|
|
SetSecurityMiddleware (security package)
|
|
└─ Adds SecurityList to context
|
|
↓
|
|
Spec Handler (restheadspec/funcspec/resolvespec)
|
|
↓
|
|
BeforeRead Hook (registered by spec)
|
|
├─ Adapts spec's HookContext → SecurityContext
|
|
├─ Calls security.LoadSecurityRules(secCtx, securityList)
|
|
│ ├─ Calls provider.GetColumnSecurity()
|
|
│ └─ Calls provider.GetRowSecurity()
|
|
└─ Caches security rules
|
|
↓
|
|
BeforeScan Hook (registered by spec)
|
|
├─ Adapts spec's HookContext → SecurityContext
|
|
├─ Calls security.ApplyRowSecurity(secCtx, securityList)
|
|
└─ Applies row security (adds WHERE clause to query)
|
|
↓
|
|
Database Query (with security filters)
|
|
↓
|
|
AfterRead Hook (registered by spec)
|
|
├─ Adapts spec's HookContext → SecurityContext
|
|
├─ Calls security.ApplyColumnSecurity(secCtx, securityList)
|
|
├─ Applies column security (masks/hides fields)
|
|
└─ Calls security.LogDataAccess(secCtx)
|
|
↓
|
|
HTTP Response (secured data)
|
|
```
|
|
|
|
**Key Points:**
|
|
- Security package is spec-agnostic and provides core logic
|
|
- Each spec registers its own hooks that adapt to SecurityContext
|
|
- Security rules are loaded once and cached for the request
|
|
- Row security is applied to the query (database level)
|
|
- Column security is applied to results (application level)
|
|
|
|
## Testing
|
|
|
|
The interface-based design makes testing straightforward:
|
|
|
|
```go
|
|
// 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 Guide
|
|
|
|
### From Old Callback System
|
|
|
|
If you're upgrading from the old callback-based system:
|
|
|
|
**Old:**
|
|
```go
|
|
security.GlobalSecurity.AuthenticateCallback = myAuthFunc
|
|
security.GlobalSecurity.LoadColumnSecurityCallback = myColSecFunc
|
|
security.GlobalSecurity.LoadRowSecurityCallback = myRowSecFunc
|
|
security.SetupSecurityProvider(handler, &security.GlobalSecurity)
|
|
```
|
|
|
|
**New:**
|
|
```go
|
|
// 1. 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
|
|
}
|
|
|
|
// 2. Create security list and register hooks
|
|
provider := &MyProvider{}
|
|
securityList := security.NewSecurityList(provider)
|
|
|
|
// 3. Register with your spec
|
|
restheadspec.RegisterSecurityHooks(handler, securityList)
|
|
```
|
|
|
|
### From Old SetupSecurityProvider API
|
|
|
|
If you're upgrading from the previous interface-based system:
|
|
|
|
**Old:**
|
|
```go
|
|
securityList := security.SetupSecurityProvider(handler, provider)
|
|
```
|
|
|
|
**New:**
|
|
```go
|
|
securityList := security.NewSecurityList(provider)
|
|
restheadspec.RegisterSecurityHooks(handler, securityList) // or funcspec/resolvespec
|
|
```
|
|
|
|
The main changes:
|
|
1. Security package no longer knows about specific spec types
|
|
2. Each spec registers its own security hooks
|
|
3. More flexible - same security provider works with all specs
|
|
|
|
## 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:
|
|
|
|
```go
|
|
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:
|
|
```go
|
|
type Refreshable interface {
|
|
RefreshToken(ctx context.Context, refreshToken string) (*LoginResponse, error)
|
|
}
|
|
```
|
|
|
|
**Validatable** - Token validation:
|
|
```go
|
|
type Validatable interface {
|
|
ValidateToken(ctx context.Context, token string) (bool, error)
|
|
}
|
|
```
|
|
|
|
**Cacheable** - Cache management:
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
}
|
|
```
|
|
|
|
## 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.
|