Files
CGW-Quote-Builder/information/PERMISSIONS_SYSTEM.md
T
2026-04-11 00:04:09 -05:00

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:

  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

{
  "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 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

@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

  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:

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)