Files
KCAPDemoServer/src/app/decorators/auth.py
Matt Hills 795068c015 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
2025-10-20 19:37:52 -04:00

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