fix(gateway): enforce API key auth on inventory and budget services (#5)

- Added X-API-Key middleware to inventory-service and budget-service
- Services reject all requests without valid API key (401)
- Gateway proxy injects service API keys for inventory and budget
- Dashboard widget fetchers inject API keys
- Generated unique API keys per service, stored in .env
- Added SERVICE_API_KEY env var to docker-compose for both services

Partial fix for #5 — internal services now require auth.
Remaining: document trust model, validate service token semantics.
This commit is contained in:
Yusuf Suleman
2026-03-29 09:06:41 -05:00
parent fb79f15f75
commit fcb9383623
6 changed files with 43 additions and 4 deletions

View File

@@ -23,6 +23,10 @@ SHELFMARK_URL = os.environ.get("SHELFMARK_URL", "http://shelfmark:8084")
SPOTIZERR_URL = os.environ.get("SPOTIZERR_URL", "http://spotizerr-app:7171")
BUDGET_URL = os.environ.get("BUDGET_BACKEND_URL", "http://localhost:3001")
# ── Service API keys (for internal service auth) ──
INVENTORY_SERVICE_API_KEY = os.environ.get("INVENTORY_SERVICE_API_KEY", "")
BUDGET_SERVICE_API_KEY = os.environ.get("BUDGET_SERVICE_API_KEY", "")
# ── Booklore (book library manager) ──
BOOKLORE_URL = os.environ.get("BOOKLORE_URL", "http://booklore:6060")
BOOKLORE_USER = os.environ.get("BOOKLORE_USER", "")

View File

@@ -5,7 +5,7 @@ Platform Gateway — Dashboard, apps, user profile, connections, pinning handler
import json
from datetime import datetime
from config import TRIPS_API_TOKEN, MINIFLUX_API_KEY
from config import TRIPS_API_TOKEN, MINIFLUX_API_KEY, INVENTORY_SERVICE_API_KEY, BUDGET_SERVICE_API_KEY
from database import get_db
from sessions import get_service_token, set_service_token, delete_service_token
import proxy as _proxy_module
@@ -212,12 +212,14 @@ def handle_dashboard(handler, user):
def fetch_inventory(app):
target = app["proxy_target"]
s1, _, b1 = proxy_request(f"{target}/needs-review-count", "GET", {}, timeout=10)
hdrs = {"X-API-Key": INVENTORY_SERVICE_API_KEY} if INVENTORY_SERVICE_API_KEY else {}
s1, _, b1 = proxy_request(f"{target}/needs-review-count", "GET", hdrs, timeout=10)
return json.loads(b1) if s1 == 200 else None
def fetch_budget(app):
try:
s, _, b = proxy_request(f"{app['proxy_target']}/summary", "GET", {}, timeout=8)
hdrs = {"X-API-Key": BUDGET_SERVICE_API_KEY} if BUDGET_SERVICE_API_KEY else {}
s, _, b = proxy_request(f"{app['proxy_target']}/summary", "GET", hdrs, timeout=8)
if s == 200:
data = json.loads(b)
return {"count": data.get("transactionCount", 0), "totalBalance": data.get("totalBalanceDollars", 0), "spending": data.get("spendingDollars", 0), "income": data.get("incomeDollars", 0), "topCategories": data.get("topCategories", [])[:5], "month": data.get("month", "")}

View File

@@ -283,16 +283,21 @@ class GatewayHandler(ResponseMixin, BaseHTTPRequestHandler):
self._send_json({"error": "Unknown service"}, 404)
return
from config import MINIFLUX_API_KEY
from config import MINIFLUX_API_KEY, INVENTORY_SERVICE_API_KEY, BUDGET_SERVICE_API_KEY
headers = {}
ct = self.headers.get("Content-Type")
if ct:
headers["Content-Type"] = ct
# Inject service-level auth
if service_id == "reader" and MINIFLUX_API_KEY:
headers["X-Auth-Token"] = MINIFLUX_API_KEY
elif service_id == "trips" and TRIPS_API_TOKEN:
headers["Authorization"] = f"Bearer {TRIPS_API_TOKEN}"
elif service_id == "inventory" and INVENTORY_SERVICE_API_KEY:
headers["X-API-Key"] = INVENTORY_SERVICE_API_KEY
elif service_id == "budget" and BUDGET_SERVICE_API_KEY:
headers["X-API-Key"] = BUDGET_SERVICE_API_KEY
elif user:
svc_token = get_service_token(user["id"], service_id)
if svc_token: