Add BEST-PRACTICE document
This commit is contained in:
parent
00dbfd9dd3
commit
0fa803cf9b
874
BEST-PRACTICE.md
Normal file
874
BEST-PRACTICE.md
Normal file
@ -0,0 +1,874 @@
|
||||
# c77_rbac Best Practices Guide
|
||||
|
||||
This guide provides recommended patterns and practices for implementing c77_rbac in production environments. While the tutorials demonstrate basic usage, this document focuses on enterprise-grade implementations.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Database Organization](#database-organization)
|
||||
2. [Permission Design Patterns](#permission-design-patterns)
|
||||
3. [Performance Optimization](#performance-optimization)
|
||||
4. [Security Hardening](#security-hardening)
|
||||
5. [Multi-Tenant Architectures](#multi-tenant-architectures)
|
||||
6. [Migration Strategies](#migration-strategies)
|
||||
7. [Framework-Specific Patterns](#framework-specific-patterns)
|
||||
8. [Monitoring and Maintenance](#monitoring-and-maintenance)
|
||||
9. [Common Pitfalls](#common-pitfalls)
|
||||
10. [Production Checklist](#production-checklist)
|
||||
|
||||
## Database Organization
|
||||
|
||||
### Schema Architecture
|
||||
|
||||
**Recommended Production Structure:**
|
||||
|
||||
```sql
|
||||
-- Extensions and system objects only
|
||||
public.*
|
||||
|
||||
-- Core application tables
|
||||
application.*
|
||||
|
||||
-- Multi-tenant schemas (if applicable)
|
||||
tenant_<id>.*
|
||||
|
||||
-- Audit and compliance data
|
||||
audit.*
|
||||
|
||||
-- Read-only views and reports
|
||||
reporting.*
|
||||
|
||||
-- Temporary processing tables
|
||||
staging.*
|
||||
```
|
||||
|
||||
**Implementation Example:**
|
||||
|
||||
```sql
|
||||
-- As database owner or superuser
|
||||
CREATE EXTENSION c77_rbac; -- Goes in public
|
||||
|
||||
-- As application user
|
||||
CREATE SCHEMA application;
|
||||
CREATE SCHEMA audit;
|
||||
CREATE SCHEMA reporting;
|
||||
|
||||
-- Create tables in appropriate schemas
|
||||
CREATE TABLE application.users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
-- ...
|
||||
);
|
||||
|
||||
-- Apply RBAC policies with schema qualification
|
||||
SELECT c77_rbac_apply_policy(
|
||||
'application.users',
|
||||
'view_users',
|
||||
'department',
|
||||
'department_id'
|
||||
);
|
||||
```
|
||||
|
||||
### Why Schema Separation Matters
|
||||
|
||||
1. **Security Isolation**: Each schema can have different access rules
|
||||
2. **Clear Boundaries**: Prevents mixing system and application objects
|
||||
3. **Easier Maintenance**: Can backup/restore schemas independently
|
||||
4. **Better Performance**: Optimizer works better with organized schemas
|
||||
5. **Compliance**: Easier to demonstrate data separation for audits
|
||||
|
||||
## Permission Design Patterns
|
||||
|
||||
### Role Naming Conventions
|
||||
|
||||
**Pattern**: `<scope>_<function>_<access_level>`
|
||||
|
||||
```sql
|
||||
-- Good role names
|
||||
SELECT c77_rbac_grant_feature('dept_expense_approver', 'approve_expenses');
|
||||
SELECT c77_rbac_grant_feature('global_report_viewer', 'view_all_reports');
|
||||
SELECT c77_rbac_grant_feature('tenant_data_editor', 'edit_tenant_data');
|
||||
|
||||
-- Avoid generic names
|
||||
-- Bad: 'admin', 'user', 'role1', 'temp'
|
||||
```
|
||||
|
||||
### Feature (Permission) Naming
|
||||
|
||||
**Pattern**: `<action>_<resource>_<qualifier>`
|
||||
|
||||
```sql
|
||||
-- Well-structured permissions
|
||||
SELECT c77_rbac_grant_feature('finance_manager', 'approve_expenses_over_1000');
|
||||
SELECT c77_rbac_grant_feature('hr_staff', 'view_employee_personal_data');
|
||||
SELECT c77_rbac_grant_feature('sales_rep', 'edit_own_opportunities');
|
||||
|
||||
-- Create feature hierarchies
|
||||
SELECT c77_rbac_grant_feature('viewer', 'view_public_data');
|
||||
SELECT c77_rbac_grant_feature('editor', 'view_public_data'); -- Inherits viewer permissions
|
||||
SELECT c77_rbac_grant_feature('editor', 'edit_public_data');
|
||||
```
|
||||
|
||||
### Scope Design Patterns
|
||||
|
||||
```sql
|
||||
-- Hierarchical scopes
|
||||
'global/all' -- System-wide access
|
||||
'region/north_america' -- Regional access
|
||||
'country/usa' -- Country-specific
|
||||
'state/california' -- State-level
|
||||
'office/san_francisco' -- Office-level
|
||||
|
||||
-- Functional scopes
|
||||
'department/engineering' -- Department-based
|
||||
'project/apollo' -- Project-based
|
||||
'team/mobile_dev' -- Team-based
|
||||
'cost_center/cc_12345' -- Financial scopes
|
||||
|
||||
-- Multi-tenant scopes
|
||||
'tenant/customer_abc' -- Tenant isolation
|
||||
'subscription/enterprise' -- Feature-based access
|
||||
'api_client/client_xyz' -- API client scoping
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Index Strategy for RLS
|
||||
|
||||
```sql
|
||||
-- Always index columns used in RLS policies
|
||||
CREATE INDEX idx_users_department_id ON application.users(department_id);
|
||||
CREATE INDEX idx_documents_tenant_id ON application.documents(tenant_id);
|
||||
|
||||
-- Composite indexes for complex policies
|
||||
CREATE INDEX idx_projects_dept_status ON application.projects(department_id, status);
|
||||
|
||||
-- Partial indexes for common filters
|
||||
CREATE INDEX idx_active_users_dept ON application.users(department_id)
|
||||
WHERE is_active = true;
|
||||
|
||||
-- Hash indexes for exact matches
|
||||
CREATE INDEX idx_users_external_id_hash ON application.users USING hash(external_id);
|
||||
```
|
||||
|
||||
### Query Optimization Patterns
|
||||
|
||||
```sql
|
||||
-- Use CTEs for complex permission checks
|
||||
WITH user_permissions AS (
|
||||
SELECT * FROM c77_rbac_user_permissions
|
||||
WHERE external_id = current_setting('c77_rbac.external_id', true)
|
||||
)
|
||||
SELECT d.*
|
||||
FROM application.documents d
|
||||
JOIN user_permissions up ON d.department_id = up.scope_id
|
||||
WHERE up.feature_name = 'view_documents';
|
||||
|
||||
-- Materialized views for permission matrices
|
||||
CREATE MATERIALIZED VIEW reporting.user_effective_permissions AS
|
||||
SELECT
|
||||
s.external_id,
|
||||
f.name as feature,
|
||||
sr.scope_type,
|
||||
sr.scope_id,
|
||||
'direct' as permission_type
|
||||
FROM c77_rbac_subjects s
|
||||
JOIN c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id
|
||||
JOIN c77_rbac_role_features rf ON sr.role_id = rf.role_id
|
||||
JOIN c77_rbac_features f ON rf.feature_id = f.feature_id;
|
||||
|
||||
CREATE UNIQUE INDEX idx_user_perms_unique
|
||||
ON reporting.user_effective_permissions(external_id, feature, scope_type, scope_id);
|
||||
|
||||
-- Refresh strategy
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY reporting.user_effective_permissions;
|
||||
```
|
||||
|
||||
### Connection Pool Optimization
|
||||
|
||||
```sql
|
||||
-- Pre-warm permission cache function
|
||||
CREATE OR REPLACE FUNCTION warmup_user_permissions(p_user_id TEXT)
|
||||
RETURNS void AS $$
|
||||
BEGIN
|
||||
-- Pre-load common permission checks
|
||||
PERFORM c77_rbac_can_access('view_data', p_user_id, 'global', 'all');
|
||||
PERFORM c77_rbac_can_access('edit_data', p_user_id, 'department',
|
||||
(SELECT department_id FROM application.users WHERE external_id = p_user_id));
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
## Security Hardening
|
||||
|
||||
### Connection Security
|
||||
|
||||
```python
|
||||
# Python/Django example
|
||||
def get_db_connection(user_id):
|
||||
with connection.cursor() as cursor:
|
||||
# Always reset context first
|
||||
cursor.execute("RESET c77_rbac.external_id")
|
||||
cursor.execute("RESET role") # Reset any SET ROLE commands
|
||||
|
||||
# Set new context
|
||||
cursor.execute("SET c77_rbac.external_id = %s", [user_id])
|
||||
|
||||
# Verify context was set
|
||||
cursor.execute("SELECT current_setting('c77_rbac.external_id', true)")
|
||||
if cursor.fetchone()[0] != str(user_id):
|
||||
raise SecurityError("Failed to set security context")
|
||||
```
|
||||
|
||||
```php
|
||||
// PHP/Laravel example
|
||||
class SecureConnection
|
||||
{
|
||||
public function setUserContext($userId)
|
||||
{
|
||||
// Use transaction to ensure atomic context setting
|
||||
DB::transaction(function () use ($userId) {
|
||||
DB::statement("RESET c77_rbac.external_id");
|
||||
DB::statement("SET LOCAL c77_rbac.external_id = ?", [$userId]);
|
||||
|
||||
// Verify
|
||||
$result = DB::selectOne("SELECT current_setting(?, true) as ctx",
|
||||
['c77_rbac.external_id']);
|
||||
|
||||
if ($result->ctx !== (string)$userId) {
|
||||
throw new SecurityException('Context verification failed');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Audit Logging
|
||||
|
||||
```sql
|
||||
-- Comprehensive audit table
|
||||
CREATE TABLE audit.rbac_changes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
event_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
event_type TEXT NOT NULL, -- 'role_assigned', 'role_revoked', etc.
|
||||
performed_by TEXT NOT NULL,
|
||||
target_user TEXT,
|
||||
role_name TEXT,
|
||||
scope_type TEXT,
|
||||
scope_id TEXT,
|
||||
success BOOLEAN NOT NULL,
|
||||
error_message TEXT,
|
||||
client_ip INET,
|
||||
session_id TEXT,
|
||||
additional_context JSONB
|
||||
);
|
||||
|
||||
-- Audit trigger for role changes
|
||||
CREATE OR REPLACE FUNCTION audit.log_rbac_changes()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO audit.rbac_changes (
|
||||
event_type,
|
||||
performed_by,
|
||||
target_user,
|
||||
role_name,
|
||||
scope_type,
|
||||
scope_id,
|
||||
success,
|
||||
client_ip,
|
||||
additional_context
|
||||
) VALUES (
|
||||
TG_OP,
|
||||
current_setting('c77_rbac.external_id', true),
|
||||
CASE
|
||||
WHEN TG_OP = 'DELETE' THEN OLD.external_id
|
||||
ELSE NEW.external_id
|
||||
END,
|
||||
TG_ARGV[0], -- Pass role name as trigger argument
|
||||
NEW.scope_type,
|
||||
NEW.scope_id,
|
||||
true,
|
||||
inet_client_addr(),
|
||||
jsonb_build_object(
|
||||
'table', TG_TABLE_NAME,
|
||||
'when', TG_WHEN,
|
||||
'level', TG_LEVEL
|
||||
)
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
### Preventing Privilege Escalation
|
||||
|
||||
```sql
|
||||
-- Function to validate role assignments
|
||||
CREATE OR REPLACE FUNCTION validate_role_assignment()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
assigner_roles TEXT[];
|
||||
BEGIN
|
||||
-- Get roles of the user making the assignment
|
||||
SELECT array_agg(r.name) INTO assigner_roles
|
||||
FROM c77_rbac_subjects s
|
||||
JOIN c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id
|
||||
JOIN c77_rbac_roles r ON sr.role_id = r.role_id
|
||||
WHERE s.external_id = current_setting('c77_rbac.external_id', true);
|
||||
|
||||
-- Prevent non-admins from assigning admin roles
|
||||
IF NEW.role_name = 'admin' AND NOT ('admin' = ANY(assigner_roles)) THEN
|
||||
RAISE EXCEPTION 'Only admins can assign admin role'
|
||||
USING HINT = 'Contact an administrator for this operation';
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
## Multi-Tenant Architectures
|
||||
|
||||
### Strict Isolation Pattern
|
||||
|
||||
```sql
|
||||
-- Each tenant gets their own schema
|
||||
CREATE OR REPLACE FUNCTION create_tenant_schema(p_tenant_id TEXT)
|
||||
RETURNS void AS $$
|
||||
DECLARE
|
||||
schema_name TEXT;
|
||||
BEGIN
|
||||
schema_name := 'tenant_' || regexp_replace(p_tenant_id, '[^a-z0-9]', '_', 'g');
|
||||
|
||||
-- Create schema
|
||||
EXECUTE format('CREATE SCHEMA %I', schema_name);
|
||||
|
||||
-- Create standard tables in tenant schema
|
||||
EXECUTE format('
|
||||
CREATE TABLE %I.users (LIKE application.users INCLUDING ALL)',
|
||||
schema_name
|
||||
);
|
||||
|
||||
EXECUTE format('
|
||||
CREATE TABLE %I.documents (LIKE application.documents INCLUDING ALL)',
|
||||
schema_name
|
||||
);
|
||||
|
||||
-- Apply RLS policies
|
||||
PERFORM c77_rbac_apply_policy(
|
||||
format('%I.users', schema_name),
|
||||
'view_tenant_data',
|
||||
'tenant',
|
||||
'tenant_id'
|
||||
);
|
||||
|
||||
-- Grant default tenant admin role
|
||||
PERFORM c77_rbac_assign_subject(
|
||||
p_tenant_id || '_admin',
|
||||
'tenant_admin',
|
||||
'tenant',
|
||||
p_tenant_id
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
### Shared Tables Pattern
|
||||
|
||||
```sql
|
||||
-- Single table with tenant isolation
|
||||
CREATE TABLE application.documents (
|
||||
id SERIAL PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT,
|
||||
-- ... other columns
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Index for tenant isolation
|
||||
CREATE INDEX idx_documents_tenant_id ON application.documents(tenant_id);
|
||||
|
||||
-- Apply tenant isolation policy
|
||||
SELECT c77_rbac_apply_policy(
|
||||
'application.documents',
|
||||
'view_tenant_data',
|
||||
'tenant',
|
||||
'tenant_id'
|
||||
);
|
||||
|
||||
-- Ensure complete isolation with additional check
|
||||
ALTER TABLE application.documents ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation_policy ON application.documents
|
||||
FOR ALL
|
||||
USING (
|
||||
tenant_id = ANY(
|
||||
SELECT scope_id
|
||||
FROM c77_rbac_subject_roles sr
|
||||
JOIN c77_rbac_subjects s ON sr.subject_id = s.subject_id
|
||||
WHERE s.external_id = current_setting('c77_rbac.external_id', true)
|
||||
AND sr.scope_type = 'tenant'
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Hybrid Pattern (Best of Both)
|
||||
|
||||
```sql
|
||||
-- Shared tables for common data
|
||||
application.users -- All users across tenants
|
||||
application.subscriptions -- Tenant subscriptions
|
||||
application.features -- Available features
|
||||
|
||||
-- Tenant-specific schemas for isolated data
|
||||
tenant_abc.invoices
|
||||
tenant_abc.customers
|
||||
tenant_xyz.invoices
|
||||
tenant_xyz.customers
|
||||
|
||||
-- Cross-tenant reporting schema
|
||||
reporting.tenant_metrics
|
||||
reporting.usage_summary
|
||||
```
|
||||
|
||||
## Migration Strategies
|
||||
|
||||
### New Project Setup
|
||||
|
||||
```sql
|
||||
-- 1. Initial database setup
|
||||
CREATE DATABASE myapp;
|
||||
CREATE USER myapp_user WITH PASSWORD 'secure_password';
|
||||
|
||||
\c myapp
|
||||
CREATE EXTENSION c77_rbac;
|
||||
|
||||
-- 2. Create application schemas
|
||||
CREATE SCHEMA application;
|
||||
CREATE SCHEMA audit;
|
||||
CREATE SCHEMA reporting;
|
||||
|
||||
-- 3. Grant privileges
|
||||
GRANT ALL PRIVILEGES ON DATABASE myapp TO myapp_user;
|
||||
GRANT ALL ON SCHEMA application, audit, reporting TO myapp_user;
|
||||
|
||||
-- 4. Set default privileges
|
||||
ALTER DEFAULT PRIVILEGES FOR USER myapp_user
|
||||
GRANT ALL ON TABLES TO myapp_user;
|
||||
ALTER DEFAULT PRIVILEGES FOR USER myapp_user
|
||||
GRANT ALL ON SEQUENCES TO myapp_user;
|
||||
```
|
||||
|
||||
### Existing Application Migration
|
||||
|
||||
```sql
|
||||
-- Phase 1: Parallel implementation
|
||||
-- Keep existing auth while testing c77_rbac
|
||||
|
||||
-- Add tracking column
|
||||
ALTER TABLE users ADD COLUMN rbac_migrated BOOLEAN DEFAULT false;
|
||||
|
||||
-- Migrate in batches
|
||||
CREATE OR REPLACE FUNCTION migrate_users_batch(p_limit INTEGER = 1000)
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
migrated_count INTEGER := 0;
|
||||
user_record RECORD;
|
||||
BEGIN
|
||||
FOR user_record IN
|
||||
SELECT * FROM users
|
||||
WHERE NOT rbac_migrated
|
||||
ORDER BY id
|
||||
LIMIT p_limit
|
||||
LOOP
|
||||
-- Map existing roles to c77_rbac
|
||||
CASE user_record.role
|
||||
WHEN 'admin' THEN
|
||||
PERFORM c77_rbac_assign_subject(
|
||||
user_record.id::TEXT,
|
||||
'admin',
|
||||
'global',
|
||||
'all'
|
||||
);
|
||||
WHEN 'manager' THEN
|
||||
PERFORM c77_rbac_assign_subject(
|
||||
user_record.id::TEXT,
|
||||
'manager',
|
||||
'department',
|
||||
user_record.department_id::TEXT
|
||||
);
|
||||
ELSE
|
||||
PERFORM c77_rbac_assign_subject(
|
||||
user_record.id::TEXT,
|
||||
'employee',
|
||||
'department',
|
||||
user_record.department_id::TEXT
|
||||
);
|
||||
END CASE;
|
||||
|
||||
-- Mark as migrated
|
||||
UPDATE users SET rbac_migrated = true WHERE id = user_record.id;
|
||||
migrated_count := migrated_count + 1;
|
||||
END LOOP;
|
||||
|
||||
RETURN migrated_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Phase 2: Gradual table migration
|
||||
-- Start with low-risk tables
|
||||
SELECT c77_rbac_apply_policy('public_content', 'view_content', 'global', 'all');
|
||||
|
||||
-- Phase 3: Critical tables with testing
|
||||
-- Apply policies but keep old auth active
|
||||
SELECT c77_rbac_apply_policy('users', 'view_users', 'department', 'department_id');
|
||||
|
||||
-- Phase 4: Cutover
|
||||
-- Remove old auth code
|
||||
-- Remove rbac_migrated column
|
||||
ALTER TABLE users DROP COLUMN rbac_migrated;
|
||||
```
|
||||
|
||||
## Framework-Specific Patterns
|
||||
|
||||
### Laravel Integration
|
||||
|
||||
```php
|
||||
// app/Traits/UsesRbac.php
|
||||
trait UsesRbac
|
||||
{
|
||||
public static function bootUsesRbac()
|
||||
{
|
||||
// Set context on model events
|
||||
static::creating(function ($model) {
|
||||
if (Auth::check()) {
|
||||
DB::statement('SET LOCAL c77_rbac.external_id = ?', [Auth::id()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Scope for RBAC-protected queries
|
||||
public function scopeAuthorized($query)
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return $query->whereRaw('1=0'); // No results
|
||||
}
|
||||
|
||||
DB::statement('SET LOCAL c77_rbac.external_id = ?', [Auth::id()]);
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in models
|
||||
class Document extends Model
|
||||
{
|
||||
use UsesRbac;
|
||||
|
||||
// Automatically filtered by RLS
|
||||
public static function allForUser()
|
||||
{
|
||||
return static::authorized()->get();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Django Integration
|
||||
|
||||
```python
|
||||
# rbac/middleware.py
|
||||
class RbacContextMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
if request.user.is_authenticated:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SET LOCAL c77_rbac.external_id = %s",
|
||||
[str(request.user.id)]
|
||||
)
|
||||
|
||||
response = self.get_response(request)
|
||||
|
||||
# Clean up
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("RESET c77_rbac.external_id")
|
||||
|
||||
return response
|
||||
|
||||
# rbac/models.py
|
||||
class RbacProtectedModel(models.Model):
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@classmethod
|
||||
def apply_rbac_policy(cls, feature, scope_type, scope_column):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SELECT c77_rbac_apply_policy(%s, %s, %s, %s)",
|
||||
[cls._meta.db_table, feature, scope_type, scope_column]
|
||||
)
|
||||
```
|
||||
|
||||
### Rails Integration
|
||||
|
||||
```ruby
|
||||
# app/models/concerns/rbac_protected.rb
|
||||
module RbacProtected
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# Set user context before queries
|
||||
default_scope -> {
|
||||
if Current.user
|
||||
connection.execute("SET LOCAL c77_rbac.external_id = '#{Current.user.id}'")
|
||||
end
|
||||
all
|
||||
}
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def apply_rbac_policy(feature, scope_type, scope_column)
|
||||
connection.execute(
|
||||
"SELECT c77_rbac_apply_policy('#{table_name}', '#{feature}', '#{scope_type}', '#{scope_column}')"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Usage
|
||||
class Document < ApplicationRecord
|
||||
include RbacProtected
|
||||
|
||||
# Apply policy after migrations
|
||||
# Document.apply_rbac_policy('view_documents', 'department', 'dept_id')
|
||||
end
|
||||
```
|
||||
|
||||
## Monitoring and Maintenance
|
||||
|
||||
### Health Check Queries
|
||||
|
||||
```sql
|
||||
-- Daily health check function
|
||||
CREATE OR REPLACE FUNCTION daily_rbac_health_check()
|
||||
RETURNS TABLE(
|
||||
check_name TEXT,
|
||||
status TEXT,
|
||||
details TEXT,
|
||||
action_required BOOLEAN
|
||||
) AS $$
|
||||
BEGIN
|
||||
-- Check for users without roles
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
'Orphaned Users'::TEXT,
|
||||
CASE WHEN COUNT(*) > 0 THEN 'WARNING' ELSE 'OK' END,
|
||||
format('%s users without any roles', COUNT(*)),
|
||||
COUNT(*) > 0
|
||||
FROM c77_rbac_subjects s
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM c77_rbac_subject_roles sr
|
||||
WHERE sr.subject_id = s.subject_id
|
||||
);
|
||||
|
||||
-- Check for excessive permissions
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
'Permission Sprawl'::TEXT,
|
||||
CASE WHEN COUNT(*) > 50 THEN 'WARNING' ELSE 'OK' END,
|
||||
format('%s users with global admin access', COUNT(*)),
|
||||
COUNT(*) > 50
|
||||
FROM c77_rbac_subject_roles
|
||||
WHERE scope_type = 'global' AND scope_id = 'all';
|
||||
|
||||
-- Check for stale roles
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
'Unused Roles'::TEXT,
|
||||
CASE WHEN COUNT(*) > 10 THEN 'WARNING' ELSE 'OK' END,
|
||||
format('%s roles with no assigned users', COUNT(*)),
|
||||
COUNT(*) > 10
|
||||
FROM c77_rbac_roles r
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM c77_rbac_subject_roles sr
|
||||
WHERE sr.role_id = r.role_id
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Schedule via pg_cron or external scheduler
|
||||
-- SELECT cron.schedule('daily-rbac-check', '0 2 * * *', 'SELECT daily_rbac_health_check()');
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
```sql
|
||||
-- Track slow permission checks
|
||||
CREATE TABLE monitoring.slow_permission_checks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
query_start TIMESTAMP,
|
||||
duration INTERVAL,
|
||||
feature_name TEXT,
|
||||
external_id TEXT,
|
||||
scope_type TEXT,
|
||||
scope_id TEXT
|
||||
);
|
||||
|
||||
-- Monitor function with timing
|
||||
CREATE OR REPLACE FUNCTION c77_rbac_can_access_monitored(
|
||||
p_feature_name TEXT,
|
||||
p_external_id TEXT,
|
||||
p_scope_type TEXT,
|
||||
p_scope_id TEXT
|
||||
) RETURNS BOOLEAN AS $$
|
||||
DECLARE
|
||||
start_time TIMESTAMP;
|
||||
result BOOLEAN;
|
||||
duration INTERVAL;
|
||||
BEGIN
|
||||
start_time := clock_timestamp();
|
||||
|
||||
result := c77_rbac_can_access(p_feature_name, p_external_id, p_scope_type, p_scope_id);
|
||||
|
||||
duration := clock_timestamp() - start_time;
|
||||
|
||||
-- Log slow checks (> 10ms)
|
||||
IF duration > interval '10 milliseconds' THEN
|
||||
INSERT INTO monitoring.slow_permission_checks
|
||||
(query_start, duration, feature_name, external_id, scope_type, scope_id)
|
||||
VALUES (start_time, duration, p_feature_name, p_external_id, p_scope_type, p_scope_id);
|
||||
END IF;
|
||||
|
||||
RETURN result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### 1. Forgetting to Set Context
|
||||
|
||||
**Problem**: Queries return no data because context isn't set.
|
||||
|
||||
**Solution**: Always verify context is set:
|
||||
```sql
|
||||
-- Add to your application's base query class
|
||||
CREATE OR REPLACE FUNCTION verify_context_set()
|
||||
RETURNS void AS $$
|
||||
BEGIN
|
||||
IF current_setting('c77_rbac.external_id', true) IS NULL THEN
|
||||
RAISE EXCEPTION 'Security context not set'
|
||||
USING HINT = 'Call SET c77_rbac.external_id before querying';
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
```
|
||||
|
||||
### 2. Connection Pool Context Leakage
|
||||
|
||||
**Problem**: User A sees User B's data due to connection reuse.
|
||||
|
||||
**Solution**: Always reset context:
|
||||
```python
|
||||
# Python context manager
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def rbac_context(user_id):
|
||||
cursor = connection.cursor()
|
||||
try:
|
||||
cursor.execute("SAVEPOINT rbac_context")
|
||||
cursor.execute("SET LOCAL c77_rbac.external_id = %s", [user_id])
|
||||
yield cursor
|
||||
finally:
|
||||
cursor.execute("ROLLBACK TO SAVEPOINT rbac_context")
|
||||
```
|
||||
|
||||
### 3. Over-Granting Permissions
|
||||
|
||||
**Problem**: Giving users more access than needed.
|
||||
|
||||
**Solution**: Regular permission audits:
|
||||
```sql
|
||||
-- Find over-privileged users
|
||||
SELECT
|
||||
s.external_id,
|
||||
COUNT(DISTINCT f.name) as permission_count,
|
||||
array_agg(DISTINCT r.name) as roles
|
||||
FROM c77_rbac_subjects s
|
||||
JOIN c77_rbac_subject_roles sr ON s.subject_id = sr.subject_id
|
||||
JOIN c77_rbac_roles r ON sr.role_id = r.role_id
|
||||
JOIN c77_rbac_role_features rf ON r.role_id = rf.role_id
|
||||
JOIN c77_rbac_features f ON rf.feature_id = f.feature_id
|
||||
GROUP BY s.external_id
|
||||
HAVING COUNT(DISTINCT f.name) > 20 -- Threshold for investigation
|
||||
ORDER BY permission_count DESC;
|
||||
```
|
||||
|
||||
### 4. Inefficient RLS Policies
|
||||
|
||||
**Problem**: Complex policies causing slow queries.
|
||||
|
||||
**Solution**: Optimize policy conditions:
|
||||
```sql
|
||||
-- Bad: Multiple subqueries
|
||||
CREATE POLICY slow_policy ON documents
|
||||
USING (
|
||||
author_id IN (SELECT user_id FROM team_members WHERE team_id IN
|
||||
(SELECT team_id FROM user_teams WHERE user_id = current_user_id()))
|
||||
);
|
||||
|
||||
-- Good: Single optimized query
|
||||
CREATE POLICY fast_policy ON documents
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM team_members tm
|
||||
JOIN user_teams ut ON tm.team_id = ut.team_id
|
||||
WHERE tm.user_id = author_id
|
||||
AND ut.user_id = current_user_id()
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
## Production Checklist
|
||||
|
||||
### Pre-Deployment
|
||||
|
||||
- [ ] All schemas created with proper ownership
|
||||
- [ ] Indexes created on all RLS policy columns
|
||||
- [ ] Connection pool context handling implemented
|
||||
- [ ] Audit logging configured
|
||||
- [ ] Monitoring queries scheduled
|
||||
- [ ] Backup strategy includes c77_rbac tables
|
||||
- [ ] Performance baseline established
|
||||
|
||||
### Deployment
|
||||
|
||||
- [ ] Extension installed by superuser
|
||||
- [ ] Application user has required privileges
|
||||
- [ ] Initial roles and features configured
|
||||
- [ ] RLS policies applied to all sensitive tables
|
||||
- [ ] Context setting added to application layer
|
||||
- [ ] Health check queries return OK
|
||||
|
||||
### Post-Deployment
|
||||
|
||||
- [ ] Monitor slow query logs
|
||||
- [ ] Review audit logs daily for first week
|
||||
- [ ] Check for orphaned users/roles
|
||||
- [ ] Verify no permission escalation attempts
|
||||
- [ ] Document any custom policies
|
||||
- [ ] Train team on RBAC concepts
|
||||
|
||||
### Ongoing Maintenance
|
||||
|
||||
- [ ] Weekly: Run health check queries
|
||||
- [ ] Monthly: Review and clean up unused roles
|
||||
- [ ] Quarterly: Permission audit and optimization
|
||||
- [ ] Annually: Review and update security policies
|
||||
|
||||
## Conclusion
|
||||
|
||||
Following these best practices will help you build a secure, performant, and maintainable authorization system with c77_rbac. Remember that security is an ongoing process - regular monitoring and updates are essential for maintaining a robust system.
|
||||
|
||||
For specific implementation questions or advanced scenarios not covered here, refer to the main documentation or open an issue in the project repository.
|
Loading…
x
Reference in New Issue
Block a user