feat: Gmail API via service account delegation + Bearer token auth
Some checks failed
Check Python Version Consistency / Check Python Version (push) Has been cancelled
Nightly GDrive Backup / Backup OSVauco → Google Drive (push) Has been cancelled

- 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:
Chris Christiansen 2026-07-02 02:17:47 +00:00
parent bc0ee3b3ee
commit e31cd7635a
2 changed files with 117 additions and 23 deletions

97
main.py
View File

@ -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")
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"})
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
View 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)