API Key 인증 완전 제거
사내 운영 전제로 X-API-Key 인증을 전면 제거. - app/core/auth.py 삭제 - routes.py에서 Depends(require_api_key) 모두 제거 - config.Settings에서 api_keys / api_key_set 제거 - .env / .env.example에서 API_KEYS 제거 - docker-compose.yml에서 API_KEYS 환경변수 제거 (KOSIMCSE_MODEL 추가) - UI(index.html)에서 DEFAULT_API_KEY 상수 + X-API-Key 헤더 모두 제거 - scripts/sample_curl.sh, sample_python.py에서 키 헤더 제거 - tests: test_detect_requires_api_key → test_detect_no_auth_required로 갱신 - README: 인증 컬럼 제거, curl 예시에서 헤더 제거main
parent
f31ef142e8
commit
4913bf3ecc
|
|
@ -4,7 +4,6 @@ PORT=8000
|
|||
LOG_LEVEL=info
|
||||
RELOAD=false
|
||||
|
||||
API_KEYS=combooks-key-change-me,baikal-key-change-me
|
||||
ENGINE_VERSION=o2o-plagiarism-2.1.0-kosimcse
|
||||
REFERENCE_CORPUS_DIR=./data/reference
|
||||
TAXONOMY_DIR=./data/taxonomy
|
||||
|
|
|
|||
34
README.md
34
README.md
|
|
@ -74,7 +74,6 @@ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
|||
# 탐지
|
||||
curl -X POST http://localhost:8000/v1/plagiarism/detect \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: combooks-key-change-me" \
|
||||
-d '{
|
||||
"doc_id": "test-001",
|
||||
"text": "검사할 본문 텍스트…",
|
||||
|
|
@ -84,17 +83,15 @@ curl -X POST http://localhost:8000/v1/plagiarism/detect \
|
|||
# 자서전 업로드
|
||||
curl -X POST http://localhost:8000/v1/corpus \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: combooks-key-change-me" \
|
||||
-d '{"title": "김OO 자서전", "text": "본문…"}'
|
||||
|
||||
# .txt 파일 업로드
|
||||
curl -X POST http://localhost:8000/v1/corpus/file \
|
||||
-H "X-API-Key: combooks-key-change-me" \
|
||||
-F "title=김OO 자서전" \
|
||||
-F "file=@autobiography.txt"
|
||||
|
||||
# 코퍼스 목록
|
||||
curl -H "X-API-Key: combooks-key-change-me" http://localhost:8000/v1/corpus
|
||||
curl http://localhost:8000/v1/corpus
|
||||
|
||||
# 분류체계 조회 (10종 태그 + 39 케이스)
|
||||
curl http://localhost:8000/v1/taxonomy
|
||||
|
|
@ -102,21 +99,19 @@ curl http://localhost:8000/v1/taxonomy
|
|||
|
||||
## 엔드포인트 요약
|
||||
|
||||
| Method | Path | 용도 | 인증 |
|
||||
|---|---|---|---|
|
||||
| GET | `/` | 검토 콘솔 (HTML) | - |
|
||||
| GET | `/docs` | Swagger UI | - |
|
||||
| GET | `/v1/health` | 헬스체크 + 엔진 정보 | - |
|
||||
| GET | `/v1/taxonomy` | 분류체계 조회 | - |
|
||||
| POST | `/v1/plagiarism/detect` | 단건 동기 탐지 | ✅ |
|
||||
| POST | `/v1/plagiarism/batch` | 배치 잡 등록 (≤500건) | ✅ |
|
||||
| GET | `/v1/plagiarism/batch/{job_id}` | 배치 결과 조회 | ✅ |
|
||||
| GET | `/v1/corpus` | 코퍼스 목록 | ✅ |
|
||||
| POST | `/v1/corpus` | JSON 업로드 | ✅ |
|
||||
| POST | `/v1/corpus/file` | .txt 파일 업로드 | ✅ |
|
||||
| DELETE | `/v1/corpus/{doc_id}` | 삭제 | ✅ |
|
||||
|
||||
인증: `X-API-Key` 헤더에 발급된 키 (기관별 분리 발급 가능, 콤마 구분).
|
||||
| Method | Path | 용도 |
|
||||
|---|---|---|
|
||||
| GET | `/` | 검토 콘솔 (HTML) |
|
||||
| GET | `/docs` | Swagger UI |
|
||||
| GET | `/v1/health` | 헬스체크 + 엔진 정보 |
|
||||
| GET | `/v1/taxonomy` | 분류체계 조회 |
|
||||
| POST | `/v1/plagiarism/detect` | 단건 동기 탐지 |
|
||||
| POST | `/v1/plagiarism/batch` | 배치 잡 등록 (≤500건) |
|
||||
| GET | `/v1/plagiarism/batch/{job_id}` | 배치 결과 조회 |
|
||||
| GET | `/v1/corpus` | 코퍼스 목록 |
|
||||
| POST | `/v1/corpus` | JSON 업로드 |
|
||||
| POST | `/v1/corpus/file` | .txt 파일 업로드 |
|
||||
| DELETE | `/v1/corpus/{doc_id}` | 삭제 |
|
||||
|
||||
## 응답 스키마 (탐지 결과)
|
||||
|
||||
|
|
@ -159,7 +154,6 @@ curl http://localhost:8000/v1/taxonomy
|
|||
|
||||
| 변수 | 기본 | 설명 |
|
||||
|---|---|---|
|
||||
| `API_KEYS` | `combooks-key-…,baikal-key-…` | 콤마 구분 다중 키 |
|
||||
| `SIMILARITY_THRESHOLD` | `0.85` | 침해 판정 임계값 (정밀도 우선) |
|
||||
| `AUTOBIOGRAPHY_MODE` | `true` | 자서전 특화 전처리 (공통표현+NER) |
|
||||
| `USE_LSH_FILTER` | `true` | MinHash+LSH 1차 필터 |
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, Request, UploadFile, status
|
||||
from fastapi import APIRouter, BackgroundTasks, File, Form, HTTPException, Request, UploadFile, status
|
||||
|
||||
from app.api.schemas import (
|
||||
BatchCreatedResponse,
|
||||
|
|
@ -17,7 +17,6 @@ from app.api.schemas import (
|
|||
HealthResponse,
|
||||
TaxonomyResponse,
|
||||
)
|
||||
from app.core.auth import require_api_key
|
||||
from app.core.config import get_settings
|
||||
from app.engine.corpus import add_document, delete_document, list_documents
|
||||
from app.engine.detector import PlagiarismDetector
|
||||
|
|
@ -79,7 +78,6 @@ async def taxonomy(request: Request) -> TaxonomyResponse:
|
|||
"/plagiarism/detect",
|
||||
response_model=DetectResponse,
|
||||
tags=["plagiarism"],
|
||||
dependencies=[Depends(require_api_key)],
|
||||
)
|
||||
async def detect(req: DetectRequest, request: Request) -> DetectResponse:
|
||||
return _detector(request).detect_request(req)
|
||||
|
|
@ -90,7 +88,6 @@ async def detect(req: DetectRequest, request: Request) -> DetectResponse:
|
|||
response_model=BatchCreatedResponse,
|
||||
status_code=status.HTTP_202_ACCEPTED,
|
||||
tags=["plagiarism"],
|
||||
dependencies=[Depends(require_api_key)],
|
||||
)
|
||||
async def batch_create(
|
||||
req: BatchRequest,
|
||||
|
|
@ -113,7 +110,6 @@ async def batch_create(
|
|||
"/plagiarism/batch/{job_id}",
|
||||
response_model=BatchStatusResponse,
|
||||
tags=["plagiarism"],
|
||||
dependencies=[Depends(require_api_key)],
|
||||
)
|
||||
async def batch_status(job_id: str, request: Request) -> BatchStatusResponse:
|
||||
job = _job_store(request).get(job_id)
|
||||
|
|
@ -142,7 +138,6 @@ def _rebuild(request: Request) -> int:
|
|||
"/corpus",
|
||||
response_model=CorpusListResponse,
|
||||
tags=["corpus"],
|
||||
dependencies=[Depends(require_api_key)],
|
||||
)
|
||||
async def corpus_list(request: Request) -> CorpusListResponse:
|
||||
settings = get_settings()
|
||||
|
|
@ -158,7 +153,6 @@ async def corpus_list(request: Request) -> CorpusListResponse:
|
|||
response_model=CorpusUploadResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
tags=["corpus"],
|
||||
dependencies=[Depends(require_api_key)],
|
||||
)
|
||||
async def corpus_upload_json(req: CorpusUploadRequest, request: Request) -> CorpusUploadResponse:
|
||||
"""JSON으로 자서전 1건 업로드. 인덱스 자동 재빌드."""
|
||||
|
|
@ -183,7 +177,6 @@ async def corpus_upload_json(req: CorpusUploadRequest, request: Request) -> Corp
|
|||
response_model=CorpusUploadResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
tags=["corpus"],
|
||||
dependencies=[Depends(require_api_key)],
|
||||
)
|
||||
async def corpus_upload_file(
|
||||
request: Request,
|
||||
|
|
@ -218,7 +211,6 @@ async def corpus_upload_file(
|
|||
"/corpus/{doc_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
tags=["corpus"],
|
||||
dependencies=[Depends(require_api_key)],
|
||||
)
|
||||
async def corpus_delete(doc_id: str, request: Request) -> None:
|
||||
settings = get_settings()
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
from fastapi import Header, HTTPException, status
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
async def require_api_key(x_api_key: str | None = Header(default=None)) -> str:
|
||||
settings = get_settings()
|
||||
if not x_api_key or x_api_key not in settings.api_key_set:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid or missing X-API-Key header",
|
||||
)
|
||||
return x_api_key
|
||||
|
|
@ -13,7 +13,6 @@ class Settings(BaseSettings):
|
|||
log_level: str = "info" # debug / info / warning / error
|
||||
reload: bool = False # 개발용 자동 재시작
|
||||
|
||||
api_keys: str = "combooks-key-change-me,baikal-key-change-me"
|
||||
engine_version: str = "o2o-plagiarism-2.0.0-pdf-v1.2"
|
||||
reference_corpus_dir: str = "./data/reference"
|
||||
taxonomy_dir: str = "./data/taxonomy"
|
||||
|
|
@ -49,10 +48,6 @@ class Settings(BaseSettings):
|
|||
autobiography_mode: bool = True
|
||||
enable_entity_masking: bool = True
|
||||
|
||||
@property
|
||||
def api_key_set(self) -> set[str]:
|
||||
return {k.strip() for k in self.api_keys.split(",") if k.strip()}
|
||||
|
||||
@property
|
||||
def corpus_path(self) -> Path:
|
||||
return Path(self.reference_corpus_dir).resolve()
|
||||
|
|
|
|||
|
|
@ -358,9 +358,6 @@
|
|||
</footer>
|
||||
|
||||
<script>
|
||||
// 검토 콘솔 내부 호출에 자동 첨부할 기본 키 (사내 운영용)
|
||||
const DEFAULT_API_KEY = "combooks-key-change-me";
|
||||
|
||||
const SAMPLES = {
|
||||
copy: {
|
||||
title: "어미만 변경한 표절 (홍길동전 기반)",
|
||||
|
|
@ -423,7 +420,6 @@ async function runDetect() {
|
|||
const docId = document.getElementById("doc-id").value.trim() || `web-${Date.now()}`;
|
||||
const title = document.getElementById("title").value.trim();
|
||||
const author = document.getElementById("author").value.trim();
|
||||
const apiKey = DEFAULT_API_KEY;
|
||||
|
||||
const thresholdVal = parseFloat(document.getElementById("threshold-slider").value);
|
||||
const autobioMode = document.querySelector('input[name="autobio-mode"]:checked').value;
|
||||
|
|
@ -451,7 +447,7 @@ async function runDetect() {
|
|||
try {
|
||||
const resp = await fetch("/v1/plagiarism/detect", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
|
|
@ -614,11 +610,10 @@ document.querySelectorAll(".tab-btn").forEach((btn) => {
|
|||
|
||||
// ========== 코퍼스 관리 ==========
|
||||
async function loadCorpus() {
|
||||
const apiKey = DEFAULT_API_KEY;
|
||||
const tbody = document.getElementById("corpus-tbody");
|
||||
tbody.innerHTML = '<tr><td colspan="4" style="color: var(--muted); text-align: center; padding: 20px;">로딩 중…</td></tr>';
|
||||
try {
|
||||
const resp = await fetch("/v1/corpus", { headers: { "X-API-Key": apiKey } });
|
||||
const resp = await fetch("/v1/corpus");
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
document.getElementById("corpus-count").textContent = data.total;
|
||||
|
|
@ -647,7 +642,6 @@ async function uploadCorpus() {
|
|||
const title = document.getElementById("corpus-title").value.trim();
|
||||
const text = document.getElementById("corpus-text").value.trim();
|
||||
const file = document.getElementById("corpus-file").files[0];
|
||||
const apiKey = DEFAULT_API_KEY;
|
||||
const statusEl = document.getElementById("corpus-status");
|
||||
|
||||
if (!title) {
|
||||
|
|
@ -670,11 +664,11 @@ async function uploadCorpus() {
|
|||
fd.append("title", title);
|
||||
if (docId) fd.append("doc_id", docId);
|
||||
fd.append("file", file);
|
||||
resp = await fetch("/v1/corpus/file", { method: "POST", headers: { "X-API-Key": apiKey }, body: fd });
|
||||
resp = await fetch("/v1/corpus/file", { method: "POST", body: fd });
|
||||
} else {
|
||||
resp = await fetch("/v1/corpus", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ doc_id: docId || null, title, text }),
|
||||
});
|
||||
}
|
||||
|
|
@ -700,11 +694,9 @@ async function uploadCorpus() {
|
|||
|
||||
async function deleteDoc(docId) {
|
||||
if (!confirm(`'${docId}' 문서를 삭제하시겠습니까? 인덱스가 재빌드됩니다.`)) return;
|
||||
const apiKey = DEFAULT_API_KEY;
|
||||
try {
|
||||
const resp = await fetch(`/v1/corpus/${encodeURIComponent(docId)}`, {
|
||||
method: "DELETE",
|
||||
headers: { "X-API-Key": apiKey },
|
||||
});
|
||||
if (!resp.ok && resp.status !== 204) {
|
||||
throw new Error(`HTTP ${resp.status}`);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ services:
|
|||
REFERENCE_CORPUS_DIR: /app/data/reference
|
||||
TAXONOMY_DIR: /app/data/taxonomy
|
||||
AUTOBIOGRAPHY_PATTERNS_PATH: /app/data/autobiography/common_patterns.txt
|
||||
KOSIMCSE_MODEL: ${KOSIMCSE_MODEL:-BM-K/KoSimCSE-roberta-multitask}
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
set -euo pipefail
|
||||
|
||||
API_HOST="${API_HOST:-http://localhost:8000}"
|
||||
API_KEY="${API_KEY:-combooks-key-change-me}"
|
||||
|
||||
echo "--- 1) Health ---"
|
||||
curl -sS "${API_HOST}/v1/health" | python3 -m json.tool
|
||||
|
|
@ -12,7 +11,6 @@ echo
|
|||
echo "--- 2) 단건 탐지: 어린왕자 유사 텍스트 ---"
|
||||
curl -sS -X POST "${API_HOST}/v1/plagiarism/detect" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" \
|
||||
-d '{
|
||||
"doc_id": "test-001",
|
||||
"text": "어린왕자는 작은 별에서 온 소년이다. 그는 별을 떠나 여러 행성을 여행하며 다양한 어른들을 만난다. 마침내 지구에 도착해 여우를 만나고 길들임의 의미를 배운다.",
|
||||
|
|
@ -23,7 +21,6 @@ echo
|
|||
echo "--- 3) 배치 등록 ---"
|
||||
JOB_RESPONSE=$(curl -sS -X POST "${API_HOST}/v1/plagiarism/batch" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: ${API_KEY}" \
|
||||
-d '{
|
||||
"items": [
|
||||
{"doc_id": "b-001", "text": "앤 셜리는 상상력이 풍부한 고아 소녀로 초록 지붕 집에 입양된다."},
|
||||
|
|
@ -36,5 +33,4 @@ JOB_ID=$(echo "${JOB_RESPONSE}" | python3 -c 'import json,sys; print(json.load(s
|
|||
echo
|
||||
echo "--- 4) 배치 결과 조회 ---"
|
||||
sleep 1
|
||||
curl -sS "${API_HOST}/v1/plagiarism/batch/${JOB_ID}" \
|
||||
-H "X-API-Key: ${API_KEY}" | python3 -m json.tool
|
||||
curl -sS "${API_HOST}/v1/plagiarism/batch/${JOB_ID}" | python3 -m json.tool
|
||||
|
|
|
|||
|
|
@ -10,14 +10,13 @@ import time
|
|||
import urllib.request
|
||||
|
||||
API_HOST = os.environ.get("API_HOST", "http://localhost:8000")
|
||||
API_KEY = os.environ.get("API_KEY", "combooks-key-change-me")
|
||||
|
||||
|
||||
def _post(path: str, body: dict) -> dict:
|
||||
req = urllib.request.Request(
|
||||
f"{API_HOST}{path}",
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json", "X-API-Key": API_KEY},
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
|
|
@ -25,7 +24,7 @@ def _post(path: str, body: dict) -> dict:
|
|||
|
||||
|
||||
def _get(path: str) -> dict:
|
||||
req = urllib.request.Request(f"{API_HOST}{path}", headers={"X-API-Key": API_KEY})
|
||||
req = urllib.request.Request(f"{API_HOST}{path}")
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ from fastapi.testclient import TestClient
|
|||
|
||||
from app.main import app
|
||||
|
||||
API_KEY = "combooks-key-change-me"
|
||||
|
||||
|
||||
def test_health():
|
||||
with TestClient(app) as client:
|
||||
|
|
@ -17,13 +15,14 @@ def test_health():
|
|||
assert body["corpus_size"] >= 0
|
||||
|
||||
|
||||
def test_detect_requires_api_key():
|
||||
def test_detect_no_auth_required():
|
||||
"""인증 제거 - 키 없이도 200 응답."""
|
||||
with TestClient(app) as client:
|
||||
resp = client.post(
|
||||
"/v1/plagiarism/detect",
|
||||
json={"doc_id": "x", "text": "테스트 본문"},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
def test_detect_returns_schema():
|
||||
|
|
@ -34,7 +33,6 @@ def test_detect_returns_schema():
|
|||
"doc_id": "t-1",
|
||||
"text": "어린왕자는 작은 별에서 온 소년이다. 그는 여우를 만나 길들임을 배운다.",
|
||||
},
|
||||
headers={"X-API-Key": API_KEY},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
|
|
@ -53,12 +51,8 @@ def test_batch_flow():
|
|||
{"doc_id": "b-1", "text": "앤 셜리는 초록 지붕 집에 입양된 소녀다."},
|
||||
]
|
||||
},
|
||||
headers={"X-API-Key": API_KEY},
|
||||
)
|
||||
assert resp.status_code == 202
|
||||
job_id = resp.json()["job_id"]
|
||||
status = client.get(
|
||||
f"/v1/plagiarism/batch/{job_id}",
|
||||
headers={"X-API-Key": API_KEY},
|
||||
)
|
||||
status = client.get(f"/v1/plagiarism/batch/{job_id}")
|
||||
assert status.status_code == 200
|
||||
|
|
|
|||
Loading…
Reference in New Issue