zioinfo-mail/itsm/test_d2_mfa.py
DESKTOP-TKLFCPR\ython e228faabf5 feat(itsm): G-1~G-12 확장 기능 + 하네스/봇/설치스크립트 구현
G-1: 메신저 Webhook Relay + _send_to_room 실제 httpx 호출 구현
G-2: POST /api/tasks/bulk SR 대량작업 엔드포인트 (최대 100건)
G-3: 라이선스 만료 알림 스케줄러 (매일 09:00 KST)
G-4: 체험판 upgrade_banner 필드 + license.py 배너 로직
G-5: core/auto_rca.py + incidents/problem auto-rca 엔드포인트
G-6: core/deploy_impact.py + vibe impact-analysis 엔드포인트
G-7: core/ticket_classifier.py + SR 생성 시 AI 분류 + ai-suggestion API
G-8: VulnPatchRecord 모델 + vuln_scan 패치추적 4개 엔드포인트
G-9: core/jira_sync.py + gateway Jira/Confluence 연동 엔드포인트
G-10: core/push_notify.py + routers/push.py + PushSubscription 모델
G-11: approvals 다중승인 (위임/서명/기한초과/마감연장)
G-12: alembic.ini + migrations/ + cicd/migrate_to_postgres.sh

하네스: guardia-orchestrator 확장기능 Phase 반영
봇명령어: /sr /status /license /bulk 슬래시 명령어 추가
설치스크립트: setup/ (Ubuntu, CentOS, RHEL, Windows) --test 옵션 포함

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:18:52 +09:00

145 lines
6.2 KiB
Python

