144 lines
5.6 KiB
Python
144 lines
5.6 KiB
Python
|
|
"""Bounded, non-sending diagnostics rendered only after platform authorization."""
|
||
|
|
|
||
|
|
from concurrent.futures import ThreadPoolExecutor
|
||
|
|
from datetime import datetime, timezone
|
||
|
|
from html import escape
|
||
|
|
import json
|
||
|
|
from urllib.request import Request, build_opener, HTTPRedirectHandler
|
||
|
|
from urllib.error import HTTPError
|
||
|
|
|
||
|
|
|
||
|
|
class NoRedirect(HTTPRedirectHandler):
|
||
|
|
def redirect_request(self, *args, **kwargs):
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def sign_in_status(base):
|
||
|
|
try:
|
||
|
|
response = build_opener(NoRedirect()).open(
|
||
|
|
base.rstrip("/") + "/readyz", timeout=5
|
||
|
|
)
|
||
|
|
except HTTPError as exc:
|
||
|
|
response = exc
|
||
|
|
with response:
|
||
|
|
raw = response.read(16385)
|
||
|
|
if len(raw) > 16384:
|
||
|
|
raise ValueError("oversized status")
|
||
|
|
return json.loads(raw)
|
||
|
|
|
||
|
|
|
||
|
|
def check_services(oidc, delivery):
|
||
|
|
def safe(call):
|
||
|
|
try:
|
||
|
|
return call()
|
||
|
|
except Exception:
|
||
|
|
return {}
|
||
|
|
|
||
|
|
with ThreadPoolExecutor(max_workers=2) as pool:
|
||
|
|
sign = pool.submit(
|
||
|
|
safe, lambda: sign_in_status(oidc.backend_url) if oidc else {}
|
||
|
|
)
|
||
|
|
mail = pool.submit(
|
||
|
|
safe,
|
||
|
|
lambda: delivery.mail_status() if hasattr(delivery, "mail_status") else {},
|
||
|
|
)
|
||
|
|
sign, mail = sign.result(), mail.result()
|
||
|
|
values = {}
|
||
|
|
sign_checks = sign.get("checks", []) if isinstance(sign, dict) else []
|
||
|
|
for row in sign_checks if isinstance(sign_checks, list) else []:
|
||
|
|
if isinstance(row, dict):
|
||
|
|
values[row.get("name")] = row.get("status")
|
||
|
|
checks = [
|
||
|
|
(
|
||
|
|
"Directory sign-in",
|
||
|
|
values.get("lldap"),
|
||
|
|
"Sign-in and identity lookup",
|
||
|
|
"Retry the check; if it still fails, ask the identity service owner to restore directory connectivity.",
|
||
|
|
),
|
||
|
|
(
|
||
|
|
"Primary authentication",
|
||
|
|
values.get("authelia"),
|
||
|
|
"Fresh sign-in",
|
||
|
|
"Retry after the authentication service recovers. Existing sessions do not prove new sign-in works.",
|
||
|
|
),
|
||
|
|
(
|
||
|
|
"Authenticator provider",
|
||
|
|
values.get("privacyidea"),
|
||
|
|
"MFA verification and enrollment",
|
||
|
|
"Retry after the MFA service recovers. Do not disable MFA to work around an outage.",
|
||
|
|
),
|
||
|
|
(
|
||
|
|
"Factor-read credential",
|
||
|
|
values.get("factor_reader"),
|
||
|
|
"Enrollment decisions and MFA verification",
|
||
|
|
"Renewal is automatic. A failure can mean expiry, lost permissions or projection failure; ask the credential owner to run the scoped renewal check.",
|
||
|
|
),
|
||
|
|
]
|
||
|
|
statuses = mail.get("checks", {}) if isinstance(mail, dict) else {}
|
||
|
|
if not isinstance(statuses, dict):
|
||
|
|
statuses = {}
|
||
|
|
checks += [
|
||
|
|
(
|
||
|
|
"Mail record store",
|
||
|
|
statuses.get("database"),
|
||
|
|
"Durable delivery tracking",
|
||
|
|
"Restore the mail store before resuming delivery.",
|
||
|
|
),
|
||
|
|
(
|
||
|
|
"SMTP authentication",
|
||
|
|
statuses.get("smtp_authentication"),
|
||
|
|
"Invitation and verification mail",
|
||
|
|
"Check the configured mail lane. Use assisted password setup when the person cannot receive email.",
|
||
|
|
),
|
||
|
|
]
|
||
|
|
rows = ""
|
||
|
|
for name, status, impact, recovery in checks:
|
||
|
|
label = {
|
||
|
|
"ok": "Check passed",
|
||
|
|
"failed": "Unavailable",
|
||
|
|
"unconfigured": "Not configured",
|
||
|
|
}.get(status, "Unknown — check unavailable")
|
||
|
|
rows += (
|
||
|
|
"<tr><td>"
|
||
|
|
+ escape(name)
|
||
|
|
+ "</td><td>"
|
||
|
|
+ escape(label)
|
||
|
|
+ "</td><td>"
|
||
|
|
+ escape(impact)
|
||
|
|
+ "</td><td>"
|
||
|
|
+ escape(recovery)
|
||
|
|
+ "</td></tr>"
|
||
|
|
)
|
||
|
|
return (
|
||
|
|
"<section><h2>Checked service state</h2><p>Checked at "
|
||
|
|
+ escape(datetime.now(timezone.utc).isoformat(timespec="seconds"))
|
||
|
|
+ ". SMTP authentication may be cached for 30 seconds. These checks send no email and do not prove inbox receipt.</p><table><thead><tr><th>Component</th><th>State</th><th>Affected journeys</th><th>Recovery</th></tr></thead><tbody>"
|
||
|
|
+ rows
|
||
|
|
+ '</tbody></table><p><a href="/platform/operations">Check services again</a></p></section>'
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def selected_mail_status(delivery, event):
|
||
|
|
if event.event_type not in {"family_member.invited", "family_invitation.resent"}:
|
||
|
|
return ""
|
||
|
|
try:
|
||
|
|
receipt = delivery.mail_delivery_status(event.event_id)
|
||
|
|
state = receipt.get("state") if isinstance(receipt, dict) else None
|
||
|
|
except Exception:
|
||
|
|
state = None
|
||
|
|
detail = {
|
||
|
|
"provider_accepted": "The mail provider accepted this event. This does not prove inbox receipt. Retrying this same record reconciles the remaining event delivery without submitting another invitation.",
|
||
|
|
"unknown": "The SMTP outcome is uncertain. Automatic resending is blocked. Ask the mail service owner to reconcile the provider record using this delivery ID; use assisted password setup if access is urgent.",
|
||
|
|
"failed": "The previous mail attempt definitely failed. Review a delivery attempt to retry this same record.",
|
||
|
|
"not_found": "No mail submission is recorded. Review a delivery attempt to submit this selected record.",
|
||
|
|
"unconfigured": "The mail lane is not configured. Use assisted password setup or restore the mail lane.",
|
||
|
|
}.get(
|
||
|
|
state,
|
||
|
|
"Mail evidence is unavailable. Check the mail service and retry this status check; do not infer that a message was sent or received.",
|
||
|
|
)
|
||
|
|
return (
|
||
|
|
"<section><h2>Selected mail evidence</h2><p>"
|
||
|
|
+ escape(detail)
|
||
|
|
+ "</p></section>"
|
||
|
|
)
|