12 KiB
12 KiB
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:
- Global Permissions: Apply across all locations (stored in user's
permissionsfield) - 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
{
"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
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
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
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
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
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
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
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 usersview_users- View user listreset_passwords- Reset other users' passwords
Product & Inventory
manage_products- Add/edit/delete productsview_products- View product catalogmanage_inventory- Adjust inventory levelsview_inventory- View inventory levels
Quotes & Orders
create_quotes- Create new quotesview_quotes- View quotesapprove_quotes- Approve/reject quotesedit_quotes- Edit existing quotesdelete_quotes- Delete quotes
Reports & Data
view_reports- Access reporting toolsexport_data- Export data to CSV/Excelview_analytics- View analytics dashboard
System Settings
manage_settings- Change system settingsmanage_locations- Add/edit location settingsview_logs- View system logs
Permission Flow Examples
Example 1: Creating a Quote
@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)
@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
@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
- Be Specific: Use descriptive permission names like
create_quotesinstead ofquotes - Granular Control: Separate permissions (create, view, edit, delete) rather than one "manage" permission
- Check Early: Check permissions at route level with decorators when possible
- Check Often: Re-check permissions before critical operations, not just at page load
- Fail Secure: Default to denying access if permission isn't explicitly granted
- Location Context: Always consider whether a permission should be global or location-specific
- UI Feedback: Hide/disable UI elements users can't use based on permissions
- Clear Errors: Show helpful error messages when permission is denied
Adding New Permissions
To add a new permission:
- Define the permission in your data structure (add to user's
permissionsorlocationSettings[LOCATION].permissions) - Protect routes with
@permission_required('new_permission') - Check in code with
can_user('new_permission') - Update frontend to show/hide elements based on permission
- 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
activestatus - 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_requiredbefore@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:
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)