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
92 lines
3.0 KiB
Python
92 lines
3.0 KiB
Python
"""Authentication and authorization decorators"""
|
|
from functools import wraps
|
|
from flask import session, redirect, url_for, request, abort, current_app
|
|
from app.models.user import UserModel
|
|
|
|
|
|
def login_required(f):
|
|
"""
|
|
Decorator to require user authentication.
|
|
Redirects to login if user is not authenticated.
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if 'user' not in session:
|
|
# Store the requested URL to redirect back after login
|
|
session['next'] = request.url
|
|
return redirect(url_for('auth.login'))
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
|
|
def admin_required(f):
|
|
"""
|
|
Decorator to require admin role.
|
|
Returns 403 if user is not an admin.
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if 'user' not in session:
|
|
session['next'] = request.url
|
|
return redirect(url_for('auth.login'))
|
|
|
|
user_id = session['user'].get('id')
|
|
if not UserModel.is_admin(user_id):
|
|
current_app.logger.warning(f"User {user_id} attempted to access admin route without permission")
|
|
abort(403, description="Admin access required")
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
|
|
def tenant_access_required(f):
|
|
"""
|
|
Decorator to require access to the specific tenant in the URL.
|
|
User must either be an admin or have explicit access to the tenant.
|
|
Admins can see server settings, regular users cannot.
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if 'user' not in session:
|
|
session['next'] = request.url
|
|
return redirect(url_for('auth.login'))
|
|
|
|
user_id = session['user'].get('id')
|
|
tenant_id = kwargs.get('tenant_id')
|
|
|
|
if not tenant_id:
|
|
# No tenant in URL, allow access (e.g., global settings for admins only)
|
|
return f(*args, **kwargs)
|
|
|
|
# Check if user has access to this tenant
|
|
if not UserModel.has_access_to_tenant(user_id, tenant_id):
|
|
current_app.logger.warning(
|
|
f"User {user_id} attempted to access tenant {tenant_id} without permission"
|
|
)
|
|
abort(403, description="You don't have access to this tenant")
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|
|
|
|
|
|
def settings_access_required(f):
|
|
"""
|
|
Decorator for server settings pages - admin only.
|
|
Regular users cannot see server settings.
|
|
"""
|
|
@wraps(f)
|
|
def decorated_function(*args, **kwargs):
|
|
if 'user' not in session:
|
|
session['next'] = request.url
|
|
return redirect(url_for('auth.login'))
|
|
|
|
user_id = session['user'].get('id')
|
|
if not UserModel.is_admin(user_id):
|
|
current_app.logger.warning(
|
|
f"User {user_id} attempted to access server settings without admin permission"
|
|
)
|
|
abort(403, description="Admin access required to view server settings")
|
|
|
|
return f(*args, **kwargs)
|
|
return decorated_function
|