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:
91
src/app/decorators/auth.py
Normal file
91
src/app/decorators/auth.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user