feat: Gmail API via service account delegation + Bearer token auth
- Add SA key in Secret Manager (gmail-sa-key) - Fix require_iap middleware to accept Bearer tokens - Fix require_auth decorator to accept Bearer tokens - Implement _notify_email with service_account.Credentials + domain-wide delegation - Set GMAIL_DEFAULT_SENDER and GMAIL_ALLOWED_SENDERS env vars
This commit is contained in:
parent
bc0ee3b3ee
commit
e31cd7635a
101
main.py
101
main.py
|
|
@ -93,9 +93,23 @@ async def require_iap(request: Request, call_next):
|
|||
return await call_next(request)
|
||||
if any(path.startswith(p) for p in IAP_EXEMPT_PREFIXES):
|
||||
return await call_next(request)
|
||||
if not request.headers.get("x-goog-authenticated-user-email"):
|
||||
return Response(status_code=401, content="Unauthorized")
|
||||
return await call_next(request)
|
||||
if request.headers.get("x-goog-authenticated-user-email"):
|
||||
return await call_next(request)
|
||||
# Fallback: aksepter Cloud Run identity token (Bearer)
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:]
|
||||
try:
|
||||
from google.oauth2 import id_token
|
||||
from google.auth.transport import requests as grequests
|
||||
id_token.verify_oauth2_token(
|
||||
token, grequests.Request(),
|
||||
audience="https://osvauco-agent-zjbqp3prqq-uc.a.run.app"
|
||||
)
|
||||
return await call_next(request)
|
||||
except Exception as e:
|
||||
print(f"IAP middleware Bearer validation failed: {e}", flush=True)
|
||||
return Response(status_code=401, content="Unauthorized")
|
||||
|
||||
# ── FIREBASE & FIRESTORE INIT ───────────────────────────────────────────────
|
||||
db = None
|
||||
|
|
@ -152,27 +166,33 @@ ALERT_EMAIL = os.environ.get("ALERT_EMAIL")
|
|||
|
||||
|
||||
def require_auth(func):
|
||||
from functools import wraps
|
||||
@wraps(func)
|
||||
async def wrapper(request: Request, *args, **kwargs):
|
||||
# 1. Session-basert auth (eksisterende)
|
||||
user = request.session.get('user')
|
||||
if not user:
|
||||
return JSONResponse(status_code=401, content={"error": "Not authenticated"})
|
||||
if ALLOWED_EMAILS and user.get('email') not in ALLOWED_EMAILS:
|
||||
return JSONResponse(status_code=403, content={"error": "Email not allowed"})
|
||||
return await func(request, *args, **kwargs)
|
||||
if user:
|
||||
return await func(request, *args, **kwargs)
|
||||
|
||||
# 2. Bearer token (Cloud Run identity token)
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if auth_header.startswith('Bearer '):
|
||||
token = auth_header[7:]
|
||||
try:
|
||||
from google.oauth2 import id_token
|
||||
from google.auth.transport import requests as grequests
|
||||
idinfo = id_token.verify_oauth2_token(
|
||||
token, grequests.Request(),
|
||||
audience="https://osvauco-agent-zjbqp3prqq-uc.a.run.app"
|
||||
)
|
||||
request.session['user'] = {'email': idinfo.get('email', 'service-account')}
|
||||
return await func(request, *args, **kwargs)
|
||||
except Exception as e:
|
||||
print(f"Bearer token validation failed: {e}")
|
||||
|
||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||
return wrapper
|
||||
|
||||
|
||||
# ── STATIC FILES ──────────────────────────────────────────────────────────────
|
||||
_static_dir = pathlib.Path(__file__).parent / "static"
|
||||
if _static_dir.is_dir():
|
||||
app.mount("/static", StaticFiles(directory=str(_static_dir), html=True), name="static")
|
||||
|
||||
|
||||
# ── DOCS PROXY ─────────────────────────────────────────────────────────────────
|
||||
_GH_REPO = "vauco-saas/OSVauco"
|
||||
_GH_BRANCH = os.environ.get("DOCS_BRANCH", "main")
|
||||
|
||||
@app.get("/docs/{path:path}")
|
||||
async def proxy_doc(path: str):
|
||||
token = os.environ.get("GITHUB_PAT")
|
||||
|
|
@ -562,10 +582,45 @@ async def _notify_webhook(request: Request, req: WebhookNotifyRequest):
|
|||
app.add_api_route("/notify/webhook", endpoint=require_auth(_notify_webhook), methods=["POST"])
|
||||
|
||||
async def _notify_email(request: Request, req: EmailNotifyRequest):
|
||||
result = await _notifier.send_email(req)
|
||||
if result["status"] == "not_configured":
|
||||
return JSONResponse(status_code=503, content=result)
|
||||
return JSONResponse(status_code=200 if result["status"]=="ok" else 500, content=result)
|
||||
import base64, json, os
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from googleapiclient.discovery import build
|
||||
from google.oauth2 import service_account
|
||||
from google.cloud import secretmanager
|
||||
|
||||
sender = os.environ.get("GMAIL_DEFAULT_SENDER", "chris.christiansen@vauco.no")
|
||||
project = os.environ.get("GOOGLE_CLOUD_PROJECT", "propane-will-491900-m5")
|
||||
|
||||
try:
|
||||
sm = secretmanager.SecretManagerServiceClient()
|
||||
secret = sm.access_secret_version(
|
||||
name=f"projects/{project}/secrets/gmail-sa-key/versions/latest"
|
||||
)
|
||||
sa_info = json.loads(secret.payload.data.decode())
|
||||
creds = service_account.Credentials.from_service_account_info(
|
||||
sa_info,
|
||||
scopes=["https://www.googleapis.com/auth/gmail.send"],
|
||||
subject=sender
|
||||
)
|
||||
except Exception as e:
|
||||
return JSONResponse(status_code=503, content={"status": "error", "reason": f"Gmail auth failed: {e}"})
|
||||
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = req.subject or "OSVauco varsel"
|
||||
msg["From"] = sender
|
||||
msg["To"] = req.to
|
||||
body = f"<h2>{req.subject}</h2><p>Event: {req.event}</p>"
|
||||
msg.attach(MIMEText(body, "html"))
|
||||
raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()
|
||||
|
||||
try:
|
||||
service = build("gmail", "v1", credentials=creds, cache_discovery=False)
|
||||
service.users().messages().send(userId="me", body={"raw": raw}).execute()
|
||||
return JSONResponse({"status": "ok", "to": req.to, "subject": req.subject})
|
||||
except Exception as e:
|
||||
return JSONResponse(status_code=500, content={"status": "error", "detail": str(e)})
|
||||
|
||||
app.add_api_route("/notify/email", endpoint=require_auth(_notify_email), methods=["POST"])
|
||||
|
||||
async def _notify_sms(request: Request, req: SmsNotifyRequest):
|
||||
|
|
|
|||
39
test_pusher.py
Normal file
39
test_pusher.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import sys
|
||||
import importlib.util
|
||||
import time
|
||||
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"opax_mcp_client",
|
||||
"/home/chris_christiansen/OSVauco/agents/core-logic/opax_mcp_client.py"
|
||||
)
|
||||
opax_mcp_client = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(opax_mcp_client)
|
||||
|
||||
print("--- Running push_file test ---")
|
||||
content = '''# Ping
|
||||
Verifisert write-path: opax-mcp → Gitea → Cloud Build
|
||||
Tidspunkt: 2026-07-02 00:00 CEST'''
|
||||
push_params = {
|
||||
'path': 'test/ping-2026-07-02.md',
|
||||
'content': content,
|
||||
'message': 'test: verify end-to-end write path'
|
||||
}
|
||||
|
||||
push_result = opax_mcp_client.call_tool("push_file", push_params)
|
||||
print("--- push_file Result ---")
|
||||
print(push_result)
|
||||
|
||||
print()
|
||||
print("--- Waiting 5 seconds for build to trigger ---")
|
||||
time.sleep(5)
|
||||
|
||||
print()
|
||||
print("--- Checking Cloud Build status ---")
|
||||
build_status = opax_mcp_client.get_build_status()
|
||||
print("--- get_build_status Result ---")
|
||||
print(build_status)
|
||||
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
sys.exit(1)
|
||||
Loading…
Reference in New Issue
Block a user