v2.3
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
"""Authentication router for login and JWT token management."""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Form
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Dict, Any
|
||||
from .. import crud, schemas
|
||||
from ..database import get_db
|
||||
from ..utils import create_access_token, decode_access_token
|
||||
from fastapi import Request, Query
|
||||
from ..security import get_current_user, revoke_token_db, REVOKED_TOKENS, set_latest_jti
|
||||
import os
|
||||
from .radius_login import check_radius_login
|
||||
from ..models import RevokedToken
|
||||
import requests
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=schemas.LoginResponse)
|
||||
async def login(login_req: schemas.LoginRequest, db: Session = Depends(get_db)):
|
||||
|
||||
# Convert env về boolean chuẩn
|
||||
IS_PRODUCT = os.getenv("IS_PRODUCT", "False").lower() == "true"
|
||||
|
||||
# =============================
|
||||
# 🚀 PRODUCT MODE (mock data)
|
||||
# =============================
|
||||
if IS_PRODUCT:
|
||||
return {
|
||||
"access_token": "test",
|
||||
"token_type": "bearer",
|
||||
"user": {
|
||||
"id": 1,
|
||||
"email": "test@gmail.com",
|
||||
"fullname": "Test User",
|
||||
"unit_id": 1,
|
||||
"unit_name": "MEDIA",
|
||||
"role_id": 1,
|
||||
"role_name": "Administrator",
|
||||
"status": 2,
|
||||
"status_name": "active",
|
||||
"created_at": "2025-12-02T14:32:10",
|
||||
"updated_at": "2025-12-23T04:24:04"
|
||||
}
|
||||
}
|
||||
|
||||
# =============================
|
||||
# 🧪 DEV MODE (login thật)
|
||||
# =============================
|
||||
user = crud.authenticate_user(db, login_req.email, login_req.password)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if int(getattr(user, "status", 0) or 0) != 2:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User is not active",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
user_details = crud.get_user_with_details(db, user)
|
||||
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
"email": user.email,
|
||||
"fullname": user.fullname,
|
||||
"unit_id": user.unit_id,
|
||||
"status": user.status,
|
||||
}
|
||||
|
||||
access_token = create_access_token(data=token_data)
|
||||
|
||||
try:
|
||||
payload = decode_access_token(access_token)
|
||||
jti = payload.get("jti")
|
||||
if jti:
|
||||
set_latest_jti(int(user.id), jti)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"user": user_details
|
||||
}
|
||||
|
||||
|
||||
@router.post("/logout", response_model=schemas.APIResponse, dependencies=[Depends(get_current_user)])
|
||||
async def logout(request: Request, db: Session = Depends(get_db)):
|
||||
"""Logout endpoint: revoke current JWT token."""
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing authorization header",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
scheme, token = auth_header.split()
|
||||
if scheme.lower() != "bearer":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication scheme",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authorization header",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
revoke_token_db(db, token)
|
||||
return {"message": "success", "data": {"revoked": True}}
|
||||
|
||||
@router.get("/validate-token", response_model=schemas.APIResponse)
|
||||
async def validate_token(request: Request, db: Session = Depends(get_db)):
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing authorization header",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
scheme, token = auth_header.split()
|
||||
if scheme.lower() != "bearer":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication scheme",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authorization header",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
if token in REVOKED_TOKENS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token has been revoked",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = decode_access_token(token)
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
jti = payload.get("jti")
|
||||
if jti and db.query(RevokedToken).filter(RevokedToken.jti == jti).first():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token has been revoked",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return {"message": "success", "data": payload}
|
||||
|
||||
@router.post("/login_sso", response_model=schemas.LoginResponse)
|
||||
async def login_sso(
|
||||
login_req: schemas.LoginSSORequest,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
server = os.getenv("RADIUS_SERVER")
|
||||
secret = os.getenv("RADIUS_SECRET")
|
||||
port = int(os.getenv("RADIUS_PORT", 1812))
|
||||
realm = os.getenv("RADIUS_REALM", "")
|
||||
if not server or not secret:
|
||||
raise HTTPException(status_code=500, detail="Configuration is missing")
|
||||
try:
|
||||
local_part = (login_req.email or "").split("@")[0]
|
||||
except Exception:
|
||||
local_part = login_req.email or ""
|
||||
username = f"{local_part}{realm}"
|
||||
radius_password = f"{login_req.password}{login_req.otp or ''}"
|
||||
ok = check_radius_login(server, port, secret, username, radius_password)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username or password or OTP")
|
||||
user = crud.get_user_by_email(db, login_req.email)
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found. Please contact to Admin.")
|
||||
if int(getattr(user, "status", 0) or 0) != 2:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User is not active",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
user_details = crud.get_user_with_details(db, user)
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
"email": user.email,
|
||||
"fullname": user.fullname,
|
||||
"unit_id": user.unit_id,
|
||||
"status": user.status,
|
||||
}
|
||||
access_token = create_access_token(data=token_data)
|
||||
try:
|
||||
payload = decode_access_token(access_token)
|
||||
jti = payload.get("jti")
|
||||
if jti:
|
||||
set_latest_jti(int(user.id), jti)
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"user": user_details,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/login_sso_vnpt", response_model=schemas.LoginResponse)
|
||||
async def login_sso_vnpt(
|
||||
ticket: str = Query(..., description="CAS ticket"),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
# ===== 1. CAS CONFIG =====
|
||||
CAS_HOST = os.getenv("CAS_HOST") # vd: https://cas.vnpt.vn/cas
|
||||
SERVICE_URL = os.getenv("CAS_SERVICE_URL")
|
||||
|
||||
if not CAS_HOST or not SERVICE_URL:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="CAS configuration missing",
|
||||
)
|
||||
|
||||
# ===== 2. BUILD VALIDATE URL =====
|
||||
validate_url = (
|
||||
f"{CAS_HOST.rstrip('/')}/serviceValidate"
|
||||
f"?ticket={ticket}&service={SERVICE_URL}"
|
||||
)
|
||||
print(validate_url)
|
||||
# ===== 3. CALL CAS =====
|
||||
try:
|
||||
resp = requests.get(validate_url, timeout=5)
|
||||
resp.raise_for_status()
|
||||
except Exception:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="CAS validate service unavailable",
|
||||
)
|
||||
|
||||
# ===== 4. PARSE XML =====
|
||||
try:
|
||||
root = ET.fromstring(resp.text)
|
||||
|
||||
except ET.ParseError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid CAS response",
|
||||
)
|
||||
|
||||
namespace = {"cas": "http://www.yale.edu/tp/cas"}
|
||||
user_node = root.find(".//cas:user", namespace)
|
||||
|
||||
if user_node is None or not user_node.text:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="CAS returned but refused to validate your identity",
|
||||
)
|
||||
|
||||
# ===== 5. NETID → EMAIL =====
|
||||
netid = user_node.text.strip()
|
||||
email = f"{netid.lower()}@vnpt.vn"
|
||||
|
||||
# ===== 6. CHECK USER DB =====
|
||||
user = crud.get_user_by_email(db, email)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found. Please contact to Admin.",
|
||||
)
|
||||
|
||||
if int(getattr(user, "status", 0) or 0) != 2:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="User is not active",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# ===== 7. USER DETAILS =====
|
||||
user_details = crud.get_user_with_details(db, user)
|
||||
|
||||
# ===== 8. TOKEN DATA (GIỮ NGUYÊN THEO YÊU CẦU) =====
|
||||
token_data = {
|
||||
"sub": str(user.id),
|
||||
"email": user.email,
|
||||
"fullname": user.fullname,
|
||||
"unit_id": user.unit_id,
|
||||
"status": user.status,
|
||||
}
|
||||
|
||||
access_token = create_access_token(data=token_data)
|
||||
try:
|
||||
payload = decode_access_token(access_token)
|
||||
jti = payload.get("jti")
|
||||
if jti:
|
||||
set_latest_jti(int(user.id), jti)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ===== 9. RESPONSE =====
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"user": user_details,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
from urllib.parse import quote_plus
|
||||
from sqlalchemy import create_engine, event
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from dotenv import load_dotenv
|
||||
import datetime
|
||||
from sqlalchemy import inspect as sa_inspect
|
||||
from .utils import audit_log_path, get_current_user_ctx
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Prefer per-component DB config when provided; fallback to DATABASE_URL; finally to sqlite for dev
|
||||
db_driver = os.getenv("DB_DRIVER")
|
||||
db_user = os.getenv("DB_USER")
|
||||
db_password = os.getenv("DB_PASSWORD")
|
||||
db_host = os.getenv("DB_HOST")
|
||||
db_port = os.getenv("DB_PORT")
|
||||
db_name = os.getenv("DB_NAME")
|
||||
|
||||
database_url = None
|
||||
if db_driver and db_user and db_host and db_port and db_name is not None:
|
||||
# URL-encode username and password. If password is empty or None, omit the ':' portion
|
||||
user_enc = quote_plus(db_user)
|
||||
if db_password:
|
||||
pwd_enc = quote_plus(db_password)
|
||||
auth = f"{user_enc}:{pwd_enc}"
|
||||
else:
|
||||
auth = f"{user_enc}"
|
||||
database_url = f"{db_driver}://{auth}@{db_host}:{db_port}/{db_name}"
|
||||
|
||||
if not database_url:
|
||||
database_url = os.getenv("DATABASE_URL")
|
||||
|
||||
if not database_url:
|
||||
# final fallback for local dev
|
||||
database_url = "sqlite:///./test.db"
|
||||
|
||||
# Engine pool configuration to avoid QueuePool timeouts under concurrency
|
||||
default_pool_size = int(os.getenv("DB_POOL_SIZE", "20"))
|
||||
default_max_overflow = int(os.getenv("DB_POOL_MAX_OVERFLOW", "40"))
|
||||
default_pool_timeout = int(os.getenv("DB_POOL_TIMEOUT", "60"))
|
||||
default_pool_recycle = int(os.getenv("DB_POOL_RECYCLE", "1800")) # seconds
|
||||
|
||||
if database_url.startswith("sqlite"):
|
||||
engine = create_engine(
|
||||
database_url,
|
||||
pool_pre_ping=True,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
else:
|
||||
engine = create_engine(
|
||||
database_url,
|
||||
pool_pre_ping=True,
|
||||
pool_size=default_pool_size,
|
||||
max_overflow=default_max_overflow,
|
||||
pool_timeout=default_pool_timeout,
|
||||
pool_recycle=default_pool_recycle,
|
||||
)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def _write_audit(line: str) -> None:
|
||||
try:
|
||||
with open(audit_log_path(), "a", encoding="utf-8") as f:
|
||||
f.write(line)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@event.listens_for(SessionLocal, "after_flush")
|
||||
def _audit_after_flush(session, context):
|
||||
user = get_current_user_ctx() or {}
|
||||
uid = str((user.get("sub") or user.get("id") or "-"))
|
||||
email = user.get("email") or "-"
|
||||
now = datetime.datetime.utcnow().isoformat()
|
||||
for obj in list(session.new):
|
||||
ins = sa_inspect(obj)
|
||||
mapper = ins.mapper
|
||||
table = mapper.local_table.name if getattr(mapper, "local_table", None) is not None else getattr(obj, "__tablename__", obj.__class__.__name__)
|
||||
pk = ins.identity
|
||||
line = f"{now} | {uid} | {email} | INSERT {table} | pk={pk}\n"
|
||||
_write_audit(line)
|
||||
for obj in list(session.dirty):
|
||||
if not session.is_modified(obj, include_collections=False):
|
||||
continue
|
||||
ins = sa_inspect(obj)
|
||||
mapper = ins.mapper
|
||||
table = mapper.local_table.name if getattr(mapper, "local_table", None) is not None else getattr(obj, "__tablename__", obj.__class__.__name__)
|
||||
changes = []
|
||||
for attr in ins.attrs:
|
||||
hist = attr.history
|
||||
if hist.has_changes():
|
||||
old = hist.deleted[0] if hist.deleted else None
|
||||
new = hist.added[0] if hist.added else getattr(obj, attr.key)
|
||||
changes.append(f"{attr.key}={old}->{new}")
|
||||
pk = ins.identity
|
||||
chs = ", ".join(changes) if changes else "-"
|
||||
line = f"{now} | {uid} | {email} | UPDATE {table} | pk={pk} | {chs}\n"
|
||||
_write_audit(line)
|
||||
for obj in list(session.deleted):
|
||||
ins = sa_inspect(obj)
|
||||
mapper = ins.mapper
|
||||
table = mapper.local_table.name if getattr(mapper, "local_table", None) is not None else getattr(obj, "__tablename__", obj.__class__.__name__)
|
||||
pk = ins.identity
|
||||
line = f"{now} | {uid} | {email} | DELETE {table} | pk={pk}\n"
|
||||
_write_audit(line)
|
||||
@@ -0,0 +1,301 @@
|
||||
import os
|
||||
import time
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI, Request, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from dotenv import load_dotenv
|
||||
from .utils import decode_access_token, set_current_user_ctx
|
||||
from .database import get_db
|
||||
from . import models
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
load_dotenv()
|
||||
|
||||
from .database import engine, Base
|
||||
from .routers import auth, users, roles, posts, month, security_index
|
||||
from .routers import hardening
|
||||
from .routers import files
|
||||
from app.routers import system_groups as system_groups_router
|
||||
from app.routers import systems as systems_router
|
||||
|
||||
app = FastAPI(
|
||||
title="ANTT Portal API",
|
||||
swagger_ui_parameters={"persistAuthorization": True},
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# @app.middleware("http")
|
||||
# async def docs_ip_whitelist(request: Request, call_next):
|
||||
# path = request.url.path
|
||||
# if path.startswith("/docs") or path.startswith("/redoc") or path.startswith("/openapi.json"):
|
||||
# wl = [x.strip() for x in (os.getenv("DOCS_WHITELIST_IPS") or "").split(",") if x.strip()]
|
||||
# client_ip = request.client.host if request.client else ""
|
||||
# if wl and client_ip not in wl:
|
||||
# raise HTTPException(status_code=403, detail="IP not allowed")
|
||||
# return await call_next(request)
|
||||
|
||||
def get_real_ip(request: Request) -> str:
|
||||
xff = request.headers.get("x-forwarded-for")
|
||||
if xff:
|
||||
# Lấy IP đầu tiên (client thật)
|
||||
return xff.split(",")[0].strip()
|
||||
|
||||
x_real_ip = request.headers.get("x-real-ip")
|
||||
if x_real_ip:
|
||||
return x_real_ip.strip()
|
||||
|
||||
if request.client:
|
||||
return request.client.host
|
||||
|
||||
return ""
|
||||
|
||||
@app.middleware("http")
|
||||
async def docs_ip_whitelist(request: Request, call_next):
|
||||
path = request.url.path
|
||||
|
||||
if path.startswith(("/docs", "/redoc", "/openapi.json")):
|
||||
wl = [
|
||||
ip.strip()
|
||||
for ip in (os.getenv("DOCS_WHITELIST_IPS") or "").split(",")
|
||||
if ip.strip()
|
||||
]
|
||||
|
||||
client_ip = get_real_ip(request)
|
||||
|
||||
if wl and client_ip not in wl:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"IP {client_ip} not allowed"
|
||||
)
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
def _activity_log_dir() -> Path:
|
||||
base = os.getenv("ACTIVITY_LOG_DIR", "logs")
|
||||
p = Path(base).resolve()
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
return p
|
||||
|
||||
def _activity_log_path() -> Path:
|
||||
now = datetime.datetime.utcnow()
|
||||
month_dir = _activity_log_dir() / now.strftime("%Y%m")
|
||||
month_dir.mkdir(parents=True, exist_ok=True)
|
||||
return month_dir / f"{now.strftime('%Y-%m-%d')}.txt"
|
||||
|
||||
@app.middleware("http")
|
||||
async def activity_logger(request: Request, call_next):
|
||||
path = request.url.path
|
||||
if path.startswith("/auth/login"):
|
||||
return await call_next(request)
|
||||
if path.startswith("/auth/login_sso"):
|
||||
return await call_next(request)
|
||||
method = request.method.upper()
|
||||
qs = request.url.query or ""
|
||||
ip = request.client.host if request.client else "-"
|
||||
start = time.time()
|
||||
status_code = 500
|
||||
user_payload = None
|
||||
auth_header = request.headers.get("Authorization") or ""
|
||||
token = None
|
||||
try:
|
||||
scheme, token = auth_header.split()
|
||||
if scheme.lower() != "bearer":
|
||||
token = None
|
||||
except Exception:
|
||||
token = None
|
||||
if token:
|
||||
try:
|
||||
user_payload = decode_access_token(token)
|
||||
except Exception:
|
||||
user_payload = None
|
||||
try:
|
||||
set_current_user_ctx(user_payload)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
response = await call_next(request)
|
||||
status_code = response.status_code
|
||||
return response
|
||||
finally:
|
||||
try:
|
||||
set_current_user_ctx(None)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
user_id = "-"
|
||||
user_email = "-"
|
||||
if user_payload:
|
||||
try:
|
||||
user_id = str(user_payload.get("sub") or "-")
|
||||
user_email = user_payload.get("email") or "-"
|
||||
except Exception:
|
||||
pass
|
||||
duration_ms = int((time.time() - start) * 1000)
|
||||
line = f"{datetime.datetime.utcnow().isoformat()} | {ip} | {user_id} | {user_email} | {method} {path}{('?' + qs) if qs else ''} | {status_code} | {duration_ms}ms\n"
|
||||
with open(_activity_log_path(), "a", encoding="utf-8") as f:
|
||||
f.write(line)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
# create tables if AUTO_CREATE_DB is enabled (convenience for dev only)
|
||||
if os.getenv("AUTO_CREATE_DB", "0") == "1":
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(roles.router, prefix="/roles", tags=["roles"])
|
||||
app.include_router(users.router, prefix="/users", tags=["users"])
|
||||
app.include_router(posts.router, prefix="/posts", tags=["posts"])
|
||||
app.include_router(month.router, prefix="/months", tags=["months"])
|
||||
app.include_router(security_index.router)
|
||||
app.include_router(hardening.router)
|
||||
# Đăng ký categories
|
||||
from .routers import categories
|
||||
app.include_router(categories.router, prefix="/categories", tags=["categories"])
|
||||
app.include_router(files.router, prefix="/files", tags=["files"])
|
||||
from .routers import pentest
|
||||
from .routers import pentest_service
|
||||
from .routers import jira
|
||||
from .routers import units
|
||||
from .routers import overview
|
||||
from .routers import scorecard
|
||||
from .routers import email as email_router
|
||||
from .routers import documents as documents_router
|
||||
from .routers import soc_ticket
|
||||
from .routers import logsource
|
||||
app.include_router(pentest.router, prefix="/pentest", tags=["pentest"])
|
||||
app.include_router(pentest_service.router)
|
||||
app.include_router(soc_ticket.router)
|
||||
app.include_router(logsource.router)
|
||||
from .routers import mail_history
|
||||
app.include_router(mail_history.router, prefix="/mail-history", tags=["mail-history"])
|
||||
from .routers import import_history as import_history_router
|
||||
app.include_router(import_history_router.router, prefix="/import-history", tags=["import-history"])
|
||||
from .routers import manage_systems as manage_systems_router
|
||||
app.include_router(manage_systems_router.router)
|
||||
# app.include_router(system_groups_router.router)
|
||||
app.include_router(systems_router.router)
|
||||
app.include_router(jira.router, prefix="/jira", tags=["jira"])
|
||||
app.include_router(units.router)
|
||||
app.include_router(overview.router)
|
||||
app.include_router(scorecard.router)
|
||||
app.include_router(email_router.email_roles_router)
|
||||
app.include_router(email_router.email_users_router)
|
||||
app.include_router(documents_router.router)
|
||||
|
||||
def custom_openapi():
|
||||
if app.openapi_schema:
|
||||
return app.openapi_schema
|
||||
|
||||
openapi_schema = get_openapi(
|
||||
title="ANTT Portal API",
|
||||
version="1.0.0",
|
||||
description="API Gateway for ANTT Portal",
|
||||
routes=app.routes,
|
||||
)
|
||||
|
||||
openapi_schema["components"]["securitySchemes"] = {
|
||||
"Bearer": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT",
|
||||
"description": "Enter JWT token (no 'Bearer' prefix needed)",
|
||||
}
|
||||
}
|
||||
# Áp dụng security toàn cục để UI biết cần Bearer
|
||||
openapi_schema["security"] = [{"Bearer": []}]
|
||||
|
||||
# Add security requirement to all paths except /auth/login
|
||||
if "paths" in openapi_schema:
|
||||
for path, path_item in openapi_schema["paths"].items():
|
||||
# Bỏ qua các endpoint auth
|
||||
if "/auth/" in path:
|
||||
continue
|
||||
# Thêm security cho từng operation nếu chưa có
|
||||
for method in ["get", "post", "put", "delete", "patch"]:
|
||||
if method in path_item:
|
||||
operation = path_item[method]
|
||||
if "security" not in operation:
|
||||
operation["security"] = [{"Bearer": []}]
|
||||
|
||||
app.openapi_schema = openapi_schema
|
||||
return app.openapi_schema
|
||||
|
||||
|
||||
app.openapi = custom_openapi
|
||||
|
||||
# Public API: system groups tree via ManageSystem (levels 0/1/2), API key + IP whitelist
|
||||
@app.post("/sync/system-groups", tags=["system_groups"])
|
||||
def system_groups_tree(
|
||||
request: Request,
|
||||
payload: Dict[str, Any],
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
cfg_key = os.getenv("SYSTEM_GROUPS_API_KEY")
|
||||
if not cfg_key:
|
||||
raise HTTPException(status_code=500, detail="SYSTEM_GROUPS_API_KEY is not configured")
|
||||
use_key = str(payload.get("api_key") or "")
|
||||
if (use_key or "").strip() != cfg_key.strip():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid API key",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
wl = [x.strip() for x in (os.getenv("SYSTEM_GROUPS_WHITELIST_IPS") or "").split(",") if x.strip()]
|
||||
client_ip = request.client.host if request.client else ""
|
||||
if wl and client_ip not in wl:
|
||||
raise HTTPException(status_code=403, detail="IP not allowed")
|
||||
|
||||
items: List[models.ManageSystem] = db.query(models.ManageSystem).order_by(models.ManageSystem.level.asc(), models.ManageSystem.id.asc()).all()
|
||||
type_map = {
|
||||
0: "Web quản trị",
|
||||
1: "Web dịch vụ",
|
||||
2: "API",
|
||||
3: "Mobile app",
|
||||
4: "Khác",
|
||||
5: "CNTT",
|
||||
6: "ATTT",
|
||||
}
|
||||
nodes: Dict[int, Dict[str, Any]] = {}
|
||||
for it in items:
|
||||
tval = int(it.type) if getattr(it, "type", None) is not None else None
|
||||
nodes[int(it.id)] = {
|
||||
"id": int(it.id),
|
||||
"name": it.name,
|
||||
"level": int(it.level),
|
||||
"parent_id": int(it.parent_id) if getattr(it, "parent_id", None) is not None else None,
|
||||
"url_ip": getattr(it, "url_ip", None),
|
||||
"unit_id": int(it.unit_id) if getattr(it, "unit_id", None) is not None else None,
|
||||
"type": tval,
|
||||
"type_name": type_map.get(tval) if tval is not None else None,
|
||||
"private": bool(it.private) if getattr(it, "private", None) is not None else None,
|
||||
"priority_level": int(it.priority_level) if getattr(it, "priority_level", None) is not None else None,
|
||||
"status": bool(it.status) if getattr(it, "status", None) is not None else None,
|
||||
"children": [],
|
||||
}
|
||||
roots: List[Dict[str, Any]] = []
|
||||
for it in items:
|
||||
node = nodes[int(it.id)]
|
||||
pid = getattr(it, "parent_id", None)
|
||||
if pid is None:
|
||||
roots.append(node)
|
||||
else:
|
||||
parent = nodes.get(int(pid))
|
||||
if parent:
|
||||
parent["children"].append(node)
|
||||
else:
|
||||
roots.append(node)
|
||||
return {"message": "success", "data": roots}
|
||||
@@ -0,0 +1,541 @@
|
||||
from sqlalchemy import Column, BigInteger, Integer, String, DateTime, Text, Boolean, Float, Index, UniqueConstraint
|
||||
from .database import Base
|
||||
import datetime
|
||||
|
||||
|
||||
class Role(Base):
|
||||
__tablename__ = "role"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name = Column(String(100), nullable=False, unique=True)
|
||||
description = Column(String(255))
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# relationships
|
||||
# relationships are intentionally omitted; handle joins in SQL queries as needed
|
||||
|
||||
|
||||
class Permission(Base):
|
||||
__tablename__ = "permission"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
path = Column(String(255), nullable=False)
|
||||
method = Column(String(20), nullable=False)
|
||||
description = Column(String(255))
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
|
||||
|
||||
class Unit(Base):
|
||||
__tablename__ = "units"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
description = Column(Text)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
__table_args__ = (Index('ix_units_name', 'name'),)
|
||||
|
||||
|
||||
class User(Base):
|
||||
|
||||
__tablename__ = "users"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
email = Column(String(255), nullable=False, unique=True)
|
||||
fullname = Column(String(255), nullable=False)
|
||||
unit_id = Column(BigInteger, nullable=True)
|
||||
role_id = Column(BigInteger, nullable=True)
|
||||
status = Column(Integer, default=1)
|
||||
#status 1 is created, 2 is active, 3 is disabled
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
hashed_password = Column(String(255), nullable=True)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
__table_args__ = (Index('ix_users_email', 'email'),)
|
||||
|
||||
class Category(Base):
|
||||
__tablename__ = "category"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name = Column(String(255), nullable=False, unique=True)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
class Post(Base):
|
||||
__tablename__ = "posts"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
content = Column(Text, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
title = Column(String(255), nullable=False)
|
||||
created_by = Column(BigInteger)
|
||||
category_id = Column(BigInteger)
|
||||
status = Column(Boolean, default=True)
|
||||
thumbnail = Column(String(255), nullable=True)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
class Month(Base):
|
||||
__tablename__ = "month"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name = Column(String(50), nullable=False)
|
||||
from_date = Column(DateTime, nullable=False)
|
||||
end_date = Column(DateTime, nullable=False)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
class NAC(Base):
|
||||
__tablename__ = "nac"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
month_id = Column(BigInteger, nullable=False)
|
||||
unit_id = Column(BigInteger, nullable=False)
|
||||
total = Column(BigInteger, default=0)
|
||||
installed = Column(BigInteger, default=0)
|
||||
ignored = Column(BigInteger, default=0)
|
||||
rate = Column(Float, default=0)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
class SmartIR(Base):
|
||||
__tablename__ = "smartir"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
month_id = Column(BigInteger, nullable=False)
|
||||
unit_id = Column(BigInteger, nullable=False)
|
||||
total = Column(BigInteger, default=0)
|
||||
installed = Column(BigInteger, default=0)
|
||||
new_install = Column(BigInteger, default=0)
|
||||
ignored = Column(BigInteger, default=0)
|
||||
rate = Column(Float, default=0)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
class Compliance(Base):
|
||||
__tablename__ = "compliance"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
month_id = Column(BigInteger, nullable=False)
|
||||
unit_id = Column(BigInteger, nullable=False)
|
||||
windows_key = Column(BigInteger, default=0)
|
||||
office = Column(BigInteger, default=0)
|
||||
ms17010 = Column(BigInteger, default=0)
|
||||
firewall = Column(BigInteger, default=0)
|
||||
uac = Column(BigInteger, default=0)
|
||||
winrar = Column(BigInteger, default=0)
|
||||
antivirus = Column(BigInteger, default=0)
|
||||
update_win = Column(BigInteger, default=0)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
class LogSource(Base):
|
||||
__tablename__ = "logsource"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
month_id = Column(BigInteger, nullable=False)
|
||||
unit_id = Column(BigInteger, nullable=False)
|
||||
log_id = Column(BigInteger)
|
||||
name = Column(String(255), nullable=False)
|
||||
description = Column(Text)
|
||||
is_enabled = Column(Boolean, default=True)
|
||||
source_type_id = Column(BigInteger)
|
||||
status = Column(Integer) # lưu mã 1=OK, 2=ERROR, 3=DISABLE, 4=NOT AVAILABLE
|
||||
message = Column(String(255))
|
||||
other = Column(String(255))
|
||||
file_id = Column(BigInteger, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
class LogSourceComment(Base):
|
||||
__tablename__ = "logsource_comment"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
logsource_id = Column(BigInteger, nullable=False)
|
||||
user_id = Column(BigInteger, nullable=False)
|
||||
comment = Column(Text, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
|
||||
class SourceType(Base):
|
||||
__tablename__ = "source_type"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
type = Column(String(100))
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
class Ticket(Base):
|
||||
__tablename__ = "ticket"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
open_count = Column(BigInteger, default=0)
|
||||
in_process = Column(BigInteger, default=0)
|
||||
completed = Column(BigInteger, default=0)
|
||||
unit_id = Column(BigInteger, nullable=False)
|
||||
month_id = Column(BigInteger, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
class RootAccess(Base):
|
||||
__tablename__ = "root_access"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
month_id = Column(BigInteger, nullable=False)
|
||||
unit_id = Column(BigInteger, nullable=False)
|
||||
number = Column(BigInteger, default=0)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
class AttackStatics(Base):
|
||||
__tablename__ = "attack_statics"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
month_id = Column(BigInteger, nullable=False)
|
||||
siem_fintech = Column(BigInteger, default=0)
|
||||
siem_media = Column(BigInteger, default=0)
|
||||
ticket = Column(BigInteger, default=0)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
class SmartIRDetail(Base):
|
||||
__tablename__ = "smartir_detail"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
month_id = Column(BigInteger, nullable=False)
|
||||
unit_id = Column(BigInteger, nullable=True)
|
||||
vnpt_ma_nhan_vien = Column(String(255), nullable=True)
|
||||
name = Column(String(255), nullable=True)
|
||||
email = Column(String(255), nullable=True)
|
||||
phong_ban = Column(String(255), nullable=True)
|
||||
ip = Column(String(255), nullable=True)
|
||||
pc_name = Column(String(255), nullable=True)
|
||||
mac = Column(String(255), nullable=True)
|
||||
agent_version = Column(String(255), nullable=True)
|
||||
last_online = Column(String(255), nullable=True)
|
||||
status = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
class NACDetail(Base):
|
||||
__tablename__ = "nac_detail"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
month_id = Column(BigInteger, nullable=False)
|
||||
unit_id = Column(BigInteger, nullable=True)
|
||||
name = Column(String(255), nullable=True)
|
||||
email = Column(String(255), nullable=True)
|
||||
status = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class ComplianceDetail(Base):
|
||||
__tablename__ = "compliance_detail"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
unit_id = Column(BigInteger, nullable=True)
|
||||
month_id = Column(BigInteger, nullable=False)
|
||||
vnpt_ma_nhan_vien = Column(String(255), nullable=True)
|
||||
name = Column(String(255), nullable=True)
|
||||
email = Column(String(255), nullable=True)
|
||||
phong_ban = Column(String(255), nullable=True)
|
||||
windows_key = Column(BigInteger, nullable=True)
|
||||
office = Column(BigInteger, nullable=True)
|
||||
ms17010 = Column(BigInteger, nullable=True)
|
||||
firewall = Column(BigInteger, nullable=True)
|
||||
uac = Column(BigInteger, nullable=True)
|
||||
winrar = Column(BigInteger, nullable=True)
|
||||
antivirus = Column(BigInteger, nullable=True)
|
||||
update_win = Column(BigInteger, nullable=True)
|
||||
status = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class ComplianceSmartIR(Base):
|
||||
__tablename__ = "compliance_smartir"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
month_id = Column(BigInteger, nullable=False)
|
||||
unit_id = Column(BigInteger, nullable=True)
|
||||
vnpt_ma_nhan_vien = Column(String(20), nullable=False)
|
||||
name = Column(String(255), nullable=False)
|
||||
email = Column(String(255), nullable=False)
|
||||
phong_ban = Column(String(255), nullable=True)
|
||||
ten_may_tinh = Column(String(255), nullable=True)
|
||||
mac = Column(String(50), nullable=True)
|
||||
active_windows = Column(Integer, nullable=True)
|
||||
bat_windows_firewall = Column(Integer, nullable=True)
|
||||
bat_uac = Column(Integer, nullable=True)
|
||||
cai_winrar_ban_moi = Column(Integer, nullable=True)
|
||||
bat_av_fullscan = Column(Integer, nullable=True)
|
||||
update_windows = Column(Integer, nullable=True)
|
||||
status = Column(Integer, nullable=False, default=0)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class MailHistory(Base):
|
||||
__tablename__ = "mail_history"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
post_id = Column(BigInteger)
|
||||
subject = Column(String(255), nullable=True)
|
||||
role_id = Column(BigInteger, nullable=True)
|
||||
content = Column(Text)
|
||||
status = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# relationships intentionally omitted
|
||||
|
||||
|
||||
class FileUpload(Base):
|
||||
__tablename__ = "file_upload"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
path = Column(String(255), nullable=False)
|
||||
table_name = Column(String(255), nullable=False)
|
||||
table_id = Column(BigInteger, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
__table_args__ = (Index('ix_fileupload_table', 'table_name', 'table_id'),)
|
||||
|
||||
|
||||
class ImportHistory(Base):
|
||||
__tablename__ = "import_history"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
type = Column(String(255), nullable=False)
|
||||
file_path = Column(String(255), nullable=False)
|
||||
month_id = Column(BigInteger, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
__table_args__ = (Index('ix_import_history_type_month', 'type', 'month_id'),)
|
||||
|
||||
|
||||
class MMFintech(Base):
|
||||
__tablename__ = "mm_fintech"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, index=True)
|
||||
month_id = Column(BigInteger, nullable=False, index=True)
|
||||
unit_id = Column(BigInteger, nullable=True, index=True)
|
||||
file_path = Column(String(255), nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class ConvertHardening(Base):
|
||||
__tablename__ = "convert_hardening"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, index=True)
|
||||
filename = Column(String(255), nullable=False, index=True)
|
||||
user_id = Column(BigInteger, nullable=False, index=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
|
||||
class SystemGroup(Base):
|
||||
__tablename__ = "system_group"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True, autoincrement=True)
|
||||
name = Column(String(255), nullable=False, index=True)
|
||||
description = Column(String(1000), nullable=True)
|
||||
|
||||
class System(Base):
|
||||
__tablename__ = "system"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, index=True, autoincrement=True)
|
||||
name = Column(String(255), nullable=False, index=True)
|
||||
url_ip = Column(String(255), nullable=True, index=True)
|
||||
unit_id = Column(BigInteger, nullable=True, index=True)
|
||||
system_group_id = Column(BigInteger, nullable=True, index=True)
|
||||
description = Column(String(1000), nullable=True)
|
||||
status = Column(Integer, nullable=True, index=True)
|
||||
|
||||
class ManageSystem(Base):
|
||||
__tablename__ = "manage_system"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name = Column(String(255), nullable=False, unique=True)
|
||||
level = Column(Integer, nullable=False)
|
||||
url_ip = Column(String(255), nullable=True)
|
||||
unit_id = Column(BigInteger, nullable=True)
|
||||
parent_id = Column(BigInteger, nullable=True)
|
||||
type = Column(Integer, nullable=True)
|
||||
private = Column(Boolean, default=False)
|
||||
priority_level = Column(Integer, nullable=True)
|
||||
status = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
__table_args__ = (
|
||||
Index('ix_manage_system_name', 'name'),
|
||||
Index('ix_manage_system_level_parent', 'level', 'parent_id'),
|
||||
Index('ix_manage_system_unit', 'unit_id'),
|
||||
)
|
||||
|
||||
class PentestService(Base):
|
||||
__tablename__ = "pentest_service"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name = Column(Text, nullable=True)
|
||||
type = Column(Integer, nullable=False) # 1: Đánh giá ATTT, 2: Công việc phối hợp, 3: Phối hợp xử lý, 4: Khác
|
||||
code = Column(String(50), nullable=False, unique=True)
|
||||
file_path = Column(String(255), nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
due_date = Column(DateTime, nullable=True)
|
||||
unit_id = Column(BigInteger, nullable=True)
|
||||
status = Column(Integer, default=0) # 0: nháp, 1: tạo mới, 2: tiếp nhận, 3: đã xử lý, 4: đóng, 5: hủy
|
||||
user_id = Column(BigInteger, nullable=True)
|
||||
target_id = Column(BigInteger, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class PentestServiceComment(Base):
|
||||
__tablename__ = "pentest_service_comment"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
pentest_service_id = Column(BigInteger, nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
user_id = Column(BigInteger, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class PentestServiceHistory(Base):
|
||||
__tablename__ = "pentest_service_history"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
pentest_service_id = Column(BigInteger, nullable=False)
|
||||
old_status = Column(Integer, nullable=True)
|
||||
new_status = Column(Integer, nullable=False)
|
||||
user_id = Column(BigInteger, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class PentestServiceAssign(Base):
|
||||
__tablename__ = "pentest_service_assign"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
user_id = Column(BigInteger, nullable=True)
|
||||
email = Column(String(255), nullable=True)
|
||||
pentest_service_id = Column(BigInteger, nullable=False)
|
||||
|
||||
|
||||
|
||||
class RevokedToken(Base):
|
||||
__tablename__ = "revoked_token"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
jti = Column(String(64), nullable=False, unique=True, index=True)
|
||||
user_id = Column(BigInteger, nullable=True)
|
||||
revoked_at = Column(DateTime, default=datetime.datetime.utcnow, nullable=False)
|
||||
expires_at = Column(DateTime, nullable=True)
|
||||
|
||||
|
||||
class EmailRole(Base):
|
||||
__tablename__ = "email_role"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
description = Column(String(1000), nullable=True)
|
||||
|
||||
|
||||
class EmailUser(Base):
|
||||
__tablename__ = "email_user"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
email = Column(String(255), nullable=False)
|
||||
name = Column(String(255), nullable=True)
|
||||
role_id = Column(BigInteger, nullable=False)
|
||||
type = Column(Integer, nullable=False)
|
||||
__table_args__ = (Index('ix_email_user_role', 'role_id'),)
|
||||
|
||||
|
||||
class Document(Base):
|
||||
__tablename__ = "documents"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
number_doc = Column(String(255), nullable=False)
|
||||
sign_date = Column(DateTime, nullable=True)
|
||||
level = Column(Integer, nullable=False)
|
||||
validate_date = Column(DateTime, nullable=True)
|
||||
file_path = Column(String(255), nullable=True)
|
||||
|
||||
|
||||
class SecurityIndex(Base):
|
||||
__tablename__ = "security_index"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True, index=True)
|
||||
soc = Column(Integer, default=0)
|
||||
pentest = Column(Integer, default=0)
|
||||
access_policy = Column(Integer, default=0)
|
||||
two_fa_policy = Column(Integer, default=0) # Cannot start with digit in Python, mapped to 2fa_policy if needed
|
||||
config_network = Column(Integer, default=0)
|
||||
config_server = Column(Integer, default=0)
|
||||
patch_security = Column(Integer, default=0)
|
||||
month_id = Column(BigInteger, nullable=False, index=True)
|
||||
unit_id = Column(BigInteger, nullable=False, index=True)
|
||||
description = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class SocTarget(Base):
|
||||
__tablename__ = "soc_target"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
unit_id = Column(BigInteger, nullable=False)
|
||||
priority = Column(Integer, nullable=False) # 1=critical → 5=low
|
||||
name = Column(String(255), nullable=False)
|
||||
code = Column(String(100), unique=True)
|
||||
status = Column(Integer, nullable=False, default=0) # 0:Open, 1:In Progress, 2:Resolved, 3:Closed
|
||||
deadline = Column(DateTime)
|
||||
description = Column(Text, nullable=False)
|
||||
system_id = Column(BigInteger, nullable=True)
|
||||
system_type = Column(Integer, nullable=True)
|
||||
source_ip = Column(Text, nullable=True)
|
||||
destination_ip = Column(Text, nullable=True)
|
||||
soc_target_type_id = Column(BigInteger, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
created_by = Column(BigInteger, nullable=True)
|
||||
|
||||
class SocTargetType(Base):
|
||||
__tablename__ = "soc_target_type"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
|
||||
class TicketRelated(Base):
|
||||
__tablename__ = "ticket_related"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
soc_target_id = Column(BigInteger, nullable=False)
|
||||
related_soc_target_id = Column(BigInteger, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class SocTargetAssign(Base):
|
||||
__tablename__ = "soc_target_assign"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
target_id = Column(BigInteger, nullable=False)
|
||||
user_id = Column(BigInteger, nullable=True)
|
||||
email = Column(String(255), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class SocTargetHistory(Base):
|
||||
__tablename__ = "soc_target_history"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
target_id = Column(BigInteger, nullable=False)
|
||||
field_name = Column(String(100), nullable=False)
|
||||
old_value = Column(Text)
|
||||
new_value = Column(Text)
|
||||
changed_by = Column(BigInteger, nullable=True)
|
||||
changed_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class SocTargetComment(Base):
|
||||
__tablename__ = "soc_target_comment"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
target_id = Column(BigInteger, nullable=False)
|
||||
user_id = Column(BigInteger, nullable=True)
|
||||
email = Column(String(255), nullable=True)
|
||||
content = Column(Text, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
|
||||
class SocTargetAttachment(Base):
|
||||
__tablename__ = "soc_target_attachment"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
target_id = Column(BigInteger, nullable=False)
|
||||
file_name = Column(String(255), nullable=False)
|
||||
file_path = Column(String(500), nullable=False)
|
||||
file_size = Column(BigInteger, nullable=True)
|
||||
file_type = Column(String(100), nullable=True)
|
||||
uploaded_by = Column(BigInteger, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
class PostComment(Base):
|
||||
__tablename__ = "post_comment"
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
post_id = Column(BigInteger, nullable=False)
|
||||
user_id = Column(BigInteger, nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
@@ -0,0 +1,142 @@
|
||||
import socket
|
||||
import struct
|
||||
import hashlib
|
||||
import random
|
||||
|
||||
# RADIUS Attribute Type names (RFC 2865 + common)
|
||||
RADIUS_ATTR_NAMES = {
|
||||
1: "User-Name",
|
||||
6: "Service-Type",
|
||||
8: "Framed-IP-Address",
|
||||
11: "Filter-Id",
|
||||
18: "Reply-Message",
|
||||
25: "Class",
|
||||
26: "Vendor-Specific",
|
||||
27: "Session-Timeout",
|
||||
}
|
||||
|
||||
def encrypt_password(password: str, secret: str, request_authenticator: bytes) -> bytes:
|
||||
"""Mã hóa mật khẩu PAP theo chuẩn RADIUS RFC 2865."""
|
||||
password_bytes = password.encode('utf-8')
|
||||
if len(password_bytes) % 16 != 0:
|
||||
password_bytes += b'\x00' * (16 - len(password_bytes) % 16)
|
||||
|
||||
encrypted = b''
|
||||
last = secret.encode('utf-8') + request_authenticator
|
||||
for i in range(0, len(password_bytes), 16):
|
||||
block = password_bytes[i:i+16]
|
||||
md5_hash = hashlib.md5(last).digest()
|
||||
encrypted_block = bytes(a ^ b for a, b in zip(block, md5_hash))
|
||||
encrypted += encrypted_block
|
||||
last = secret.encode('utf-8') + encrypted_block
|
||||
return encrypted
|
||||
|
||||
|
||||
def build_access_request(username: str, password: str, secret: str,
|
||||
identifier: int = None) -> bytes:
|
||||
"""Tạo gói Access-Request PAP cho RADIUS server."""
|
||||
if identifier is None:
|
||||
identifier = random.randint(0, 255)
|
||||
|
||||
request_authenticator = bytes(random.getrandbits(8) for _ in range(16))
|
||||
|
||||
username_attr = b'\x01' + struct.pack('B', len(username) + 2) + username.encode('utf-8')
|
||||
password_attr_bytes = encrypt_password(password, secret, request_authenticator)
|
||||
password_attr = b'\x02' + struct.pack('B', len(password_attr_bytes) + 2) + password_attr_bytes
|
||||
|
||||
attrs = username_attr + password_attr
|
||||
length = 20 + len(attrs)
|
||||
header = struct.pack('!BBH', 1, identifier, length) + request_authenticator
|
||||
return header + attrs
|
||||
|
||||
|
||||
def parse_radius_attributes(attributes_raw: bytes) -> dict:
|
||||
"""
|
||||
Parse phần attributes của gói RADIUS response.
|
||||
Trả về dict: {type_int: [value_bytes, ...]}
|
||||
"""
|
||||
result: dict[int, list] = {}
|
||||
idx = 0
|
||||
while idx < len(attributes_raw):
|
||||
if idx + 2 > len(attributes_raw):
|
||||
break
|
||||
attr_type = attributes_raw[idx]
|
||||
attr_len = attributes_raw[idx + 1]
|
||||
if attr_len < 2 or idx + attr_len > len(attributes_raw):
|
||||
break
|
||||
attr_value = attributes_raw[idx + 2: idx + attr_len]
|
||||
result.setdefault(attr_type, []).append(attr_value)
|
||||
idx += attr_len
|
||||
return result
|
||||
|
||||
|
||||
def check_radius_login(server: str, port: int, secret: str, username: str,
|
||||
password: str, timeout: int = 5) -> bool:
|
||||
"""
|
||||
Kiểm tra username/password với RADIUS server (PAP).
|
||||
Trả về True nếu Access-Accept, False nếu thất bại.
|
||||
(Giữ nguyên signature để không ảnh hưởng code cũ)
|
||||
"""
|
||||
ok, _ = check_radius_login_extended(server, port, secret, username, password, timeout)
|
||||
return ok
|
||||
|
||||
|
||||
def check_radius_login_extended(server: str, port: int, secret: str, username: str,
|
||||
password: str, timeout: int = 5) -> tuple[bool, dict]:
|
||||
"""
|
||||
Kiểm tra username/password với RADIUS server (PAP).
|
||||
Trả về (success: bool, attributes: dict)
|
||||
attributes chứa các giá trị decode từ RADIUS response, ví dụ:
|
||||
{
|
||||
'filter_id': ['Trung tâm An ninh thông tin'],
|
||||
'reply_message': ['privacyIDEA access granted'],
|
||||
'raw': {11: [b'...'], 18: [b'...']},
|
||||
}
|
||||
"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(timeout)
|
||||
attrs_decoded: dict = {}
|
||||
try:
|
||||
req_packet = build_access_request(username, password, secret)
|
||||
sock.sendto(req_packet, (server, port))
|
||||
|
||||
resp, _ = sock.recvfrom(4096)
|
||||
code = resp[0]
|
||||
length = struct.unpack('!H', resp[2:4])[0]
|
||||
raw_attrs = parse_radius_attributes(resp[20:length])
|
||||
|
||||
# Decode các attributes thường gặp sang chuỗi
|
||||
def _decode_list(raw_list: list) -> list[str]:
|
||||
return [v.decode('utf-8', errors='replace') for v in raw_list]
|
||||
|
||||
attrs_decoded['raw'] = raw_attrs
|
||||
|
||||
# Filter-Id (Type 11) — thường chứa thông tin nhóm/đơn vị
|
||||
if 11 in raw_attrs:
|
||||
attrs_decoded['filter_id'] = _decode_list(raw_attrs[11])
|
||||
|
||||
# Reply-Message (Type 18)
|
||||
if 18 in raw_attrs:
|
||||
attrs_decoded['reply_message'] = _decode_list(raw_attrs[18])
|
||||
|
||||
# Class (Type 25)
|
||||
if 25 in raw_attrs:
|
||||
attrs_decoded['class'] = _decode_list(raw_attrs[25])
|
||||
|
||||
if code == 2: # Access-Accept
|
||||
print(f"[RADIUS] ✅ Đăng nhập thành công: {username}")
|
||||
if attrs_decoded.get('filter_id'):
|
||||
print(f"[RADIUS] Filter-Id: {attrs_decoded['filter_id']}")
|
||||
return True, attrs_decoded
|
||||
else:
|
||||
print(f"[RADIUS] ❌ Sai username hoặc password: {username}")
|
||||
return False, attrs_decoded
|
||||
|
||||
except socket.timeout:
|
||||
print("[RADIUS] ⏱ Timeout khi kết nối đến RADIUS server.")
|
||||
return False, {}
|
||||
except Exception as e:
|
||||
print(f"[RADIUS] ⚠️ Lỗi: {e}")
|
||||
return False, {}
|
||||
finally:
|
||||
sock.close()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,342 @@
|
||||
"""JWT authentication dependencies for FastAPI."""
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer
|
||||
from starlette.requests import Request
|
||||
from app.utils import decode_access_token
|
||||
import jwt
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.models import RevokedToken, User
|
||||
import re
|
||||
import datetime
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
# In-memory revoked token store (non-persistent). Use DB for production.
|
||||
# In-memory fallback (không bền vững). Giữ lại để chặn ngay trong tiến trình hiện tại.
|
||||
REVOKED_TOKENS: set[str] = set()
|
||||
LATEST_JTI_PER_USER: dict[int, str] = {}
|
||||
|
||||
def revoke_token(token: str) -> None:
|
||||
REVOKED_TOKENS.add(token)
|
||||
|
||||
def set_latest_jti(user_id: int, jti: str) -> None:
|
||||
LATEST_JTI_PER_USER[user_id] = jti
|
||||
|
||||
def revoke_token_db(db: Session, token: str) -> None:
|
||||
"""Decode token and persist its jti to DB for revocation."""
|
||||
payload = decode_access_token(token)
|
||||
jti = payload.get("jti")
|
||||
sub = payload.get("sub")
|
||||
exp = payload.get("exp")
|
||||
expires_at = None
|
||||
if isinstance(exp, (int, float)):
|
||||
expires_at = datetime.datetime.utcfromtimestamp(exp)
|
||||
elif isinstance(exp, datetime.datetime):
|
||||
expires_at = exp
|
||||
|
||||
if not jti:
|
||||
# không có jti thì vẫn dùng in-memory để tránh bỏ sót
|
||||
REVOKED_TOKENS.add(token)
|
||||
return
|
||||
|
||||
if not db.query(RevokedToken).filter(RevokedToken.jti == jti).first():
|
||||
db.add(RevokedToken(
|
||||
jti=jti,
|
||||
user_id=int(sub) if sub is not None else None,
|
||||
expires_at=expires_at
|
||||
))
|
||||
db.commit()
|
||||
|
||||
async def get_current_user(request: Request, db: Session = Depends(get_db)):
|
||||
"""Dependency to extract and validate JWT token from request headers.
|
||||
|
||||
Returns the decoded token payload with user info.
|
||||
|
||||
Raises:
|
||||
HTTPException: If token is invalid, expired, or missing
|
||||
"""
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if not auth_header:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing authorization header",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
scheme, token = auth_header.split()
|
||||
if scheme.lower() != "bearer":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication scheme",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authorization header",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Reject revoked tokens (in-memory fallback)
|
||||
if token in REVOKED_TOKENS:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token has been revoked",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
try:
|
||||
payload = decode_access_token(token)
|
||||
# Check DB revocation via jti
|
||||
jti = payload.get("jti")
|
||||
if jti and db.query(RevokedToken).filter(RevokedToken.jti == jti).first():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token has been revoked",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
user_id: str = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
uid_int = int(user_id)
|
||||
latest = LATEST_JTI_PER_USER.get(uid_int)
|
||||
if latest and jti and jti != latest:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token is not the latest",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
u = db.query(User).filter(User.id == uid_int).first()
|
||||
if u:
|
||||
if payload.get("role_id") is None:
|
||||
payload["role_id"] = getattr(u, "role_id", None)
|
||||
if payload.get("unit_id") is None:
|
||||
payload["unit_id"] = getattr(u, "unit_id", None)
|
||||
if not payload.get("email"):
|
||||
payload["email"] = getattr(u, "email", None)
|
||||
if not payload.get("fullname"):
|
||||
payload["fullname"] = getattr(u, "fullname", None)
|
||||
if payload.get("status") is None:
|
||||
payload["status"] = getattr(u, "status", None)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Token has expired",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
except jwt.InvalidTokenError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
# ---- RBAC helpers ----
|
||||
|
||||
# Role IDs
|
||||
ROLE_ADMIN = 1
|
||||
ROLE_DIRECTOR = 2
|
||||
ROLE_MANAGER = 3
|
||||
ROLE_LEADER_PM = 4
|
||||
ROLE_ADMIN_LIMITED = 9
|
||||
|
||||
def _role_id(user: dict) -> int | None:
|
||||
try:
|
||||
rid = user.get("role_id")
|
||||
return int(rid) if rid is not None else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def is_admin(user: dict) -> bool:
|
||||
return _role_id(user) in (ROLE_ADMIN, ROLE_ADMIN_LIMITED)
|
||||
|
||||
def is_director(user: dict) -> bool:
|
||||
return _role_id(user) == ROLE_DIRECTOR
|
||||
|
||||
def is_manager(user: dict) -> bool:
|
||||
return _role_id(user) == ROLE_MANAGER
|
||||
|
||||
def is_leader_pm(user: dict) -> bool:
|
||||
return _role_id(user) == ROLE_LEADER_PM
|
||||
|
||||
def get_user_email(user: dict) -> str:
|
||||
return (user.get("email") or "").strip()
|
||||
|
||||
def get_user_unit_id(user: dict) -> int | None:
|
||||
try:
|
||||
uid = user.get("unit_id")
|
||||
return int(uid) if uid is not None else None
|
||||
except Exception:
|
||||
return None
|
||||
# RBAC
|
||||
ROLE_ADMIN = 1
|
||||
ROLE_DIRECTOR = 2
|
||||
ROLE_MANAGER = 3
|
||||
ROLE_LEADER_PM = 4
|
||||
ROLE_ADMIN_LIMITED = 9
|
||||
|
||||
def _role_id(user: dict) -> int | None:
|
||||
try:
|
||||
rid = user.get("role_id")
|
||||
return int(rid) if rid is not None else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
# def is_admin(user: dict) -> bool:
|
||||
# return _role_id(user) in (ROLE_ADMIN, ROLE_ADMIN_LIMITED)
|
||||
|
||||
# def is_director(user: dict) -> bool:
|
||||
# return _role_id(user) == ROLE_DIRECTOR
|
||||
|
||||
# def is_manager(user: dict) -> bool:
|
||||
# return _role_id(user) == ROLE_MANAGER
|
||||
|
||||
# def is_leader_pm(user: dict) -> bool:
|
||||
# return _role_id(user) == ROLE_LEADER_PM
|
||||
|
||||
# def get_user_email(user: dict) -> str:
|
||||
# return (user.get("email") or "").strip()
|
||||
|
||||
def _method_path(request: Request) -> tuple[str, str]:
|
||||
return request.method.upper(), request.url.path.lower()
|
||||
|
||||
def _path_match(path: str, patterns: list[str]) -> bool:
|
||||
for p in patterns:
|
||||
q = p.lower()
|
||||
if "*" in q:
|
||||
regex = "^" + re.escape(q).replace("\\*", ".*") + "$"
|
||||
if re.match(regex, path):
|
||||
return True
|
||||
else:
|
||||
if path == q or path.startswith(q + "/"):
|
||||
return True
|
||||
return False
|
||||
|
||||
DIRECTOR_GET = [
|
||||
"/months",
|
||||
"/units",
|
||||
"/jira/tickets-overview",
|
||||
"/scorecard*",
|
||||
"/overview/nac",
|
||||
"/overview/compliance",
|
||||
"/months/*/compliance",
|
||||
"/overview/smart-ir",
|
||||
"/pentest/get-overview",
|
||||
"/pentest/vuln-lastest-12month",
|
||||
"/pentest/category",
|
||||
"/pentest/category-vuln",
|
||||
"/pentest/get-top-vulns",
|
||||
"/pentest/get-target-detail",
|
||||
"/pentest/download-target-checklist",
|
||||
"/months/*/files",
|
||||
"/files/download",
|
||||
"/jira/tickets-week",
|
||||
"/jira/tickets-12-month-lastest",
|
||||
"/months/*/root-access",
|
||||
"/months/*/attack-statics",
|
||||
"/months/*/logsource",
|
||||
"/months/*/nac-smartir",
|
||||
"/months/*/compliance",
|
||||
"/posts",
|
||||
"/categories",
|
||||
"/documents",
|
||||
"/documents/download",
|
||||
"/months/*/pam-tsc",
|
||||
"/hardening/*",
|
||||
"/pentest/log_history/*",
|
||||
"/pentest-service*",
|
||||
"/logsource/*/comments",
|
||||
]
|
||||
DIRECTOR_POST = [
|
||||
"/auth/login",
|
||||
"/auth/logout",
|
||||
"/pentest-service*",
|
||||
]
|
||||
|
||||
MANAGER_GET = DIRECTOR_GET + [
|
||||
"/pentest/update-vuln-status",
|
||||
]
|
||||
MANAGER_POST = DIRECTOR_POST
|
||||
|
||||
LEADER_GET = MANAGER_GET
|
||||
LEADER_POST = MANAGER_POST
|
||||
|
||||
def enforce_rbac(request: Request, user: dict = Depends(get_current_user)):
|
||||
method, path = _method_path(request)
|
||||
|
||||
# Cho phép tất cả các nhóm người dùng đã đăng nhập truy cập vào /pentest-service
|
||||
if "pentest-service" in path:
|
||||
return
|
||||
|
||||
rid = _role_id(user)
|
||||
if rid == ROLE_ADMIN:
|
||||
return
|
||||
if rid == ROLE_ADMIN_LIMITED:
|
||||
deny_get = [
|
||||
# "/pentest/category-vuln",
|
||||
"/pentest/get-target-detail",
|
||||
"/pentest/download-target-checklist",
|
||||
]
|
||||
deny_post = [
|
||||
"/users",
|
||||
"/roles",
|
||||
]
|
||||
deny_put = [
|
||||
"/users/*",
|
||||
"/roles/*",
|
||||
]
|
||||
deny_delete = [
|
||||
"/users/*",
|
||||
"/roles/*",
|
||||
]
|
||||
if method == "GET" and _path_match(path, deny_get):
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
if method == "POST" and _path_match(path, deny_post):
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
if method == "PUT" and _path_match(path, deny_put):
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
if method == "DELETE" and _path_match(path, deny_delete):
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
return
|
||||
|
||||
# Cho phép các method khác GET cho pentest-service
|
||||
NONGET_ALLOWED_ALL_ROLES = [
|
||||
"/systems/search-leak",
|
||||
"/hardening/convert/upload",
|
||||
"/pentest-service*",
|
||||
"/posts/*/comments", # Cho phép tất cả người dùng đã đăng nhập thêm comment
|
||||
"/logsource/*/comments", # Cho phép tất cả người dùng đã đăng nhập thêm bình luận LogSource
|
||||
]
|
||||
|
||||
if method != "GET":
|
||||
if _path_match(path, NONGET_ALLOWED_ALL_ROLES):
|
||||
return
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
if is_director(user):
|
||||
allowed = DIRECTOR_GET if method == "GET" else DIRECTOR_POST
|
||||
elif is_manager(user):
|
||||
allowed = MANAGER_GET if method == "GET" else MANAGER_POST
|
||||
elif is_leader_pm(user):
|
||||
allowed = LEADER_GET if method == "GET" else LEADER_POST
|
||||
else:
|
||||
# Nếu không thuộc các nhóm trên nhưng là /pentest-service thì đã return ở trên
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
|
||||
if not _path_match(path, allowed):
|
||||
raise HTTPException(status_code=403, detail="Forbidden")
|
||||
return
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
from passlib.context import CryptContext
|
||||
import hashlib
|
||||
import jwt
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any
|
||||
import os
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
import contextvars
|
||||
|
||||
# Password hashing
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
"""Hash a plain-text password using PBKDF2-SHA256.
|
||||
|
||||
This supports arbitrary password lengths and avoids backend problems.
|
||||
"""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def verify_password(plain: str, hashed: str) -> bool:
|
||||
return pwd_context.verify(plain, hashed)
|
||||
# return True
|
||||
|
||||
|
||||
# JWT configuration
|
||||
JWT_SECRET = os.getenv("JWT_SECRET", "your-secret-key-change-this-in-production")
|
||||
JWT_ALGORITHM = "HS256"
|
||||
JWT_EXPIRATION_HOURS = int(os.getenv("JWT_EXPIRATION_HOURS", 24))
|
||||
|
||||
|
||||
def create_access_token(data: Dict[str, Any], expires_delta: timedelta = None) -> str:
|
||||
"""Create a JWT access token.
|
||||
|
||||
Args:
|
||||
data: Dictionary of claims to encode
|
||||
expires_delta: Optional timedelta for token expiration; defaults to JWT_EXPIRATION_HOURS
|
||||
|
||||
Returns:
|
||||
JWT token string
|
||||
"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(hours=JWT_EXPIRATION_HOURS)
|
||||
to_encode.update({"exp": expire, "jti": uuid.uuid4().hex, "iat": datetime.utcnow()})
|
||||
encoded_jwt = jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def decode_access_token(token: str) -> Dict[str, Any]:
|
||||
"""Decode and verify a JWT access token.
|
||||
|
||||
Args:
|
||||
token: JWT token string
|
||||
|
||||
Returns:
|
||||
Decoded payload dictionary
|
||||
|
||||
Raises:
|
||||
jwt.InvalidTokenError: If token is invalid or expired
|
||||
"""
|
||||
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
||||
|
||||
def audit_log_path() -> str:
|
||||
base = os.getenv("AUDIT_LOG_DIR", "audit_logs")
|
||||
p = Path(base).resolve()
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
now = datetime.utcnow()
|
||||
month_dir = p / now.strftime("%Y%m")
|
||||
month_dir.mkdir(parents=True, exist_ok=True)
|
||||
return str(month_dir / f"{now.strftime('%Y-%m-%d')}.txt")
|
||||
|
||||
# Per-request user context for audit logging
|
||||
CURRENT_USER_CTX: contextvars.ContextVar = contextvars.ContextVar("CURRENT_USER_CTX", default=None)
|
||||
|
||||
def set_current_user_ctx(user: dict | None) -> None:
|
||||
try:
|
||||
CURRENT_USER_CTX.set(user)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_current_user_ctx() -> dict | None:
|
||||
try:
|
||||
return CURRENT_USER_CTX.get()
|
||||
except Exception:
|
||||
return None
|
||||
Reference in New Issue
Block a user