feat: proxy portal actions to ariadne
This commit is contained in:
parent
e83a189d91
commit
deb3813c2e
73
backend/atlas_portal/ariadne_client.py
Normal file
73
backend/atlas_portal/ariadne_client.py
Normal file
@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from flask import jsonify, request
|
||||
|
||||
from . import settings
|
||||
|
||||
|
||||
class AriadneError(Exception):
|
||||
def __init__(self, message: str, status_code: int = 502) -> None:
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
def enabled() -> bool:
|
||||
return bool(settings.ARIADNE_URL)
|
||||
|
||||
|
||||
def _auth_headers() -> dict[str, str]:
|
||||
header = request.headers.get("Authorization", "").strip()
|
||||
return {"Authorization": header} if header else {}
|
||||
|
||||
|
||||
def _url(path: str) -> str:
|
||||
base = settings.ARIADNE_URL.rstrip("/")
|
||||
suffix = path.lstrip("/")
|
||||
return f"{base}/{suffix}" if suffix else base
|
||||
|
||||
|
||||
def request_raw(
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
payload: Any | None = None,
|
||||
params: dict[str, Any] | None = None,
|
||||
) -> httpx.Response:
|
||||
if not enabled():
|
||||
raise AriadneError("ariadne not configured", 503)
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=settings.ARIADNE_TIMEOUT_SEC) as client:
|
||||
return client.request(
|
||||
method,
|
||||
_url(path),
|
||||
headers=_auth_headers(),
|
||||
json=payload,
|
||||
params=params,
|
||||
)
|
||||
except httpx.RequestError as exc:
|
||||
raise AriadneError("ariadne unavailable", 502) from exc
|
||||
|
||||
|
||||
def proxy(
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
payload: Any | None = None,
|
||||
params: dict[str, Any] | None = None,
|
||||
) -> tuple[Any, int]:
|
||||
try:
|
||||
resp = request_raw(method, path, payload=payload, params=params)
|
||||
except AriadneError as exc:
|
||||
return jsonify({"error": str(exc)}), exc.status_code
|
||||
|
||||
try:
|
||||
data = resp.json()
|
||||
except ValueError:
|
||||
detail = resp.text.strip()
|
||||
data = {"error": detail or "upstream error"}
|
||||
|
||||
return jsonify(data), resp.status_code
|
||||
@ -51,6 +51,10 @@ def ensure_schema() -> None:
|
||||
conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS email_verification_token_hash TEXT")
|
||||
conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS email_verification_sent_at TIMESTAMPTZ")
|
||||
conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS email_verified_at TIMESTAMPTZ")
|
||||
conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS welcome_email_sent_at TIMESTAMPTZ")
|
||||
conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS approval_flags TEXT[]")
|
||||
conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS approval_note TEXT")
|
||||
conn.execute("ALTER TABLE access_requests ADD COLUMN IF NOT EXISTS denial_note TEXT")
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS access_request_tasks (
|
||||
|
||||
@ -13,6 +13,7 @@ from flask import jsonify, request, g
|
||||
|
||||
import psycopg
|
||||
|
||||
from .. import ariadne_client
|
||||
from ..db import connect, configured
|
||||
from ..keycloak import admin_client, require_auth
|
||||
from ..mailer import MailerError, access_request_verification_body, send_text_email
|
||||
@ -541,7 +542,7 @@ def register(app) -> None:
|
||||
if not row:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
current_status = _normalize_status(row.get("status") or "")
|
||||
if current_status == "accounts_building":
|
||||
if current_status == "accounts_building" and not ariadne_client.enabled():
|
||||
try:
|
||||
provision_access_request(code)
|
||||
except Exception:
|
||||
|
||||
@ -9,6 +9,7 @@ import httpx
|
||||
from flask import jsonify, g, request
|
||||
|
||||
from .. import settings
|
||||
from .. import ariadne_client
|
||||
from ..db import connect
|
||||
from ..keycloak import admin_client, require_auth, require_account_access
|
||||
from ..nextcloud_mail_sync import trigger as trigger_nextcloud_mail_sync
|
||||
@ -343,6 +344,8 @@ def register(app) -> None:
|
||||
ok, resp = require_account_access()
|
||||
if not ok:
|
||||
return resp
|
||||
if ariadne_client.enabled():
|
||||
return ariadne_client.proxy("POST", "/api/account/mailu/rotate")
|
||||
if not admin_client().ready():
|
||||
return jsonify({"error": "server not configured"}), 503
|
||||
|
||||
@ -394,6 +397,8 @@ def register(app) -> None:
|
||||
ok, resp = require_account_access()
|
||||
if not ok:
|
||||
return resp
|
||||
if ariadne_client.enabled():
|
||||
return ariadne_client.proxy("POST", "/api/account/wger/reset")
|
||||
if not admin_client().ready():
|
||||
return jsonify({"error": "server not configured"}), 503
|
||||
|
||||
@ -445,6 +450,8 @@ def register(app) -> None:
|
||||
ok, resp = require_account_access()
|
||||
if not ok:
|
||||
return resp
|
||||
if ariadne_client.enabled():
|
||||
return ariadne_client.proxy("POST", "/api/account/firefly/reset")
|
||||
if not admin_client().ready():
|
||||
return jsonify({"error": "server not configured"}), 503
|
||||
|
||||
@ -496,6 +503,9 @@ def register(app) -> None:
|
||||
ok, resp = require_account_access()
|
||||
if not ok:
|
||||
return resp
|
||||
if ariadne_client.enabled():
|
||||
payload = request.get_json(silent=True) or {}
|
||||
return ariadne_client.proxy("POST", "/api/account/nextcloud/mail/sync", payload=payload)
|
||||
if not admin_client().ready():
|
||||
return jsonify({"error": "server not configured"}), 503
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import quote
|
||||
|
||||
from flask import jsonify, g
|
||||
from flask import jsonify, g, request
|
||||
|
||||
from .. import ariadne_client
|
||||
from ..db import connect, configured
|
||||
from ..keycloak import require_auth, require_portal_admin
|
||||
from ..provisioning import provision_access_request
|
||||
@ -18,6 +20,8 @@ def register(app) -> None:
|
||||
return resp
|
||||
if not configured():
|
||||
return jsonify({"error": "server not configured"}), 503
|
||||
if ariadne_client.enabled():
|
||||
return ariadne_client.proxy("GET", "/api/admin/access/requests")
|
||||
|
||||
try:
|
||||
with connect() as conn:
|
||||
@ -55,18 +59,34 @@ def register(app) -> None:
|
||||
return resp
|
||||
if not configured():
|
||||
return jsonify({"error": "server not configured"}), 503
|
||||
payload = request.get_json(silent=True) or {}
|
||||
if ariadne_client.enabled():
|
||||
return ariadne_client.proxy(
|
||||
"POST",
|
||||
f"/api/admin/access/requests/{quote(username, safe='')}/approve",
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
decided_by = getattr(g, "keycloak_username", "") or ""
|
||||
flags_raw = payload.get("flags") if isinstance(payload, dict) else None
|
||||
flags = [f for f in flags_raw if isinstance(f, str)] if isinstance(flags_raw, list) else []
|
||||
note = payload.get("note") if isinstance(payload, dict) else None
|
||||
note = str(note).strip() if isinstance(note, str) else None
|
||||
try:
|
||||
with connect() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
UPDATE access_requests
|
||||
SET status = 'accounts_building', decided_at = NOW(), decided_by = %s
|
||||
SET status = 'accounts_building',
|
||||
decided_at = NOW(),
|
||||
decided_by = %s,
|
||||
approval_flags = %s,
|
||||
approval_note = %s
|
||||
WHERE username = %s AND status = 'pending'
|
||||
AND email_verified_at IS NOT NULL
|
||||
RETURNING request_code
|
||||
""",
|
||||
(decided_by or None, username),
|
||||
(decided_by or None, flags or None, note, username),
|
||||
).fetchone()
|
||||
except Exception:
|
||||
return jsonify({"error": "failed to approve request"}), 502
|
||||
@ -90,18 +110,30 @@ def register(app) -> None:
|
||||
return resp
|
||||
if not configured():
|
||||
return jsonify({"error": "server not configured"}), 503
|
||||
payload = request.get_json(silent=True) or {}
|
||||
if ariadne_client.enabled():
|
||||
return ariadne_client.proxy(
|
||||
"POST",
|
||||
f"/api/admin/access/requests/{quote(username, safe='')}/deny",
|
||||
payload=payload,
|
||||
)
|
||||
|
||||
decided_by = getattr(g, "keycloak_username", "") or ""
|
||||
note = payload.get("note") if isinstance(payload, dict) else None
|
||||
note = str(note).strip() if isinstance(note, str) else None
|
||||
try:
|
||||
with connect() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
UPDATE access_requests
|
||||
SET status = 'denied', decided_at = NOW(), decided_by = %s
|
||||
SET status = 'denied',
|
||||
decided_at = NOW(),
|
||||
decided_by = %s,
|
||||
denial_note = %s
|
||||
WHERE username = %s AND status = 'pending'
|
||||
RETURNING request_code
|
||||
""",
|
||||
(decided_by or None, username),
|
||||
(decided_by or None, note, username),
|
||||
).fetchone()
|
||||
except Exception:
|
||||
return jsonify({"error": "failed to deny request"}), 502
|
||||
|
||||
@ -48,6 +48,9 @@ KEYCLOAK_ADMIN_CLIENT_ID = os.getenv("KEYCLOAK_ADMIN_CLIENT_ID", "")
|
||||
KEYCLOAK_ADMIN_CLIENT_SECRET = os.getenv("KEYCLOAK_ADMIN_CLIENT_SECRET", "")
|
||||
KEYCLOAK_ADMIN_REALM = os.getenv("KEYCLOAK_ADMIN_REALM", KEYCLOAK_REALM)
|
||||
|
||||
ARIADNE_URL = os.getenv("ARIADNE_URL", "").strip()
|
||||
ARIADNE_TIMEOUT_SEC = float(os.getenv("ARIADNE_TIMEOUT_SEC", "10"))
|
||||
|
||||
ACCOUNT_ALLOWED_GROUPS = [
|
||||
g.strip()
|
||||
for g in os.getenv("ACCOUNT_ALLOWED_GROUPS", "dev,admin").split(",")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user