Add Entra ID authentication with role-based access control

Implement Microsoft Entra ID (Azure AD) authentication with user and admin roles.

Features:
- OAuth 2.0 authentication flow using MSAL
- User and admin role-based access control
- Tenant ownership and access management
- Server-side session storage
- Protected routes with authentication decorators

Authentication:
- Users authenticate via Microsoft organizational accounts
- Admin role automatically assigned to DEFAULT_ADMIN_EMAIL
- User profiles synced from Microsoft Graph API

Access Control:
- Admins: Full access to all tenants and server settings
- Users: Can create and manage their own tenants only
- Tenant association: Auto-created when user adds products
- API endpoints remain open for AR application access

Database Changes:
- New users table with email, azure_oid, name, and role
- New user_tenants table for many-to-many tenant ownership
- Case-insensitive admin email comparison

Configuration:
- Environment-based config via .env file
- Azure AD credentials (client ID, secret, tenant ID)
- Configurable redirect URI and admin email
- Updated to Flask 2.2.5 for compatibility
This commit is contained in:
2025-10-20 19:36:52 -04:00
parent 32ce69057b
commit 795068c015
17 changed files with 775 additions and 15 deletions

View File

@@ -1,10 +1,12 @@
from flask import render_template, request, redirect, flash, jsonify, Response, current_app
from flask import render_template, request, redirect, flash, jsonify, Response, current_app, session
from . import tenant_bp
from app.models import TenantModel, ProductModel, ARFieldModel, SettingsModel
from app.models import TenantModel, ProductModel, ARFieldModel, SettingsModel, UserModel
from app.services import AuthService, ProductService, BarcodeService
from app.decorators.auth import tenant_access_required
import os
@tenant_bp.route('/')
@tenant_access_required
def index(tenant_id):
"""Tenant home page - product listing"""
tenant = TenantModel.get_or_create(tenant_id)
@@ -17,13 +19,19 @@ def index(tenant_id):
return render_template('index.html', tenant=tenant, products=products, custom_fields=custom_fields)
@tenant_bp.route('/delete', methods=['POST'])
@tenant_access_required
def delete_tenant(tenant_id):
"""Delete a tenant and all its data"""
# Also remove the user's association with this tenant
user_id = session['user']['id']
UserModel.remove_tenant(user_id, tenant_id)
TenantModel.delete(tenant_id)
flash(f'Tenant "{tenant_id}" has been deleted successfully.', 'success')
return redirect('/')
@tenant_bp.route('/settings')
@tenant_access_required
def settings(tenant_id):
"""Tenant settings page"""
tenant = TenantModel.get_or_create(tenant_id)
@@ -36,6 +44,7 @@ def settings(tenant_id):
return render_template('tenant_settings.html', tenant=tenant, custom_fields=custom_fields, server_url=server_url)
@tenant_bp.route('/settings/credentials', methods=['POST'])
@tenant_access_required
def update_credentials(tenant_id):
"""Update tenant credentials"""
tenant = TenantModel.get_by_id(tenant_id)
@@ -54,6 +63,7 @@ def update_credentials(tenant_id):
return redirect(f'/{tenant_id}/settings')
@tenant_bp.route('/settings/barcode', methods=['POST'])
@tenant_access_required
def update_barcode_type(tenant_id):
"""Update tenant barcode type"""
tenant = TenantModel.get_by_id(tenant_id)