"""D-2 MFA (TOTP) 테스트"""
import sys, ast, os
os.environ.setdefault("GUARDIA_SECRET_KEY", "test-mfa-secret-key-32bytes-pad!!")
os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_d2.db")
# ── 1. 구문 검사 ─────────────────────────────────────────────────────────────
print("=== 1. 구문 검사 ===")
files = ["core/mfa.py", "routers/auth.py", "models.py"]
ok = True
for f in files:
try:
with open(f, encoding="utf-8") as fh:
src = fh.read()
ast.parse(src)
print(f" OK {f}")
except SyntaxError as e:
print(f" ERR {f}: {e}")
ok = False
if not ok:
sys.exit(1)
# ── 2. AES-GCM 암호화/복호화 라운드트립 ─────────────────────────────────────
print("\n=== 2. AES-GCM 암호화/복호화 테스트 ===")
from core.mfa import (
generate_totp_secret,
get_totp_uri,
verify_totp,
generate_qr_base64,
encrypt_totp_secret,
decrypt_totp_secret,
create_mfa_pending_token,
verify_mfa_pending_token,
_verify_totp_fallback,
)
secret = generate_totp_secret()
print(f" OK generate_totp_secret: {secret[:8]}... (len={len(secret)})")
enc = encrypt_totp_secret(secret)
dec = decrypt_totp_secret(enc)
assert dec == secret, f"복호화 실패: {dec!r} != {secret!r}"
print(f" OK AES-GCM 암호화/복호화 라운드트립")
# 다른 시크릿과 다른지 확인
enc2 = encrypt_totp_secret(secret)
assert enc != enc2, "동일 평문이 항상 다른 ciphertext 생성 (nonce 랜덤화)"
print(f" OK 랜덤 nonce로 매번 다른 ciphertext 생성")
# ── 3. TOTP URI 생성 ─────────────────────────────────────────────────────────
print("\n=== 3. TOTP URI 생성 테스트 ===")
uri = get_totp_uri("test_user", secret)
assert "otpauth://totp/" in uri, f"URI 포맷 오류: {uri[:60]}"
assert "GUARDiA" in uri or "guardia" in uri.lower(), f"issuer 없음: {uri[:80]}"
assert secret in uri, "secret 미포함"
print(f" OK TOTP URI: {uri[:60]}...")
# ── 4. QR 코드 생성 (옵션) ──────────────────────────────────────────────────
print("\n=== 4. QR 코드 생성 테스트 ===")
qr = generate_qr_base64(uri)
if qr:
assert len(qr) > 100, "QR base64 너무 짧음"
print(f" OK QR base64 생성 (len={len(qr)})")
else:
print(f" INFO qrcode not installed - QR skip (returns None)")
# ── 5. TOTP 검증 (폴백 함수 사용) ───────────────────────────────────────────
print("\n=== 5. TOTP 검증 테스트 ===")
# 현재 코드 생성 후 검증 (pyotp 있는 경우)
try:
import pyotp
totp_obj = pyotp.TOTP(secret)
current_code = totp_obj.now()
result = verify_totp(secret, current_code)
assert result == True, f"현재 코드 검증 실패: {current_code}"
print(f" OK 현재 TOTP 코드 검증 성공 (pyotp)")
# 잘못된 코드
assert verify_totp(secret, "000000") == False or verify_totp(secret, "000000") == True # 우연히 맞을 수 있음
assert verify_totp(secret, "abc123") == False, "비숫자 코드는 무효"
assert verify_totp(secret, "12345") == False, "5자리는 무효"
print(f" OK 잘못된 코드 거부 (형식 검사)")
except ImportError:
print(f" INFO pyotp not installed - using fallback")
# 폴백은 실제 코드 없이 테스트 불가 (시간 기반)
assert verify_totp(secret, "abc123") == False
print(f" OK 잘못된 코드 거부 (폴백)")
# ── 6. MFA 대기 토큰 발급/검증 ──────────────────────────────────────────────
print("\n=== 6. MFA 대기 토큰 테스트 ===")
mfa_token = create_mfa_pending_token("alice")
assert mfa_token, "토큰 생성 실패"
print(f" OK MFA 대기 토큰 생성: {mfa_token[:20]}...")
username = verify_mfa_pending_token(mfa_token)
assert username == "alice", f"사용자 추출 실패: {username}"
print(f" OK MFA 대기 토큰 검증: sub={username}")
# 일반 access_token은 mfa_pending 토큰으로 거부
from core.auth import create_access_token
normal_token = create_access_token({"sub": "alice", "role": "ENGINEER"})
assert verify_mfa_pending_token(normal_token) is None, "일반 토큰이 mfa_pending으로 통과됨!"
print(f" OK 일반 access_token은 mfa_pending 검증 거부")
# 위조된 토큰 거부
assert verify_mfa_pending_token("invalid.token.here") is None
print(f" OK 위조 토큰 거부")
# ── 7. models.py User 컬럼 확인 ─────────────────────────────────────────────
print("\n=== 7. User 모델 MFA 컬럼 확인 ===")
from models import User, UserOut
cols = {c.key for c in User.__table__.columns}
for col in ["mfa_enabled", "totp_secret_enc"]:
status = "OK" if col in cols else "ERR"
print(f" {status} User.{col}")
if status == "ERR":
ok = False
out_fields = UserOut.model_fields
status = "OK" if "mfa_enabled" in out_fields else "ERR"
print(f" {status} UserOut.mfa_enabled")
# ── 8. routers/auth.py 엔드포인트 확인 ─────────────────────────────────────
print("\n=== 8. auth.py MFA 엔드포인트 확인 ===")
with open("routers/auth.py", encoding="utf-8") as f:
auth_src = f.read()
endpoints = [
("/mfa/setup", "mfa_setup"),
("/mfa/enable", "mfa_enable"),
("/mfa/disable", "mfa_disable"),
("/mfa/status", "mfa_status"),
("/login/mfa", "login_mfa"),
("/admin/users", "admin_mfa_reset"),
]
for path, fn in endpoints:
status = "OK" if path in auth_src and fn in auth_src else "ERR"
print(f" {status} {path} ({fn})")
print("\n=== 테스트 완료: D-2 MFA (TOTP) ===")
if ok:
print("모든 검사 통과")
else:
sys.exit(1)