ibrahimgulbutt commited on
Commit
97e667f
·
1 Parent(s): 327b929

security: require a shared service key on /tts and /extract-resume

Browse files

Both compute endpoints were reachable by anyone who knew the Space URL.
require_service_key checks x-service-key with hmac.compare_digest and fails
closed when SERVICE_API_KEY is unset. /health stays open for platform probes.

SERVICE_API_KEY is set on this Space and the backend already sends it.

Files changed (3) hide show
  1. routers/extraction.py +3 -2
  2. routers/tts.py +3 -2
  3. services/auth.py +27 -0
routers/extraction.py CHANGED
@@ -1,11 +1,12 @@
1
  import io
2
- from fastapi import APIRouter, HTTPException, File, UploadFile
 
3
  from services.pdf_extractor import extract_docx_from_stream, extract_resume_from_stream
4
 
5
  router = APIRouter(prefix="/extract-resume", tags=["extraction"])
6
 
7
 
8
- @router.post("")
9
  async def extract_resume(file: UploadFile = File(...)):
10
  """
11
  Receives an uploaded PDF file stream, extracts and structures its text,
 
1
  import io
2
+ from fastapi import APIRouter, Depends, HTTPException, File, UploadFile
3
+ from services.auth import require_service_key
4
  from services.pdf_extractor import extract_docx_from_stream, extract_resume_from_stream
5
 
6
  router = APIRouter(prefix="/extract-resume", tags=["extraction"])
7
 
8
 
9
+ @router.post("", dependencies=[Depends(require_service_key)])
10
  async def extract_resume(file: UploadFile = File(...)):
11
  """
12
  Receives an uploaded PDF file stream, extracts and structures its text,
routers/tts.py CHANGED
@@ -1,9 +1,10 @@
1
  from typing import Literal
2
 
3
- from fastapi import APIRouter, HTTPException
4
  from fastapi.responses import Response
5
  from pydantic import BaseModel, Field
6
 
 
7
  from services.tts_service import synthesize, is_available, backend_status
8
 
9
  router = APIRouter(prefix="/tts", tags=["tts"])
@@ -23,7 +24,7 @@ def tts_health():
23
  return {"available": is_available(), "backend": backend_status()}
24
 
25
 
26
- @router.post("")
27
  def tts(req: TtsRequest):
28
  """
29
  Synthesize interviewer speech with Kokoro. Returns audio (WAV or Ogg/Opus)
 
1
  from typing import Literal
2
 
3
+ from fastapi import APIRouter, Depends, HTTPException
4
  from fastapi.responses import Response
5
  from pydantic import BaseModel, Field
6
 
7
+ from services.auth import require_service_key
8
  from services.tts_service import synthesize, is_available, backend_status
9
 
10
  router = APIRouter(prefix="/tts", tags=["tts"])
 
24
  return {"available": is_available(), "backend": backend_status()}
25
 
26
 
27
+ @router.post("", dependencies=[Depends(require_service_key)])
28
  def tts(req: TtsRequest):
29
  """
30
  Synthesize interviewer speech with Kokoro. Returns audio (WAV or Ogg/Opus)
services/auth.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import hmac
2
+ import os
3
+
4
+ from fastapi import Header, HTTPException
5
+
6
+
7
+ def require_service_key(x_service_key: str | None = Header(default=None)) -> None:
8
+ """
9
+ Shared-secret gate for the compute endpoints (/extract-resume, /tts).
10
+
11
+ This service is only ever called by the NestJS backend, but on a public host
12
+ it is reachable by anyone — without this, the TTS and extraction compute is
13
+ free for whoever finds the URL.
14
+
15
+ Fails closed: if SERVICE_API_KEY is unset the endpoints refuse traffic
16
+ rather than running open, so a missing env var can't silently expose them.
17
+ Health checks stay unauthenticated so platform probes keep working.
18
+ """
19
+ expected = os.getenv("SERVICE_API_KEY")
20
+ if not expected:
21
+ raise HTTPException(
22
+ status_code=503,
23
+ detail="Service authentication is not configured.",
24
+ )
25
+ # compare_digest avoids leaking the key through response timing.
26
+ if not x_service_key or not hmac.compare_digest(x_service_key, expected):
27
+ raise HTTPException(status_code=401, detail="Invalid or missing service key.")