"""D-1 LDAP/AD 연동 테스트""" import sys, ast, os os.environ.setdefault("GUARDIA_SECRET_KEY", "test-d1-secret-key-32bytes-padded!") os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./test_d1.db") ok = True print("=== 1. 구문 검사 ===") files = ["core/ldap_auth.py", "routers/ldap.py", "main.py"] 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 print("\n=== 2. core/ldap_auth.py 핵심 기능 확인 ===") with open("core/ldap_auth.py", encoding="utf-8") as f: ldap_src = f.read() ldap_checks = [ ("class LDAPConfig", "LDAPConfig 데이터클래스"), ("bind_password", "bind_password 필드"), ("DEFAULT_GROUP_ROLE_MAP", "기본 그룹→역할 매핑"), ("def _load_config_from_env", "_load_config_from_env 함수"), ("def init_ldap_config", "init_ldap_config 함수"), ("def get_ldap_config", "get_ldap_config 함수"), ("def set_group_role_map", "set_group_role_map 함수"), ("def map_groups_to_role", "map_groups_to_role 함수"), ("def authenticate_ldap", "authenticate_ldap 함수"), ("async def sync_ldap_user", "sync_ldap_user 비동기 함수"), ("def test_ldap_connection", "test_ldap_connection 함수"), ("LDAP_SERVER_URL", "LDAP_SERVER_URL 환경변수"), ("LDAP_BIND_PASSWORD", "LDAP_BIND_PASSWORD 환경변수"), ("LDAP_ENABLED", "LDAP_ENABLED 환경변수"), ("ldap3", "ldap3 패키지 참조"), ('"GUARDiA-ADMIN"', "GUARDiA-ADMIN 그룹 매핑"), ('"Domain Admins"', "Domain Admins 그룹 매핑"), ("절대 로그", "bind_password 로그 금지 주석"), ] for sym, desc in ldap_checks: status = "OK" if sym in ldap_src else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== 3. routers/ldap.py 엔드포인트 확인 ===") with open("routers/ldap.py", encoding="utf-8") as f: router_src = f.read() router_checks = [ ('@router.get("/status"', "GET /status"), ('@router.post("/test"', "POST /test (연결 테스트)"), ('@router.post("/authenticate"', "POST /authenticate (인증 테스트)"), ('@router.get("/config"', "GET /config"), ('@router.put("/config"', "PUT /config"), ('@router.get("/group-map"', "GET /group-map"), ('@router.put("/group-map"', "PUT /group-map"), ('/sync/', "POST /sync/{username}"), ('@router.get("/users"', "GET /users"), ('"***"', "bind_password 마스킹"), ('"bind_password", None', "응답에서 bind_password 제거"), ('UserRole.ADMIN', "ADMIN 권한 검증"), ] for sym, desc in router_checks: found = sym in router_src status = "OK" if found else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== 4. main.py D-1 등록 확인 ===") with open("main.py", encoding="utf-8") as f: main_src = f.read() for sym, desc in [ ("ldap", "ldap import"), ("ldap.router", "ldap 라우터 등록"), ("D-1", "D-1 섹션 주석"), ]: status = "OK" if sym in main_src else "ERR" if status == "ERR": ok = False print(f" {status} {desc}") import sys as _sys import importlib.util as _ilu if "." not in _sys.path: _sys.path.insert(0, ".") def _load_ldap_mod(): """core/ldap_auth.py를 매번 새 모듈로 로드.""" import time mod_name = f"_ldap_auth_{int(time.time()*1000)}" spec = _ilu.spec_from_file_location(mod_name, "core/ldap_auth.py") if spec is None: raise ImportError("spec_from_file_location 실패") m = _ilu.module_from_spec(spec) m.__package__ = "" # Python 3.14: @dataclass 가 sys.modules[cls.__module__].__dict__ 를 사용 _sys.modules[mod_name] = m try: spec.loader.exec_module(m) finally: # 테스트 후 정리 (sys.modules 오염 방지) _sys.modules.pop(mod_name, None) return m print("\n=== 5. 환경변수 로딩 테스트 ===") try: mod = _load_ldap_mod() # 환경변수 없을 때 기본값 cfg = mod.get_ldap_config() assert cfg.enabled == False, f"기본값 enabled=False 기대: {cfg.enabled}" print(" OK 기본값 enabled=False") assert cfg.server_url == "", f"기본값 server_url='' 기대: {cfg.server_url}" print(" OK 기본값 server_url=''") # 환경변수 설정 후 로딩 os.environ["LDAP_ENABLED"] = "true" os.environ["LDAP_SERVER_URL"] = "ldap://192.168.0.10:389" os.environ["LDAP_BASE_DN"] = "DC=company,DC=local" os.environ["LDAP_BIND_DN"] = "CN=svc-ldap,DC=company,DC=local" os.environ["LDAP_BIND_PASSWORD"] = "secret123" # 강제 재로드 mod._current_config = None cfg2 = mod.get_ldap_config() assert cfg2.enabled == True, f"enabled=True 기대: {cfg2.enabled}" assert cfg2.server_url == "ldap://192.168.0.10:389", f"server_url 오류: {cfg2.server_url}" assert cfg2.bind_dn == "CN=svc-ldap,DC=company,DC=local" print(" OK 환경변수에서 설정 로딩") # bind_password 로그 노출 없음 확인 import logging, io log_buf = io.StringIO() handler = logging.StreamHandler(log_buf) mod.logger.addHandler(handler) try: mod.authenticate_ldap("testuser", "testpass") except Exception: pass log_output = log_buf.getvalue() assert "secret123" not in log_output, "bind_password가 로그에 노출됨!" print(" OK bind_password 로그 미노출") except AssertionError as e: print(f" ERR {e}") ok = False except Exception as e: print(f" ERR 환경변수 로딩 오류: {type(e).__name__}: {e}") ok = False finally: for key in ["LDAP_ENABLED", "LDAP_SERVER_URL", "LDAP_BASE_DN", "LDAP_BIND_DN", "LDAP_BIND_PASSWORD"]: os.environ.pop(key, None) print("\n=== 6. map_groups_to_role 역할 우선순위 테스트 ===") try: mod2 = _load_ldap_mod() # ADMIN > PM > ENGINEER > VIEWER role = mod2.map_groups_to_role(["GUARDiA-ENGINEER", "Domain Admins"]) assert role == "ADMIN", f"ADMIN이 우선이어야 함: {role}" print(f" OK [ENGINEER, Domain Admins] -> {role}") role = mod2.map_groups_to_role(["GUARDiA-PM", "GUARDiA-VIEWER"]) assert role == "PM", f"PM이 우선이어야 함: {role}" print(f" OK [PM, VIEWER] -> {role}") role = mod2.map_groups_to_role(["GUARDiA-VIEWER"]) assert role == "VIEWER", f"VIEWER 기대: {role}" print(f" OK [VIEWER] -> {role}") role = mod2.map_groups_to_role(["Unknown-Group"]) assert role == "VIEWER", f"알 수 없는 그룹 -> VIEWER: {role}" print(f" OK [Unknown-Group] -> {role} (기본값)") # 부분 일치 테스트 role = mod2.map_groups_to_role(["CN=GUARDiA-ADMIN,OU=Groups,DC=company,DC=local"]) print(f" OK CN= 형식 그룹: role={role}") except AssertionError as e: print(f" ERR {e}") ok = False except Exception as e: print(f" ERR map_groups_to_role 오류: {type(e).__name__}: {e}") ok = False print("\n=== 7. set_group_role_map 업데이트 테스트 ===") try: mod3 = _load_ldap_mod() mod3.set_group_role_map({"CustomGroup-Dev": "ENGINEER", "CustomGroup-Lead": "PM"}) role = mod3.map_groups_to_role(["CustomGroup-Dev"]) assert role == "ENGINEER", f"커스텀 그룹 매핑 오류: {role}" print(f" OK CustomGroup-Dev -> {role}") role = mod3.map_groups_to_role(["CustomGroup-Lead"]) assert role == "PM", f"커스텀 그룹 매핑 오류: {role}" print(f" OK CustomGroup-Lead -> {role}") role = mod3.map_groups_to_role(["Domain Admins"]) assert role == "ADMIN", f"기본 매핑 유지 실패: {role}" print(f" OK Domain Admins -> {role} (기본 매핑 유지)") except AssertionError as e: print(f" ERR {e}") ok = False except Exception as e: print(f" ERR set_group_role_map 오류: {type(e).__name__}: {e}") ok = False print("\n=== 8. LDAP 비활성화 Fallback 테스트 ===") try: mod4 = _load_ldap_mod() # 비활성화 상태에서 인증 시도 success, info, err = mod4.authenticate_ldap("user", "pass") assert success == False, f"비활성 LDAP에서 success=False 기대: {success}" assert "비활성화" in err or "LDAP" in err, f"오류 메시지 확인: {err}" print(f" OK 비활성 LDAP -> success=False, err='{err}'") # 연결 테스트도 비활성 반환 result = mod4.test_ldap_connection() assert result["success"] == False print(f" OK test_ldap_connection 비활성 -> success=False") except AssertionError as e: print(f" ERR {e}") ok = False except Exception as e: print(f" ERR Fallback 테스트 오류: {type(e).__name__}: {e}") ok = False print("\n=== 9. 보안 제약 확인 ===") with open("routers/ldap.py", encoding="utf-8") as f: router_full = f.read() security_checks = [ # bind_password가 응답 dict에 직접 포함되지 않음 ("bind_password.*return" not in router_full.replace("\n", " "), "bind_password가 return에 미포함"), # 마스킹 처리 ('***' in router_full or '"***"' in router_full, "비밀번호 마스킹 처리"), # ADMIN 권한 검사 (router_full.count("UserRole.ADMIN") >= 5, "핵심 엔드포인트 ADMIN 권한 검사"), ] for check, desc in security_checks: status = "OK" if check else "WARN" print(f" {status} {desc}") print("\n=== 10. User 모델 LDAP 연동 필드 확인 ===") with open("models.py", encoding="utf-8") as f: models_src = f.read() for sym, desc in [ ("auth_type", "auth_type 컬럼 (LDAP/LOCAL 구분)"), ("display_name", "display_name 컬럼"), ("department", "department 컬럼"), ]: status = "OK" if sym in models_src else "WARN" if status == "ERR": ok = False print(f" {status} {desc}") print("\n=== D-1 LDAP/AD 연동 테스트 완료 ===") if ok: print("모든 검사 통과") else: sys.exit(1)