This commit is contained in:
Luftmensch
2026-08-26 14:11:37 +07:00
commit e528419f9a
135 changed files with 413970 additions and 0 deletions
View File
+314
View File
@@ -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
+114
View File
@@ -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)
+301
View File
@@ -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}
+541
View File
@@ -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)
+142
View File
@@ -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
+342
View File
@@ -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
+90
View File
@@ -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