Let edge nodes read /systems with their own api_key
The node builds its OP25 config from GET /systems, but that router only accepted a Firebase token or the shared service key — a node holds neither. Every fetch returned 401 and the node fell back to its stale offline cache, so a system edited in the UI never reached the field. Confirmed on node-002 against the live server: "Failed to fetch systems from C2: 401 Unauthorized ... Offline cache will be used." The node sends no node_id with the request, only the bearer token, so the key is matched by querying node_keys for the value instead of fetching a known document the way /upload does. Read access only: the mutating routes in this router each carry their own require_admin_token, so widening the router-level gate doesn't let a node create, edit or delete a system. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
55cd1110df
commit
a195563da6
@@ -37,6 +37,41 @@ async def require_service_or_firebase_token(
|
||||
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
||||
|
||||
|
||||
async def require_node_service_or_firebase_token(
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Security(_bearer),
|
||||
) -> dict:
|
||||
"""Accept a node's own API key in addition to a service key / Firebase token.
|
||||
|
||||
Edge nodes need to read ``/systems`` to build their OP25 config, but they
|
||||
hold neither a Firebase token nor the shared service key — only the
|
||||
per-node api_key that ``/upload`` already trusts. Without this they got a
|
||||
flat 401 and silently fell back to their stale offline cache, so a system
|
||||
edited in the UI never reached the node.
|
||||
|
||||
Unlike ``/upload``, the node sends no node_id alongside the bearer token,
|
||||
so the key is matched by querying ``node_keys`` for the value rather than
|
||||
fetching a known document. Mutating routes are unaffected: they carry
|
||||
their own ``require_admin_token`` dependency, so widening the router-level
|
||||
gate grants nodes read access only.
|
||||
"""
|
||||
if not credentials:
|
||||
raise HTTPException(status_code=401, detail="Missing authorization token")
|
||||
token = credentials.credentials
|
||||
if settings.service_key and secrets.compare_digest(token, settings.service_key):
|
||||
return {"service": True}
|
||||
try:
|
||||
return firebase_auth.verify_id_token(token)
|
||||
except Exception:
|
||||
pass
|
||||
# Deferred import: app.internal.firestore initialises firebase-admin at
|
||||
# import time, and auth.py is imported from module scope in the routers.
|
||||
from app.internal import firestore as fstore
|
||||
matches = await fstore.collection_list("node_keys", api_key=token)
|
||||
if matches:
|
||||
return {"node": True, "node_id": matches[0].get("node_id")}
|
||||
raise HTTPException(status_code=401, detail="Invalid or expired token")
|
||||
|
||||
|
||||
def get_role(decoded: dict) -> str:
|
||||
"""Extract the effective role from a decoded Firebase token.
|
||||
|
||||
|
||||
+10
-2
@@ -9,7 +9,11 @@ from app.internal.summarizer import summarizer_loop
|
||||
from app.internal.vocabulary_learner import vocabulary_induction_loop
|
||||
from app.internal.recorrelation_sweep import recorrelation_loop
|
||||
from app.config import settings
|
||||
from app.internal.auth import require_firebase_token, require_service_or_firebase_token
|
||||
from app.internal.auth import (
|
||||
require_firebase_token,
|
||||
require_service_or_firebase_token,
|
||||
require_node_service_or_firebase_token,
|
||||
)
|
||||
from app.routers import nodes, systems, calls, upload, tokens, incidents, alerts, admin, trips, places, links, users
|
||||
from app.routers import enrollment
|
||||
from app.internal import dynsec
|
||||
@@ -81,7 +85,11 @@ app.add_middleware(
|
||||
)
|
||||
|
||||
app.include_router(nodes.router, dependencies=[Depends(require_service_or_firebase_token)])
|
||||
app.include_router(systems.router, dependencies=[Depends(require_service_or_firebase_token)])
|
||||
# systems is the one router edge nodes read directly (system_cacher.py builds
|
||||
# the OP25 config from it), so its gate also accepts a per-node api_key. The
|
||||
# write routes inside carry their own require_admin_token, so nodes get read
|
||||
# access only.
|
||||
app.include_router(systems.router, dependencies=[Depends(require_node_service_or_firebase_token)])
|
||||
app.include_router(calls.router, dependencies=[Depends(require_service_or_firebase_token)])
|
||||
app.include_router(tokens.router, dependencies=[Depends(require_service_or_firebase_token)])
|
||||
app.include_router(incidents.router, dependencies=[Depends(require_service_or_firebase_token)])
|
||||
|
||||
Reference in New Issue
Block a user