diff --git a/check_db_status.py b/check_db_status.py new file mode 100755 index 0000000..df49eaa --- /dev/null +++ b/check_db_status.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +""" +Database Migration Status Checker +Checks what migration steps have been completed and what's missing +""" +import sys +import os +from sqlalchemy import create_engine, text, inspect +from sqlalchemy.orm import sessionmaker +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Get database URL from environment or use provided one +DATABASE_URL = os.getenv('DATABASE_URL') + +if not DATABASE_URL: + print("ERROR: DATABASE_URL not found in environment") + sys.exit(1) + +# Create database connection +engine = create_engine(DATABASE_URL) +Session = sessionmaker(bind=engine) +db = Session() +inspector = inspect(engine) + +print("=" * 80) +print("DATABASE MIGRATION STATUS CHECKER") +print("=" * 80) +print(f"\nConnected to: {DATABASE_URL.split('@')[1] if '@' in DATABASE_URL else 'database'}") +print() + +# Colors for output +class Colors: + GREEN = '\033[92m' + RED = '\033[91m' + YELLOW = '\033[93m' + BLUE = '\033[94m' + END = '\033[0m' + +def check_mark(exists): + return f"{Colors.GREEN}✓{Colors.END}" if exists else f"{Colors.RED}✗{Colors.END}" + +def warning(exists): + return f"{Colors.YELLOW}⚠{Colors.END}" if exists else f"{Colors.GREEN}✓{Colors.END}" + +issues = [] +warnings = [] + +# ============================================================ +# Check 1: Does roles table exist? +# ============================================================ +print(f"{Colors.BLUE}[1] Checking if 'roles' table exists...{Colors.END}") +roles_table_exists = 'roles' in inspector.get_table_names() +print(f" {check_mark(roles_table_exists)} roles table exists") + +if not roles_table_exists: + issues.append("❌ MISSING: 'roles' table - run migration 006_add_dynamic_roles.sql") + print(f"\n{Colors.RED}ISSUE: roles table not found!{Colors.END}") + print(f" Action: Run 'psql $DATABASE_URL -f migrations/006_add_dynamic_roles.sql'") + +# ============================================================ +# Check 2: Does users table have role_id column? +# ============================================================ +print(f"\n{Colors.BLUE}[2] Checking if 'users' table has 'role_id' column...{Colors.END}") +users_columns = [col['name'] for col in inspector.get_columns('users')] +users_has_role_id = 'role_id' in users_columns +print(f" {check_mark(users_has_role_id)} users.role_id column exists") + +if not users_has_role_id: + issues.append("❌ MISSING: 'users.role_id' column - run migration 006_add_dynamic_roles.sql") + print(f"\n{Colors.RED}ISSUE: users.role_id column not found!{Colors.END}") + print(f" Action: Run 'psql $DATABASE_URL -f migrations/006_add_dynamic_roles.sql'") + +# ============================================================ +# Check 3: Does role_permissions table have role_id column? +# ============================================================ +print(f"\n{Colors.BLUE}[3] Checking if 'role_permissions' table has 'role_id' column...{Colors.END}") +rp_columns = [col['name'] for col in inspector.get_columns('role_permissions')] +rp_has_role_id = 'role_id' in rp_columns +print(f" {check_mark(rp_has_role_id)} role_permissions.role_id column exists") + +if not rp_has_role_id: + issues.append("❌ MISSING: 'role_permissions.role_id' column - run migration 006_add_dynamic_roles.sql") + print(f"\n{Colors.RED}ISSUE: role_permissions.role_id column not found!{Colors.END}") + print(f" Action: Run 'psql $DATABASE_URL -f migrations/006_add_dynamic_roles.sql'") + +# ============================================================ +# Check 4: Are system roles seeded? +# ============================================================ +if roles_table_exists: + print(f"\n{Colors.BLUE}[4] Checking if system roles are seeded...{Colors.END}") + result = db.execute(text("SELECT COUNT(*) as count FROM roles WHERE is_system_role = true")) + system_roles_count = result.scalar() + print(f" System roles found: {system_roles_count}") + + if system_roles_count == 0: + issues.append("❌ MISSING: System roles not seeded - run roles_seed.py") + print(f" {Colors.RED}✗ No system roles found!{Colors.END}") + print(f" Action: Run 'python3 roles_seed.py'") + elif system_roles_count < 5: + warnings.append(f"⚠️ WARNING: Expected 5 system roles, found {system_roles_count}") + print(f" {Colors.YELLOW}⚠{Colors.END} Expected 5 roles, found {system_roles_count}") + print(f" Action: Run 'python3 roles_seed.py' to ensure all roles exist") + else: + print(f" {Colors.GREEN}✓{Colors.END} All system roles seeded") + + # Show which roles exist + result = db.execute(text("SELECT code, name FROM roles WHERE is_system_role = true ORDER BY code")) + existing_roles = result.fetchall() + if existing_roles: + print(f"\n Existing roles:") + for role in existing_roles: + print(f" - {role[0]}: {role[1]}") + +# ============================================================ +# Check 5: Are users migrated to dynamic roles? +# ============================================================ +if users_has_role_id and roles_table_exists: + print(f"\n{Colors.BLUE}[5] Checking if users are migrated to dynamic roles...{Colors.END}") + + # Count total users + result = db.execute(text("SELECT COUNT(*) FROM users")) + total_users = result.scalar() + print(f" Total users: {total_users}") + + # Count users with role_id set + result = db.execute(text("SELECT COUNT(*) FROM users WHERE role_id IS NOT NULL")) + migrated_users = result.scalar() + print(f" Migrated users (with role_id): {migrated_users}") + + # Count users without role_id + unmigrated_users = total_users - migrated_users + + if unmigrated_users > 0: + issues.append(f"❌ INCOMPLETE: {unmigrated_users} users not migrated to dynamic roles") + print(f" {Colors.RED}✗ {unmigrated_users} users still need migration!{Colors.END}") + print(f" Action: Run 'python3 migrate_users_to_dynamic_roles.py'") + + # Show sample unmigrated users + result = db.execute(text(""" + SELECT email, role FROM users + WHERE role_id IS NULL + LIMIT 5 + """)) + unmigrated = result.fetchall() + if unmigrated: + print(f"\n Sample unmigrated users:") + for user in unmigrated: + print(f" - {user[0]} (role: {user[1]})") + else: + print(f" {Colors.GREEN}✓{Colors.END} All users migrated to dynamic roles") + +# ============================================================ +# Check 6: Are role permissions migrated? +# ============================================================ +if rp_has_role_id and roles_table_exists: + print(f"\n{Colors.BLUE}[6] Checking if role permissions are migrated...{Colors.END}") + + # Count total role_permissions + result = db.execute(text("SELECT COUNT(*) FROM role_permissions")) + total_perms = result.scalar() + print(f" Total role_permissions: {total_perms}") + + # Count permissions with role_id set + result = db.execute(text("SELECT COUNT(*) FROM role_permissions WHERE role_id IS NOT NULL")) + migrated_perms = result.scalar() + print(f" Migrated permissions (with role_id): {migrated_perms}") + + unmigrated_perms = total_perms - migrated_perms + + if unmigrated_perms > 0: + issues.append(f"❌ INCOMPLETE: {unmigrated_perms} permissions not migrated to dynamic roles") + print(f" {Colors.RED}✗ {unmigrated_perms} permissions still need migration!{Colors.END}") + print(f" Action: Run 'python3 migrate_role_permissions_to_dynamic_roles.py'") + else: + print(f" {Colors.GREEN}✓{Colors.END} All permissions migrated to dynamic roles") + +# ============================================================ +# Check 7: Verify admin account +# ============================================================ +print(f"\n{Colors.BLUE}[7] Checking admin account...{Colors.END}") +result = db.execute(text(""" + SELECT email, role, role_id + FROM users + WHERE email LIKE '%admin%' OR role = 'admin' OR role = 'superadmin' + LIMIT 5 +""")) +admin_users = result.fetchall() + +if admin_users: + print(f" Found {len(admin_users)} admin/superadmin users:") + for user in admin_users: + role_id_status = "✓" if user[2] else "✗" + print(f" {role_id_status} {user[0]} (role: {user[1]}, role_id: {user[2] or 'NULL'})") +else: + warnings.append("⚠️ WARNING: No admin users found") + print(f" {Colors.YELLOW}⚠{Colors.END} No admin users found in database") + +# ============================================================ +# Summary +# ============================================================ +print("\n" + "=" * 80) +print("SUMMARY") +print("=" * 80) + +if not issues and not warnings: + print(f"\n{Colors.GREEN}✓ All migration steps completed successfully!{Colors.END}") + print("\nNext steps:") + print(" 1. Deploy latest backend code") + print(" 2. Restart backend server") + print(" 3. Test /api/admin/users/export endpoint") +else: + if issues: + print(f"\n{Colors.RED}ISSUES FOUND ({len(issues)}):{Colors.END}") + for i, issue in enumerate(issues, 1): + print(f" {i}. {issue}") + + if warnings: + print(f"\n{Colors.YELLOW}WARNINGS ({len(warnings)}):{Colors.END}") + for i, warning in enumerate(warnings, 1): + print(f" {i}. {warning}") + + print(f"\n{Colors.BLUE}RECOMMENDED ACTIONS:{Colors.END}") + if not roles_table_exists or not users_has_role_id or not rp_has_role_id: + print(" 1. Run: psql $DATABASE_URL -f migrations/006_add_dynamic_roles.sql") + if roles_table_exists and system_roles_count == 0: + print(" 2. Run: python3 roles_seed.py") + if unmigrated_users > 0: + print(" 3. Run: python3 migrate_users_to_dynamic_roles.py") + if unmigrated_perms > 0: + print(" 4. Run: python3 migrate_role_permissions_to_dynamic_roles.py") + print(" 5. Deploy latest backend code and restart server") + +print("\n" + "=" * 80) + +db.close() diff --git a/check_permissions.py b/check_permissions.py new file mode 100644 index 0000000..ec59c14 --- /dev/null +++ b/check_permissions.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +""" +Check permissions table status +""" +import sys +import os +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +from dotenv import load_dotenv + +load_dotenv() +DATABASE_URL = os.getenv('DATABASE_URL') + +engine = create_engine(DATABASE_URL) +Session = sessionmaker(bind=engine) +db = Session() + +print("Checking permissions table...") +print("=" * 80) + +# Check if permissions table exists +result = db.execute(text("SELECT COUNT(*) FROM permissions")) +count = result.scalar() + +print(f"Total permissions in database: {count}") + +if count > 0: + print("\nSample permissions:") + result = db.execute(text("SELECT code, name, module FROM permissions LIMIT 10")) + for perm in result.fetchall(): + print(f" - {perm[0]}: {perm[1]} (module: {perm[2]})") +else: + print("\n⚠️ WARNING: Permissions table is EMPTY!") + print("\nThis will cause permission checks to fail.") + print("\nAction needed: Run 'python3 seed_permissions.py'") + +db.close() diff --git a/seed_permissions_rbac.py b/seed_permissions_rbac.py new file mode 100755 index 0000000..f3a01e6 --- /dev/null +++ b/seed_permissions_rbac.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +""" +Permission Seeding Script for Dynamic RBAC System + +This script populates the database with 56 granular permissions and assigns them +to the appropriate dynamic roles (not the old enum roles). + +Usage: + python3 seed_permissions_rbac.py + +Environment Variables: + DATABASE_URL - PostgreSQL connection string +""" + +import os +import sys +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from database import Base +from models import Permission, RolePermission, Role +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +# Database connection +DATABASE_URL = os.getenv("DATABASE_URL") +if not DATABASE_URL: + print("Error: DATABASE_URL environment variable not set") + sys.exit(1) + +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# ============================================================ +# Permission Definitions (56 permissions across 9 modules) +# ============================================================ + +PERMISSIONS = [ + # ========== USERS MODULE (10) ========== + {"code": "users.view", "name": "View Users", "description": "View user list and user profiles", "module": "users"}, + {"code": "users.create", "name": "Create Users", "description": "Create new users and send invitations", "module": "users"}, + {"code": "users.edit", "name": "Edit Users", "description": "Edit user profiles and information", "module": "users"}, + {"code": "users.delete", "name": "Delete Users", "description": "Delete user accounts", "module": "users"}, + {"code": "users.status", "name": "Change User Status", "description": "Change user status (active, inactive, etc.)", "module": "users"}, + {"code": "users.approve", "name": "Approve/Validate Users", "description": "Approve or validate user applications", "module": "users"}, + {"code": "users.export", "name": "Export Users", "description": "Export user data to CSV", "module": "users"}, + {"code": "users.import", "name": "Import Users", "description": "Import users from CSV", "module": "users"}, + {"code": "users.reset_password", "name": "Reset User Password", "description": "Reset user passwords via email", "module": "users"}, + {"code": "users.resend_verification", "name": "Resend Verification Email", "description": "Resend email verification links", "module": "users"}, + {"code": "users.invite", "name": "Invite Users", "description": "Send user invitations", "module": "users"}, + + # ========== EVENTS MODULE (8) ========== + {"code": "events.view", "name": "View Events", "description": "View event list and event details", "module": "events"}, + {"code": "events.create", "name": "Create Events", "description": "Create new events", "module": "events"}, + {"code": "events.edit", "name": "Edit Events", "description": "Edit existing events", "module": "events"}, + {"code": "events.delete", "name": "Delete Events", "description": "Delete events", "module": "events"}, + {"code": "events.publish", "name": "Publish Events", "description": "Publish or unpublish events", "module": "events"}, + {"code": "events.attendance", "name": "Mark Event Attendance", "description": "Mark user attendance for events", "module": "events"}, + {"code": "events.rsvps", "name": "View Event RSVPs", "description": "View and manage event RSVPs", "module": "events"}, + {"code": "events.calendar_export", "name": "Export Event Calendar", "description": "Export events to iCal format", "module": "events"}, + + # ========== SUBSCRIPTIONS MODULE (6) ========== + {"code": "subscriptions.view", "name": "View Subscriptions", "description": "View subscription list and details", "module": "subscriptions"}, + {"code": "subscriptions.create", "name": "Create Subscriptions", "description": "Create manual subscriptions for users", "module": "subscriptions"}, + {"code": "subscriptions.edit", "name": "Edit Subscriptions", "description": "Edit subscription details", "module": "subscriptions"}, + {"code": "subscriptions.cancel", "name": "Cancel Subscriptions", "description": "Cancel user subscriptions", "module": "subscriptions"}, + {"code": "subscriptions.activate", "name": "Activate Subscriptions", "description": "Manually activate subscriptions", "module": "subscriptions"}, + {"code": "subscriptions.plans", "name": "Manage Subscription Plans", "description": "Create and edit subscription plans", "module": "subscriptions"}, + + # ========== FINANCIALS MODULE (6) ========== + {"code": "financials.view", "name": "View Financial Reports", "description": "View financial reports and dashboards", "module": "financials"}, + {"code": "financials.create", "name": "Create Financial Reports", "description": "Upload and create financial reports", "module": "financials"}, + {"code": "financials.edit", "name": "Edit Financial Reports", "description": "Edit existing financial reports", "module": "financials"}, + {"code": "financials.delete", "name": "Delete Financial Reports", "description": "Delete financial reports", "module": "financials"}, + {"code": "financials.export", "name": "Export Financial Data", "description": "Export financial data to CSV/PDF", "module": "financials"}, + {"code": "financials.payments", "name": "View Payment Details", "description": "View detailed payment information", "module": "financials"}, + + # ========== NEWSLETTERS MODULE (6) ========== + {"code": "newsletters.view", "name": "View Newsletters", "description": "View newsletter archives", "module": "newsletters"}, + {"code": "newsletters.create", "name": "Create Newsletters", "description": "Upload and create newsletters", "module": "newsletters"}, + {"code": "newsletters.edit", "name": "Edit Newsletters", "description": "Edit existing newsletters", "module": "newsletters"}, + {"code": "newsletters.delete", "name": "Delete Newsletters", "description": "Delete newsletter archives", "module": "newsletters"}, + {"code": "newsletters.send", "name": "Send Newsletters", "description": "Send newsletter emails to subscribers", "module": "newsletters"}, + {"code": "newsletters.subscribers", "name": "Manage Newsletter Subscribers", "description": "View and manage newsletter subscribers", "module": "newsletters"}, + + # ========== BYLAWS MODULE (5) ========== + {"code": "bylaws.view", "name": "View Bylaws", "description": "View organization bylaws documents", "module": "bylaws"}, + {"code": "bylaws.create", "name": "Create Bylaws", "description": "Upload new bylaws documents", "module": "bylaws"}, + {"code": "bylaws.edit", "name": "Edit Bylaws", "description": "Edit existing bylaws documents", "module": "bylaws"}, + {"code": "bylaws.delete", "name": "Delete Bylaws", "description": "Delete bylaws documents", "module": "bylaws"}, + {"code": "bylaws.publish", "name": "Publish Bylaws", "description": "Mark bylaws as current/published version", "module": "bylaws"}, + + # ========== GALLERY MODULE (5) ========== + {"code": "gallery.view", "name": "View Event Gallery", "description": "View event gallery photos", "module": "gallery"}, + {"code": "gallery.upload", "name": "Upload Photos", "description": "Upload photos to event galleries", "module": "gallery"}, + {"code": "gallery.edit", "name": "Edit Photos", "description": "Edit photo captions and details", "module": "gallery"}, + {"code": "gallery.delete", "name": "Delete Photos", "description": "Delete photos from galleries", "module": "gallery"}, + {"code": "gallery.moderate", "name": "Moderate Gallery Content", "description": "Approve/reject uploaded photos", "module": "gallery"}, + + # ========== SETTINGS MODULE (6) ========== + {"code": "settings.view", "name": "View Settings", "description": "View application settings", "module": "settings"}, + {"code": "settings.edit", "name": "Edit Settings", "description": "Edit application settings", "module": "settings"}, + {"code": "settings.email_templates", "name": "Manage Email Templates", "description": "Edit email templates and notifications", "module": "settings"}, + {"code": "settings.storage", "name": "Manage Storage", "description": "View and manage storage usage", "module": "settings"}, + {"code": "settings.backup", "name": "Backup & Restore", "description": "Create and restore database backups", "module": "settings"}, + {"code": "settings.logs", "name": "View System Logs", "description": "View application and audit logs", "module": "settings"}, + + # ========== PERMISSIONS MODULE (4) ========== + {"code": "permissions.view", "name": "View Permissions", "description": "View permission definitions and assignments", "module": "permissions"}, + {"code": "permissions.assign", "name": "Assign Permissions", "description": "Assign permissions to roles", "module": "permissions"}, + {"code": "permissions.manage_roles", "name": "Manage Roles", "description": "Create and manage user roles", "module": "permissions"}, + {"code": "permissions.audit", "name": "View Permission Audit Log", "description": "View permission change audit logs", "module": "permissions"}, +] + +# Default permission assignments for dynamic roles +DEFAULT_ROLE_PERMISSIONS = { + "guest": [], # Guests have no permissions + + "member": [ + # Members can view public content + "events.view", "events.rsvps", "events.calendar_export", + "newsletters.view", "bylaws.view", "gallery.view", + ], + + "finance": [ + # Finance role has financial permissions + some user viewing + "users.view", "financials.view", "financials.create", "financials.edit", + "financials.delete", "financials.export", "financials.payments", + "subscriptions.view", "subscriptions.create", "subscriptions.edit", + "subscriptions.cancel", "subscriptions.activate", "subscriptions.plans", + ], + + "admin": [ + # Admins have most permissions except RBAC management + "users.view", "users.create", "users.edit", "users.status", "users.approve", + "users.export", "users.import", "users.reset_password", "users.resend_verification", + "users.invite", + "events.view", "events.create", "events.edit", "events.delete", "events.publish", + "events.attendance", "events.rsvps", "events.calendar_export", + "subscriptions.view", "subscriptions.create", "subscriptions.edit", + "subscriptions.cancel", "subscriptions.activate", "subscriptions.plans", + "financials.view", "financials.create", "financials.edit", "financials.delete", + "financials.export", "financials.payments", + "newsletters.view", "newsletters.create", "newsletters.edit", "newsletters.delete", + "newsletters.send", "newsletters.subscribers", + "bylaws.view", "bylaws.create", "bylaws.edit", "bylaws.delete", "bylaws.publish", + "gallery.view", "gallery.upload", "gallery.edit", "gallery.delete", "gallery.moderate", + "settings.view", "settings.edit", "settings.email_templates", "settings.storage", + "settings.logs", + ], + + "superadmin": [ + # Superadmin gets ALL permissions + *[p["code"] for p in PERMISSIONS] + ] +} + + +def seed_permissions(): + """Seed permissions and assign them to dynamic roles""" + db = SessionLocal() + + try: + print("=" * 80) + print("🌱 PERMISSION SEEDING FOR DYNAMIC RBAC SYSTEM") + print("=" * 80) + + # Step 1: Clear existing permissions and role_permissions + print("\n📦 Clearing existing permissions and role assignments...") + deleted_rp = db.query(RolePermission).delete() + deleted_p = db.query(Permission).delete() + db.commit() + print(f"✓ Cleared {deleted_rp} role-permission mappings") + print(f"✓ Cleared {deleted_p} permissions") + + # Step 2: Create permissions + print(f"\n📝 Creating {len(PERMISSIONS)} permissions...") + permission_map = {} # Map code to permission object + + for perm_data in PERMISSIONS: + permission = Permission( + code=perm_data["code"], + name=perm_data["name"], + description=perm_data["description"], + module=perm_data["module"] + ) + db.add(permission) + permission_map[perm_data["code"]] = permission + + db.commit() + print(f"✓ Created {len(PERMISSIONS)} permissions") + + # Step 3: Get all roles from database + print("\n🔍 Fetching dynamic roles...") + roles = db.query(Role).all() + role_map = {role.code: role for role in roles} + print(f"✓ Found {len(roles)} roles: {', '.join(role_map.keys())}") + + # Step 4: Assign permissions to roles + print("\n🔐 Assigning permissions to roles...") + + from models import UserRole # Import for enum mapping + + # Enum mapping for backward compatibility + role_enum_map = { + 'guest': UserRole.guest, + 'member': UserRole.member, + 'admin': UserRole.admin, + 'superadmin': UserRole.superadmin, + 'finance': UserRole.admin # Finance uses admin enum for now + } + + total_assigned = 0 + for role_code, permission_codes in DEFAULT_ROLE_PERMISSIONS.items(): + if role_code not in role_map: + print(f" ⚠️ Warning: Role '{role_code}' not found in database, skipping") + continue + + role = role_map[role_code] + role_enum = role_enum_map.get(role_code, UserRole.guest) + + for perm_code in permission_codes: + if perm_code not in permission_map: + print(f" ⚠️ Warning: Permission '{perm_code}' not found") + continue + + role_permission = RolePermission( + role=role_enum, # Legacy enum for backward compatibility + role_id=role.id, # New dynamic role system + permission_id=permission_map[perm_code].id + ) + db.add(role_permission) + total_assigned += 1 + + db.commit() + print(f" ✓ {role.name}: Assigned {len(permission_codes)} permissions") + + # Step 5: Summary + print("\n" + "=" * 80) + print("📊 SEEDING SUMMARY") + print("=" * 80) + + # Count permissions by module + modules = {} + for perm in PERMISSIONS: + module = perm["module"] + modules[module] = modules.get(module, 0) + 1 + + print("\nPermissions by module:") + for module, count in sorted(modules.items()): + print(f" • {module.capitalize()}: {count} permissions") + + print(f"\nTotal permissions created: {len(PERMISSIONS)}") + print(f"Total role-permission mappings: {total_assigned}") + print("\n✅ Permission seeding completed successfully!") + print("\nNext step: Restart backend server") + print("=" * 80) + + except Exception as e: + db.rollback() + print(f"\n❌ Error seeding permissions: {str(e)}") + import traceback + traceback.print_exc() + raise + finally: + db.close() + + +if __name__ == "__main__": + seed_permissions()