Add BEST-PRACTICE document

This commit is contained in:
trogers1884 2025-05-25 09:48:58 -05:00
parent 00dbfd9dd3
commit 0fa803cf9b

874
BEST-PRACTICE.md Normal file
View 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.