dotandru commited on
Commit
6daa01f
·
1 Parent(s): 137ff4e

Security Hardening: Firebase Auth + Quota V2 Enforcement

Browse files
Files changed (2) hide show
  1. firebase_manager.py +28 -1
  2. main.py +41 -2
firebase_manager.py CHANGED
@@ -1,7 +1,7 @@
1
  import logging
2
  import os
3
  import firebase_admin
4
- from firebase_admin import credentials, storage
5
 
6
  # Initialize logging
7
  logger = logging.getLogger("BIT-LOG")
@@ -14,6 +14,7 @@ class FirebaseManager:
14
 
15
  _instance = None
16
  _bucket = None
 
17
 
18
  def __new__(cls):
19
  if cls._instance is None:
@@ -56,9 +57,35 @@ class FirebaseManager:
56
  logger.info(f"SUCCESS: [FIREBASE] Initialized successfully for {'PROD' if IS_PRODUCTION else 'DEV'}.")
57
 
58
  self._bucket = storage.bucket()
 
59
  except Exception as e:
60
  logger.error(f"ERROR: [FIREBASE] Initialization failed: {e}")
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  def upload_file(self, local_path: str, destination_blob_name: str) -> str:
63
  """
64
  V273.1: Uploads local file and returns a Signed URL (CORS proof).
 
1
  import logging
2
  import os
3
  import firebase_admin
4
+ from firebase_admin import credentials, storage, firestore, auth
5
 
6
  # Initialize logging
7
  logger = logging.getLogger("BIT-LOG")
 
14
 
15
  _instance = None
16
  _bucket = None
17
+ _db = None
18
 
19
  def __new__(cls):
20
  if cls._instance is None:
 
57
  logger.info(f"SUCCESS: [FIREBASE] Initialized successfully for {'PROD' if IS_PRODUCTION else 'DEV'}.")
58
 
59
  self._bucket = storage.bucket()
60
+ self._db = firestore.client()
61
  except Exception as e:
62
  logger.error(f"ERROR: [FIREBASE] Initialization failed: {e}")
63
 
64
+ def get_db(self):
65
+ """V2: Returns the initialized Firestore client."""
66
+ if not self._db:
67
+ logger.error("❌ [FIREBASE] DB not initialized.")
68
+ return None
69
+ return self._db
70
+
71
+ def verify_token(self, id_token: str):
72
+ """
73
+ V2: Verifies a Firebase Auth ID token.
74
+ Returns the decoded token dictionary (including 'uid') if valid, else None.
75
+ """
76
+ try:
77
+ decoded_token = auth.verify_id_token(id_token)
78
+ return decoded_token
79
+ except auth.ExpiredIdTokenError:
80
+ logger.warning("⚠️ [FIREBASE] Token expired.")
81
+ return None
82
+ except auth.InvalidIdTokenError:
83
+ logger.error("❌ [FIREBASE] Invalid token.")
84
+ return None
85
+ except Exception as e:
86
+ logger.error(f"❌ [FIREBASE] Error verifying token: {e}")
87
+ return None
88
+
89
  def upload_file(self, local_path: str, destination_blob_name: str) -> str:
90
  """
91
  V273.1: Uploads local file and returns a Signed URL (CORS proof).
main.py CHANGED
@@ -21,6 +21,7 @@ class AskQuestionRequest(BaseModel):
21
  context_data: dict | Any
22
  question: str
23
  student_name: str = "תלמיד"
 
24
 
25
  # --- HEALTH CHECK : Top-level Dependency Verification ---
26
  # We do this before standard imports to ensure a clear error message
@@ -340,12 +341,50 @@ async def solve_stream_v2(
340
  @app.post("/explain_step")
341
  async def explain_step(request: Request):
342
  data = await request.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  res = await orchestrator.explain_specific_step(data.get("context"), data.get("step_text"), data.get("student_name"))
344
  return JSONResponse(content=res)
345
 
346
  @app.post("/ask_question")
347
- async def ask_question(request: AskQuestionRequest):
348
- data = request.dict()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  res = await orchestrator.ask_question(data.get("context_data"), data.get("question"), data.get("student_name"))
350
  return JSONResponse(content=res)
351
 
 
21
  context_data: dict | Any
22
  question: str
23
  student_name: str = "תלמיד"
24
+ id_token: Optional[str] = None
25
 
26
  # --- HEALTH CHECK : Top-level Dependency Verification ---
27
  # We do this before standard imports to ensure a clear error message
 
341
  @app.post("/explain_step")
342
  async def explain_step(request: Request):
343
  data = await request.json()
344
+
345
+ # Auth Hardening (V3.1)
346
+ auth_header = request.headers.get('Authorization')
347
+ token = (data.get("id_token") or (auth_header.split('Bearer ')[1] if auth_header and auth_header.startswith('Bearer ') else None))
348
+
349
+ if not token:
350
+ return JSONResponse(status_code=401, content={"error": "Unauthorized: Missing Token"})
351
+
352
+ decoded_token = firebase_manager.verify_token(token)
353
+ if not decoded_token:
354
+ return JSONResponse(status_code=401, content={"error": "Unauthorized: Invalid Token"})
355
+
356
+ uid = decoded_token.get('uid')
357
+
358
+ # Quota check (Gate only, no increment for simple explanations yet)
359
+ is_allowed, msg, _, _ = quota_manager_v2.check_limit(uid)
360
+ if not is_allowed:
361
+ return JSONResponse(status_code=403, content={"error": "QUOTA_EXCEEDED", "message": msg})
362
+
363
  res = await orchestrator.explain_specific_step(data.get("context"), data.get("step_text"), data.get("student_name"))
364
  return JSONResponse(content=res)
365
 
366
  @app.post("/ask_question")
367
+ async def ask_question(request: Request, ask_req: AskQuestionRequest):
368
+ data = ask_req.dict()
369
+
370
+ # Auth Hardening (V3.1)
371
+ auth_header = request.headers.get('Authorization')
372
+ token = (data.get("id_token") or (auth_header.split('Bearer ')[1] if auth_header and auth_header.startswith('Bearer ') else None))
373
+
374
+ if not token:
375
+ return JSONResponse(status_code=401, content={"error": "Unauthorized: Missing Token"})
376
+
377
+ decoded_token = firebase_manager.verify_token(token)
378
+ if not decoded_token:
379
+ return JSONResponse(status_code=401, content={"error": "Unauthorized: Invalid Token"})
380
+
381
+ uid = decoded_token.get('uid')
382
+
383
+ # Quota check
384
+ is_allowed, msg, _, _ = quota_manager_v2.check_limit(uid)
385
+ if not is_allowed:
386
+ return JSONResponse(status_code=403, content={"error": "QUOTA_EXCEEDED", "message": msg})
387
+
388
  res = await orchestrator.ask_question(data.get("context_data"), data.get("question"), data.get("student_name"))
389
  return JSONResponse(content=res)
390