phase1: RLS simplified to tenant isolation only + canAccess fallback removed + useUserPermissions hook + security kernel docs
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"""Simplify RLS to pure tenant isolation.
|
||||
|
||||
Per architecture review: RLS should be the "safety belt" (tenant isolation only),
|
||||
NOT the "vehicle control" (business authorization). Business authorization
|
||||
(owner_id, sharing, entity_permissions) belongs in the application layer
|
||||
(visibility.py with Defense-in-Depth tenant_id filter).
|
||||
|
||||
Revision ID: 0069
|
||||
Revises: 0068
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
revision = "0069"
|
||||
down_revision = "0068"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
RLS_TABLES = [
|
||||
"contacts", "addresses", "attachments", "bank_accounts",
|
||||
"contact_folders", "contact_folder_permissions", "entity_permissions",
|
||||
"entity_policies", "event_outbox", "audit_log", "notifications",
|
||||
"saved_filters", "saved_views", "webhooks", "workflow_instances",
|
||||
"workflow_step_history", "sequences", "custom_field_definitions",
|
||||
"custom_field_values", "guest_users", "guest_invitations",
|
||||
"consumer_inbox", "tenant_plugin_activation", "permission_templates",
|
||||
"permission_delegations", "dms_files", "dms_folders",
|
||||
"calendar_events", "calendars", "tasks", "task_lists",
|
||||
"messages", "channels", "entity_links", "tags", "tag_assignments",
|
||||
"mail_accounts", "mail_messages", "mail_folders",
|
||||
"report_templates", "report_generations", "ai_conversations",
|
||||
"ai_messages", "automation_workflows", "automation_runs",
|
||||
"mcp_server_configs", "mcp_client_configs", "system_notifications",
|
||||
]
|
||||
|
||||
CONTACTS_POLICIES_TO_DROP = [
|
||||
"contacts_admin_select", "contacts_owner_select",
|
||||
"contacts_shared_select", "contacts_tenant_owned_select",
|
||||
"contacts_delete_policy", "contacts_insert_policy",
|
||||
"contacts_update_policy",
|
||||
]
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# 1. Drop all business-logic RLS policies on contacts
|
||||
for policy in CONTACTS_POLICIES_TO_DROP:
|
||||
op.execute(f"DROP POLICY IF EXISTS {policy} ON contacts")
|
||||
|
||||
# 2. Drop old tenant_isolation policy on contacts
|
||||
op.execute("DROP POLICY IF EXISTS contacts_tenant_isolation ON contacts")
|
||||
|
||||
# 3. Create simple tenant isolation for ALL operations on contacts
|
||||
op.execute(
|
||||
"CREATE POLICY contacts_tenant_isolation ON contacts "
|
||||
"FOR ALL "
|
||||
"USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid) "
|
||||
"WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid)"
|
||||
)
|
||||
|
||||
# 4. For all other RLS tables: drop existing policies, create simple tenant isolation
|
||||
for table in RLS_TABLES:
|
||||
if table == "contacts":
|
||||
continue
|
||||
|
||||
# Check if table exists first
|
||||
table_exists = conn.execute(
|
||||
text(f"SELECT 1 FROM information_schema.tables WHERE table_name = '{table}'")
|
||||
).fetchone() is not None
|
||||
|
||||
if not table_exists:
|
||||
continue
|
||||
|
||||
# Get all existing policies on this table
|
||||
result = conn.execute(
|
||||
text(f"SELECT polname FROM pg_policy WHERE polrelid = '{table}'::regclass")
|
||||
)
|
||||
policies = [row[0] for row in result]
|
||||
|
||||
# Drop each policy
|
||||
for policy in policies:
|
||||
op.execute(f'DROP POLICY IF EXISTS "{policy}" ON {table}')
|
||||
|
||||
# Check if table has tenant_id column
|
||||
col_result = conn.execute(
|
||||
text(f"SELECT 1 FROM information_schema.columns "
|
||||
f"WHERE table_name = '{table}' AND column_name = 'tenant_id'")
|
||||
)
|
||||
has_tenant_id = col_result.fetchone() is not None
|
||||
|
||||
if has_tenant_id:
|
||||
op.execute(
|
||||
f"CREATE POLICY {table}_tenant_isolation ON {table} "
|
||||
"FOR ALL "
|
||||
"USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid) "
|
||||
"WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid)"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
Reference in New Issue
Block a user