Initial
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
# 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/<location>')
|
||||
@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:
|
||||
// <button data-permission="create_quotes">Create Quote</button>
|
||||
// <div data-permission="manage_users">Admin Panel</div>
|
||||
```
|
||||
|
||||
## 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/<quote_id>/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)
|
||||
```
|
||||
Reference in New Issue
Block a user