fix: stronger handling of data
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ class Settings(BaseSettings):
|
|||||||
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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)):
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}%"),
|
||||||
|
|||||||
@@ -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})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user