mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-01-02 01:44:25 +00:00
Updated the security package
This commit is contained in:
634
pkg/security/README.md
Normal file
634
pkg/security/README.md
Normal file
@@ -0,0 +1,634 @@
|
||||
# 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. 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:
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
### 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 metadata
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
```go
|
||||
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)
|
||||
|
||||
```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)
|
||||
|
||||
provider := security.NewCompositeSecurityProvider(auth, colSec, rowSec)
|
||||
securityList := security.SetupSecurityProvider(handler, provider)
|
||||
|
||||
// Setup routes...
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: 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
|
||||
provider := &MySecurityProvider{db: db}
|
||||
securityList := security.SetupSecurityProvider(handler, provider)
|
||||
```
|
||||
|
||||
## 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
|
||||
├─ 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:
|
||||
|
||||
```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 from Callbacks
|
||||
|
||||
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
|
||||
// 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:
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Part of the ResolveSpec project.
|
||||
Reference in New Issue
Block a user