fix: stronger handling of data

This commit is contained in:
Furqan-14
2026-02-17 11:57:24 +05:30
parent c3017b81da
commit 512c39e4f4
12 changed files with 95 additions and 32 deletions
+1
View File
@@ -42,6 +42,7 @@ ACCESS_TOKEN_EXPIRES=900
REFRESH_TOKEN_SECRET="6Z0yOfkhPjfLH77WTnBh3Iv0JU_gWIfIqpGuGU41GFRV4fnLZMfKN3gAsPTfsqFKv1rRc6szUJRngW8Py0UYUQ" REFRESH_TOKEN_SECRET="6Z0yOfkhPjfLH77WTnBh3Iv0JU_gWIfIqpGuGU41GFRV4fnLZMfKN3gAsPTfsqFKv1rRc6szUJRngW8Py0UYUQ"
REFRESH_TOKEN_EXPIRES=864000 REFRESH_TOKEN_EXPIRES=864000
JWT_ALGORITHM=HS256 JWT_ALGORITHM=HS256
COOKIE_SECURE=true
ADMIN_JWT="upRCbNd-3Ex3sG2aEHxcrCx7LZu91BkiNGPIs-vxNXp7YBHyU-0jMpGUYA5dJHLcyOIMLBk4HCw1cpI4WOlIzA" ADMIN_JWT="upRCbNd-3Ex3sG2aEHxcrCx7LZu91BkiNGPIs-vxNXp7YBHyU-0jMpGUYA5dJHLcyOIMLBk4HCw1cpI4WOlIzA"
# External SaaS Webhook # External SaaS Webhook
+5 -1
View File
@@ -72,7 +72,11 @@ class Settings(BaseSettings):
REFRESH_TOKEN_SECRET: str REFRESH_TOKEN_SECRET: str
REFRESH_TOKEN_EXPIRES: int = 864000 REFRESH_TOKEN_EXPIRES: int = 864000
JWT_ALGORITHM: str = "HS256" JWT_ALGORITHM: str = "HS256"
# Cookie settings
COOKIE_SECURE: bool = False
COOKIE_DOMAIN: Optional[str] = None
# Super Admin Setup # Super Admin Setup
SUPER_ADMIN_EMAIL: str SUPER_ADMIN_EMAIL: str
SUPER_ADMIN_PASSWORD: str SUPER_ADMIN_PASSWORD: str
+4
View File
@@ -27,6 +27,10 @@ class AuthController:
def refresh_token(db: Session, token_data: RefreshTokenRequest): def refresh_token(db: Session, token_data: RefreshTokenRequest):
return AuthService.refresh_access_token(db, token_data.refresh_token) return AuthService.refresh_access_token(db, token_data.refresh_token)
@staticmethod
def refresh_token_raw(db: Session, refresh_token: str):
return AuthService.refresh_access_token(db, refresh_token)
@staticmethod @staticmethod
def update_user( def update_user(
db: Session, user_id: uuid.UUID, user_data: UserUpdate, current_user: User db: Session, user_id: uuid.UUID, user_data: UserUpdate, current_user: User
+20 -5
View File
@@ -2,6 +2,7 @@ from sqlalchemy.orm import Session
from fastapi import Request, HTTPException from fastapi import Request, HTTPException
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
import uuid import uuid
import logging
from app.services.auth.sso_service import SSOService from app.services.auth.sso_service import SSOService
from app.services.auth.trust_service import TrustService from app.services.auth.trust_service import TrustService
from app.schemas.auth.sso_schema import SSOInitiateRequest, SSOExchangeRequest from app.schemas.auth.sso_schema import SSOInitiateRequest, SSOExchangeRequest
@@ -9,6 +10,8 @@ from app.models.auth.module_model import Module
from app.models.auth.module_environment_model import ModuleEnvironment from app.models.auth.module_environment_model import ModuleEnvironment
from app.models.auth.user_model import User from app.models.auth.user_model import User
logger = logging.getLogger(__name__)
class SSOController: class SSOController:
@staticmethod @staticmethod
def initiate_sso(db: Session, request: SSOInitiateRequest, current_user: User): def initiate_sso(db: Session, request: SSOInitiateRequest, current_user: User):
@@ -44,11 +47,23 @@ class SSOController:
if x_module_key: if x_module_key:
headers["X-Module-Key"] = x_module_key headers["X-Module-Key"] = x_module_key
TrustService.validate_module_trust( actual_body = payload.model_dump_json()
environment=env, try:
request_headers=headers, TrustService.validate_module_trust(
request_body="" environment=env,
) request_headers=headers,
request_body=actual_body
)
except HTTPException:
logger.warning(
"HMAC verify with body failed for %s, trying empty fallback (DEPRECATED)",
payload.module_id,
)
TrustService.validate_module_trust(
environment=env,
request_headers=headers,
request_body=""
)
return SSOService.exchange_grant( return SSOService.exchange_grant(
db=db, db=db,
@@ -1,6 +1,6 @@
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from uuid import UUID from uuid import UUID
from typing import List, Optional from typing import List
from app.models.auth.user_model import User from app.models.auth.user_model import User
from app.models.theme.color_palette_model import ColorPalette from app.models.theme.color_palette_model import ColorPalette
from app.schemas.theme.color_palette_schema import ( from app.schemas.theme.color_palette_schema import (
@@ -12,9 +12,7 @@ from app.services.theme.color_palette_service import PaletteService
class PaletteController: class PaletteController:
@staticmethod @staticmethod
def get_all_palettes(db: Session, current_user: User) -> List[ColorPalette]: def get_all_palettes(db: Session, current_user: User) -> List[ColorPalette]:
return PaletteService.get_all_palettes(db)
tenant_id = current_user.tenant_id
return PaletteService.get_all_palettes(db, tenant_id)
@staticmethod @staticmethod
def get_palette(db: Session, palette_id: UUID) -> ColorPalette: def get_palette(db: Session, palette_id: UUID) -> ColorPalette:
@@ -24,7 +22,7 @@ class PaletteController:
def create_palette( def create_palette(
db: Session, data: ColorPaletteCreate, current_user: User db: Session, data: ColorPaletteCreate, current_user: User
) -> ColorPalette: ) -> ColorPalette:
return PaletteService.create_palette(db, data, tenant_id=None) return PaletteService.create_palette(db, data)
@staticmethod @staticmethod
def update_palette( def update_palette(
+4 -4
View File
@@ -1,4 +1,4 @@
from .auth import router from .auth import router as auth_router
from .tenant import router from .tenant import router as tenant_router
from .role import router from .role import router as role_router
from .user import router from .user import router as user_router
+36 -6
View File
@@ -1,7 +1,8 @@
from fastapi import APIRouter, Depends, Request, status, HTTPException from fastapi import APIRouter, Depends, Request, Response, status, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.config.database import get_db from app.config.database import get_db
from app.config.settings import settings
from app.controllers.auth.auth_controller import AuthController from app.controllers.auth.auth_controller import AuthController
from app.schemas.auth.auth_schema import ( from app.schemas.auth.auth_schema import (
UserSignup, UserSignup,
@@ -23,6 +24,12 @@ import uuid
router = APIRouter() router = APIRouter()
def _cookie_kwargs():
kwargs = dict(httponly=True, samesite="lax", secure=settings.COOKIE_SECURE, path="/")
if settings.COOKIE_DOMAIN:
kwargs["domain"] = settings.COOKIE_DOMAIN
return kwargs
@router.post("/signup", response_model=UserResponse, status_code=status.HTTP_201_CREATED) @router.post("/signup", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
def signup( def signup(
user_data: UserSignup, user_data: UserSignup,
@@ -32,12 +39,30 @@ def signup(
return AuthController.signup(db, user_data, tenant_id) return AuthController.signup(db, user_data, tenant_id)
@router.post("/signin", response_model=TokenResponse) @router.post("/signin", response_model=TokenResponse)
def signin(signin_data: UserSignin, db: Session = Depends(get_db)): def signin(signin_data: UserSignin, response: Response, db: Session = Depends(get_db)):
return AuthController.signin(db, signin_data) result = AuthController.signin(db, signin_data)
cookie_kw = _cookie_kwargs()
access_max_age = settings.ACCESS_TOKEN_EXPIRES if signin_data.remember_me else None
refresh_max_age = settings.REFRESH_TOKEN_EXPIRES if signin_data.remember_me else None
response.set_cookie(key="access_token", value=result["access_token"], max_age=access_max_age, **cookie_kw)
response.set_cookie(key="refresh_token", value=result["refresh_token"], max_age=refresh_max_age, **cookie_kw)
return result
@router.post("/refresh", response_model=TokenResponse) @router.post("/refresh", response_model=TokenResponse)
def refresh_token(token_data: RefreshTokenRequest, db: Session = Depends(get_db)): def refresh_token(
return AuthController.refresh_token(db, token_data) token_data: RefreshTokenRequest,
request: Request,
response: Response,
db: Session = Depends(get_db),
):
refresh_tok = token_data.refresh_token or request.cookies.get("refresh_token")
if not refresh_tok:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing refresh token")
result = AuthController.refresh_token_raw(db, refresh_tok)
cookie_kw = _cookie_kwargs()
response.set_cookie(key="access_token", value=result["access_token"], **cookie_kw)
response.set_cookie(key="refresh_token", value=result["refresh_token"], **cookie_kw)
return result
@router.get("/me", response_model=UserResponse) @router.get("/me", response_model=UserResponse)
def get_me( def get_me(
@@ -76,11 +101,16 @@ def reset_password(
@router.post("/logout") @router.post("/logout")
def logout( def logout(
request: Request, request: Request,
response: Response,
credentials: HTTPAuthorizationCredentials = Depends(security_scheme), credentials: HTTPAuthorizationCredentials = Depends(security_scheme),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
): ):
token = credentials.credentials if credentials else request.cookies.get("access_token") token = credentials.credentials if credentials else request.cookies.get("access_token")
return AuthController.logout(current_user, token) result = AuthController.logout(current_user, token)
cookie_kw = _cookie_kwargs()
response.delete_cookie(key="access_token", **cookie_kw)
response.delete_cookie(key="refresh_token", **cookie_kw)
return result
@router.post("/forgot-password") @router.post("/forgot-password")
def forgot_password(request: ForgotPasswordRequest, db: Session = Depends(get_db)): def forgot_password(request: ForgotPasswordRequest, db: Session = Depends(get_db)):
+2 -1
View File
@@ -16,6 +16,7 @@ class UserSignup(UserBase):
class UserSignin(BaseModel): class UserSignin(BaseModel):
email: EmailStr email: EmailStr
password: str password: str
remember_me: bool = False
class AccessInRole(BaseModel): class AccessInRole(BaseModel):
id: str id: str
@@ -55,7 +56,7 @@ class TokenResponse(BaseModel):
class RefreshTokenRequest(BaseModel): class RefreshTokenRequest(BaseModel):
refresh_token: str refresh_token: Optional[str] = None
class ResetPassword(BaseModel): class ResetPassword(BaseModel):
old_password: str old_password: str
@@ -37,7 +37,6 @@ class EnvironmentResponse(BaseModel):
frontend_base_url: str frontend_base_url: str
backend_base_url: str backend_base_url: str
sso_entry_path: str sso_entry_path: str
sso_entry_path: str
permission_sync_endpoint: str permission_sync_endpoint: str
sso_exchange_endpoint: Optional[str] sso_exchange_endpoint: Optional[str]
provisioning_endpoint: Optional[str] = "/internal/tenants/provision" provisioning_endpoint: Optional[str] = "/internal/tenants/provision"
+2 -2
View File
@@ -14,7 +14,7 @@ class TrustService:
if environment.trust_type != "hmac": if environment.trust_type != "hmac":
if environment.trust_type == "static_key": if environment.trust_type == "static_key":
secret = environment.trust_credentials.get("secret_key") secret = environment.trust_credentials.get("secret_key")
return signature == secret return hmac.compare_digest(signature, secret)
return False return False
secret = environment.trust_credentials.get("hmac_secret") secret = environment.trust_credentials.get("hmac_secret")
@@ -101,7 +101,7 @@ class TrustService:
elif environment.trust_type == "static_key": elif environment.trust_type == "static_key":
api_key = request_headers.get("X-Module-Key") api_key = request_headers.get("X-Module-Key")
secret = environment.trust_credentials.get("secret_key") secret = environment.trust_credentials.get("secret_key")
if not api_key or api_key != secret: if not api_key or not hmac.compare_digest(api_key, secret):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid API Key" detail="Invalid API Key"
+15
View File
@@ -13,6 +13,7 @@ import json
from app.models.auth.role_module_access_model import RoleModuleAccess from app.models.auth.role_module_access_model import RoleModuleAccess
from app.models.auth.module_access_model import ModuleAccess from app.models.auth.module_access_model import ModuleAccess
from app.models.auth.tenant_module_model import TenantModule from app.models.auth.tenant_module_model import TenantModule
from app.models.auth.role_model import Role
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -108,6 +109,19 @@ class UserService:
if tenant_id: if tenant_id:
update_dict.pop("tenant_id", None) update_dict.pop("tenant_id", None)
if "role_id" in update_dict and update_dict["role_id"] is not None:
role = db.query(Role).filter(Role.id == update_dict["role_id"]).first()
if not role:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Role not found"
)
if tenant_id and role.tenant_id is not None and role.tenant_id != tenant_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot assign a role from another tenant"
)
if "email" in update_dict and update_dict["email"] != user.email: if "email" in update_dict and update_dict["email"] != user.email:
if db.query(User).filter(User.email == update_dict["email"]).first(): if db.query(User).filter(User.email == update_dict["email"]).first():
raise HTTPException( raise HTTPException(
@@ -231,6 +245,7 @@ class UserService:
if search and search.strip(): if search and search.strip():
search_term = search.strip() search_term = search.strip()
search_term = search_term.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
query = query.filter( query = query.filter(
or_( or_(
User.email.ilike(f"%{search_term}%"), User.email.ilike(f"%{search_term}%"),
+3 -7
View File
@@ -1,6 +1,6 @@
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from uuid import UUID from uuid import UUID
from typing import List, Optional from typing import List
from fastapi import HTTPException, status from fastapi import HTTPException, status
from app.models.theme.color_palette_model import ColorPalette from app.models.theme.color_palette_model import ColorPalette
from app.schemas.theme.color_palette_schema import ( from app.schemas.theme.color_palette_schema import (
@@ -10,9 +10,7 @@ from app.schemas.theme.color_palette_schema import (
class PaletteService: class PaletteService:
@staticmethod @staticmethod
def get_all_palettes( def get_all_palettes(db: Session) -> List[ColorPalette]:
db: Session, tenant_id: Optional[UUID] = None
) -> List[ColorPalette]:
return db.query(ColorPalette).all() return db.query(ColorPalette).all()
@staticmethod @staticmethod
@@ -23,9 +21,7 @@ class PaletteService:
return palette return palette
@staticmethod @staticmethod
def create_palette( def create_palette(db: Session, data: ColorPaletteCreate) -> ColorPalette:
db: Session, data: ColorPaletteCreate, tenant_id: Optional[UUID] = None
) -> ColorPalette:
if data.is_default: if data.is_default:
db.query(ColorPalette).update({"is_default": False}) db.query(ColorPalette).update({"is_default": False})