update configuration docker setup for data platform
This commit is contained in:
1
03-apiservice/app/middleware/__init__.py
Normal file
1
03-apiservice/app/middleware/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Middleware package
|
||||
169
03-apiservice/app/middleware/auth_middleware.py
Normal file
169
03-apiservice/app/middleware/auth_middleware.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
Authentication middleware for web pages
|
||||
Protects web UI routes while allowing API endpoints to use API Key auth
|
||||
"""
|
||||
from fastapi import Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from app.core.config import settings
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebAuthenticationMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware to enforce Keycloak authentication for web pages
|
||||
|
||||
Protected routes (require user login):
|
||||
- / (landing page)
|
||||
- /docs (API documentation)
|
||||
- /data-management/* (data management pages)
|
||||
|
||||
Excluded routes (no auth required or use different auth):
|
||||
- /auth/* (authentication endpoints)
|
||||
- /api/v1/* (API endpoints - use API Key auth)
|
||||
- /admin/* (SQLAdmin - has its own auth)
|
||||
- /apiservice/admin/statics/* (admin static files)
|
||||
- /admin/statics/* (admin static files)
|
||||
"""
|
||||
|
||||
# Routes that require user authentication
|
||||
PROTECTED_PATHS = [
|
||||
"/",
|
||||
"/docs",
|
||||
"/redoc",
|
||||
"/openapi.json",
|
||||
"/data-management"
|
||||
]
|
||||
|
||||
# Routes that are excluded from user authentication
|
||||
EXCLUDED_PATHS = [
|
||||
"/auth", # Authentication endpoints
|
||||
"/api/v1", # API endpoints (use API Key)
|
||||
"/admin", # SQLAdmin (has own auth)
|
||||
]
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
"""Process request and check authentication if needed"""
|
||||
|
||||
# Get the path without root_path prefix
|
||||
path = request.url.path
|
||||
|
||||
# Remove root_path prefix if it exists
|
||||
if settings.ROOT_PATH and path.startswith(settings.ROOT_PATH):
|
||||
path = path[len(settings.ROOT_PATH):]
|
||||
|
||||
# Ensure path starts with /
|
||||
if not path.startswith("/"):
|
||||
path = "/" + path
|
||||
|
||||
# Check if path is excluded from authentication
|
||||
is_excluded = any(path.startswith(excluded) for excluded in self.EXCLUDED_PATHS)
|
||||
|
||||
if is_excluded:
|
||||
# Skip authentication for excluded paths (including /api/v1/*)
|
||||
return await call_next(request)
|
||||
|
||||
# Check if path requires authentication
|
||||
requires_auth = any(
|
||||
path == protected or path.startswith(protected + "/")
|
||||
for protected in self.PROTECTED_PATHS
|
||||
)
|
||||
|
||||
if requires_auth:
|
||||
# Check if user is authenticated
|
||||
user = request.session.get("user")
|
||||
|
||||
if not user:
|
||||
# User not authenticated - redirect to login
|
||||
# Preserve the original path for redirect after login
|
||||
original_path = request.url.path
|
||||
|
||||
# Build login URL with redirect
|
||||
login_url = f"{settings.ROOT_PATH}/auth/login?redirect_to={original_path}"
|
||||
|
||||
logger.info(f"Unauthenticated access to {original_path}, redirecting to login")
|
||||
|
||||
return RedirectResponse(url=login_url, status_code=302)
|
||||
|
||||
# Role-based access control for specific paths
|
||||
user_roles = user.get("roles", [])
|
||||
|
||||
# /data-management/* requires 'operation' or 'admin' role
|
||||
if path.startswith("/data-management"):
|
||||
if not any(role in user_roles for role in ["admin", "operation"]):
|
||||
logger.warning(
|
||||
f"Access denied: User {user.get('username')} "
|
||||
f"(roles: {user_roles}) tried to access {path}"
|
||||
)
|
||||
# Return 403 Forbidden page
|
||||
from fastapi.responses import HTMLResponse
|
||||
return HTMLResponse(
|
||||
content=f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Access Denied</title>
|
||||
<style>
|
||||
body {{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}}
|
||||
.container {{
|
||||
background: white;
|
||||
padding: 60px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
}}
|
||||
h1 {{ color: #ff6b6b; font-size: 48px; margin-bottom: 20px; }}
|
||||
p {{ color: #666; font-size: 18px; margin: 15px 0; }}
|
||||
.role-info {{
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
.required {{ color: #ff6b6b; font-weight: bold; }}
|
||||
.your-roles {{ color: #667eea; font-weight: bold; }}
|
||||
a {{
|
||||
display: inline-block;
|
||||
margin-top: 30px;
|
||||
padding: 12px 30px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}}
|
||||
a:hover {{ background: #5568d3; transform: translateY(-2px); }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚫</h1>
|
||||
<h2>Access Denied</h2>
|
||||
<p>You don't have permission to access this page.</p>
|
||||
<div class="role-info">
|
||||
<p>Required role: <span class="required">operation</span> or <span class="required">admin</span></p>
|
||||
<p>Your roles: <span class="your-roles">{', '.join(user_roles) if user_roles else 'None'}</span></p>
|
||||
</div>
|
||||
<p>Please contact your administrator if you need access.</p>
|
||||
<a href="{settings.ROOT_PATH}/">← Go to Home</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
status_code=403
|
||||
)
|
||||
|
||||
# Continue with request
|
||||
return await call_next(request)
|
||||
Reference in New Issue
Block a user