portal: reject duplicate external emails
This commit is contained in:
parent
315dab839f
commit
632cb9c17b
@ -106,6 +106,30 @@ class KeycloakAdminClient:
|
||||
user = users[0]
|
||||
return user if isinstance(user, dict) else None
|
||||
|
||||
def find_user_by_email(self, email: str) -> dict[str, Any] | None:
|
||||
email = (email or "").strip()
|
||||
if not email:
|
||||
return None
|
||||
|
||||
url = f"{settings.KEYCLOAK_ADMIN_URL}/admin/realms/{settings.KEYCLOAK_REALM}/users"
|
||||
# Match the portal's username query behavior: set a low `max` and post-filter for exact matches.
|
||||
params = {"email": email, "exact": "true", "max": "2"}
|
||||
email_norm = email.lower()
|
||||
|
||||
with httpx.Client(timeout=settings.HTTP_CHECK_TIMEOUT_SEC) as client:
|
||||
resp = client.get(url, params=params, headers=self._headers())
|
||||
resp.raise_for_status()
|
||||
users = resp.json()
|
||||
if not isinstance(users, list) or not users:
|
||||
return None
|
||||
for user in users:
|
||||
if not isinstance(user, dict):
|
||||
continue
|
||||
candidate = user.get("email")
|
||||
if isinstance(candidate, str) and candidate.strip().lower() == email_norm:
|
||||
return user
|
||||
return None
|
||||
|
||||
def get_user(self, user_id: str) -> dict[str, Any]:
|
||||
url = f"{settings.KEYCLOAK_ADMIN_URL}/admin/realms/{settings.KEYCLOAK_REALM}/users/{quote(user_id, safe='')}"
|
||||
with httpx.Client(timeout=settings.HTTP_CHECK_TIMEOUT_SEC) as client:
|
||||
|
||||
@ -71,7 +71,25 @@ def _safe_error_detail(exc: Exception, fallback: str) -> str:
|
||||
if msg:
|
||||
return msg
|
||||
if isinstance(exc, httpx.HTTPStatusError):
|
||||
return f"http {exc.response.status_code}"
|
||||
detail = f"http {exc.response.status_code}"
|
||||
try:
|
||||
payload = exc.response.json()
|
||||
msg: str | None = None
|
||||
if isinstance(payload, dict):
|
||||
raw = payload.get("errorMessage") or payload.get("error") or payload.get("message")
|
||||
if isinstance(raw, str) and raw.strip():
|
||||
msg = raw.strip()
|
||||
elif isinstance(payload, str) and payload.strip():
|
||||
msg = payload.strip()
|
||||
if msg:
|
||||
msg = " ".join(msg.split())
|
||||
detail = f"{detail}: {msg[:200]}"
|
||||
except Exception:
|
||||
text = (exc.response.text or "").strip()
|
||||
if text:
|
||||
text = " ".join(text.split())
|
||||
detail = f"{detail}: {text[:200]}"
|
||||
return detail
|
||||
if isinstance(exc, httpx.TimeoutException):
|
||||
return "timeout"
|
||||
return fallback
|
||||
@ -181,6 +199,9 @@ def provision_access_request(request_code: str) -> ProvisionResult:
|
||||
email = contact_email.strip()
|
||||
if not email:
|
||||
raise RuntimeError("missing verified email address")
|
||||
existing_email_user = admin_client().find_user_by_email(email)
|
||||
if existing_email_user and (existing_email_user.get("username") or "") != username:
|
||||
raise RuntimeError("email is already associated with an existing Atlas account")
|
||||
# Always enforce email verification in Keycloak itself (even if the portal
|
||||
# already verified an external email before approval).
|
||||
required_actions = ["UPDATE_PASSWORD", "VERIFY_EMAIL", "CONFIGURE_TOTP"]
|
||||
|
||||
@ -236,6 +236,8 @@ def register(app) -> None:
|
||||
|
||||
if admin_client().ready() and admin_client().find_user(username):
|
||||
return jsonify({"error": "username already exists"}), 409
|
||||
if admin_client().ready() and admin_client().find_user_by_email(email):
|
||||
return jsonify({"error": "email is already associated with an existing Atlas account"}), 409
|
||||
|
||||
try:
|
||||
with connect() as conn:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user