# Permission System Documentation The CGW Product Finder uses a WordPress-style permission system that allows fine-grained control over what users can do both globally and at specific locations. ## Overview Permissions can be set at two levels: 1. **Global Permissions**: Apply across all locations (stored in user's `permissions` field) 2. **Location-Specific Permissions**: Apply only at specific locations (stored in `locationSettings[LOCATION].permissions`) Permission checks follow this hierarchy: - First checks global permissions - Then checks location-specific permissions - Location-specific permissions can override global permissions - If a permission isn't found anywhere, it defaults to `false` ## User Structure ```json { "username": "john_doe", "password": "pbkdf2:sha256:1000000$...", "defaultLocation": "LINDS", "active": true, "permissions": { "manage_users": true, "view_reports": true, "create_quotes": true }, "locationSettings": { "LINDS": { "accessible": true, "permissions": { "manage_inventory": true, "approve_quotes": true } }, "IOLA": { "accessible": true, "permissions": { "manage_inventory": false, "approve_quotes": false } } } } ``` ## Backend Usage (Python) ### Checking Permissions in Code ```python from app import can_user # Check if user has permission at current location if can_user('create_quotes'): # User can create quotes pass # Check if user has permission at specific location if can_user('manage_inventory', location='LINDS'): # User can manage inventory at Lindsborg pass # Check only global permissions (ignore location-specific) if can_user('manage_users', location='global'): # User has global user management permission pass ``` ### Protecting Routes with Decorators ```python from app import permission_required, login_required @app.route('/admin/users') @login_required @permission_required('manage_users') def admin_users(): """Only users with manage_users permission can access""" return render_template('admin_users.html') # Check permission at specific location @app.route('/inventory/') @login_required @permission_required('manage_inventory', location_param='location') def location_inventory(location): """Permission checked for the location in URL parameter""" return render_template('inventory.html') ``` ### Multiple Permission Checks ```python from app import user_has_any_permission, user_has_all_permissions # Check if user has ANY of these permissions if user_has_any_permission(['create_quotes', 'approve_quotes']): # User can either create OR approve quotes pass # Check if user has ALL of these permissions if user_has_all_permissions(['manage_users', 'view_reports']): # User has both permissions pass ``` ### Getting All User Permissions ```python from app import get_user_permissions # Get all permissions (global + current location) permissions = get_user_permissions() # Returns: {'manage_users': True, 'create_quotes': True, ...} # Get permissions for specific location permissions = get_user_permissions(location='LINDS') # Get only global permissions permissions = get_user_permissions(location='global') ``` ## Frontend Usage (JavaScript) ### Checking Single Permission ```javascript async function checkPermission(permissionName, location = null) { try { const response = await fetch('/api/check-permission', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ permission: permissionName, location: location // Optional }) }); const data = await response.json(); return data.hasPermission; } catch (error) { console.error('Error checking permission:', error); return false; } } // Usage if (await checkPermission('create_quotes')) { // Show create quote button document.getElementById('createQuoteBtn').style.display = 'block'; } ``` ### Getting All User Permissions ```javascript async function getUserPermissions(location = null) { try { const url = location ? `/api/user-permissions?location=${location}` : '/api/user-permissions'; const response = await fetch(url); const data = await response.json(); return data.permissions; } catch (error) { console.error('Error fetching permissions:', error); return {}; } } // Usage const permissions = await getUserPermissions(); if (permissions.manage_users) { // Show admin menu } ``` ### Show/Hide Elements Based on Permissions ```javascript async function initializePermissions() { const permissions = await getUserPermissions(); // Show/hide elements document.querySelectorAll('[data-permission]').forEach(element => { const requiredPermission = element.dataset.permission; if (!permissions[requiredPermission]) { element.style.display = 'none'; } }); } // In HTML: // //
Admin Panel
``` ## Common Permissions Here are some suggested permission names for the Product Finder: ### User Management - `manage_users` - Create, edit, delete users - `view_users` - View user list - `reset_passwords` - Reset other users' passwords ### Product & Inventory - `manage_products` - Add/edit/delete products - `view_products` - View product catalog - `manage_inventory` - Adjust inventory levels - `view_inventory` - View inventory levels ### Quotes & Orders - `create_quotes` - Create new quotes - `view_quotes` - View quotes - `approve_quotes` - Approve/reject quotes - `edit_quotes` - Edit existing quotes - `delete_quotes` - Delete quotes ### Reports & Data - `view_reports` - Access reporting tools - `export_data` - Export data to CSV/Excel - `view_analytics` - View analytics dashboard ### System Settings - `manage_settings` - Change system settings - `manage_locations` - Add/edit location settings - `view_logs` - View system logs ## Permission Flow Examples ### Example 1: Creating a Quote ```python @app.route('/api/quotes', methods=['POST']) @login_required @permission_required('create_quotes') def create_quote(): # User needs create_quotes permission at their current location data = request.json # Create quote logic... return jsonify({'status': 'success'}) ``` ### Example 2: Approving Quotes (Location-Specific) ```python @app.route('/api/quotes//approve', methods=['POST']) @login_required def approve_quote(quote_id): # Check permission at the quote's location quote = get_quote(quote_id) if not can_user('approve_quotes', location=quote['location']): return render_template('access_denied.html', required_permission='approve_quotes'), 403 # Approve quote logic... return jsonify({'status': 'success'}) ``` ### Example 3: Multi-Location Access ```python @app.route('/api/inventory/transfer', methods=['POST']) @login_required def transfer_inventory(): data = request.json from_location = data['from_location'] to_location = data['to_location'] # User must have manage_inventory at BOTH locations if not user_has_all_permissions(['manage_inventory'], location=from_location): return jsonify({'error': 'No permission at source location'}), 403 if not user_has_all_permissions(['manage_inventory'], location=to_location): return jsonify({'error': 'No permission at destination location'}), 403 # Transfer logic... return jsonify({'status': 'success'}) ``` ## Access Denied Page When a user lacks permission, they see an access denied page that shows: - Clear "Access Denied" message - The specific permission that was required - Options to go back or return home - Contact information for requesting access ## Best Practices 1. **Be Specific**: Use descriptive permission names like `create_quotes` instead of `quotes` 2. **Granular Control**: Separate permissions (create, view, edit, delete) rather than one "manage" permission 3. **Check Early**: Check permissions at route level with decorators when possible 4. **Check Often**: Re-check permissions before critical operations, not just at page load 5. **Fail Secure**: Default to denying access if permission isn't explicitly granted 6. **Location Context**: Always consider whether a permission should be global or location-specific 7. **UI Feedback**: Hide/disable UI elements users can't use based on permissions 8. **Clear Errors**: Show helpful error messages when permission is denied ## Adding New Permissions To add a new permission: 1. **Define the permission** in your data structure (add to user's `permissions` or `locationSettings[LOCATION].permissions`) 2. **Protect routes** with `@permission_required('new_permission')` 3. **Check in code** with `can_user('new_permission')` 4. **Update frontend** to show/hide elements based on permission 5. **Document** the permission in this file ## Troubleshooting ### Permission check returns False but user should have access - Check if permission is spelled correctly (case-sensitive) - Verify user is logged in (`session['user_id']` exists) - Check user's `active` status - Verify permission exists in either global or location-specific permissions - Check if using correct location (current vs specific vs global) ### Access denied page shows even for users with permission - Ensure decorators are in correct order: `@login_required` before `@permission_required` - Check that permission name matches exactly - Verify user data was saved correctly in users.json - Clear browser cache/cookies if session is stale ### Frontend shows elements but backend denies access - Frontend permission checks are for UX only - always enforce in backend - Make sure frontend is checking the same permission name - Ensure frontend is checking at the same location context ## Security Notes - **Never trust frontend permission checks** - they're for UI only - **Always validate permissions on the backend** before performing operations - **Session security** - permissions are loaded from session, which is server-side - **Password hashing** - uses PBKDF2-SHA256 with 1M iterations - **HTTP-only cookies** - session cookies cannot be accessed by JavaScript - **Permission inheritance** - location-specific permissions override global ones ## Migration Guide If you have existing users without permissions, you can add default permissions: ```python import json def add_default_permissions(): with open('data/users.json', 'r') as f: users = json.load(f) for user in users: # Add global permissions if missing if 'permissions' not in user: user['permissions'] = { 'view_products': True, 'create_quotes': True, 'manage_users': False # Admin only } # Add location permissions if missing for location in user.get('locationSettings', {}): if 'permissions' not in user['locationSettings'][location]: user['locationSettings'][location]['permissions'] = { 'manage_inventory': False, 'approve_quotes': False } with open('data/users.json', 'w') as f: json.dump(users, f, indent=2) ```