portal: reject duplicate external emails
This commit is contained in:
parent
315dab839f
commit
632cb9c17b
@ -106,6 +106,30 @@ class KeycloakAdminClient:
|
|||||||
user = users[0]
|
user = users[0]
|
||||||
return user if isinstance(user, dict) else None
|
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]:
|
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='')}"
|
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:
|
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:
|
if msg:
|
||||||
return msg
|
return msg
|
||||||
if isinstance(exc, httpx.HTTPStatusError):
|
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):
|
if isinstance(exc, httpx.TimeoutException):
|
||||||
return "timeout"
|
return "timeout"
|
||||||
return fallback
|
return fallback
|
||||||
@ -181,6 +199,9 @@ def provision_access_request(request_code: str) -> ProvisionResult:
|
|||||||
email = contact_email.strip()
|
email = contact_email.strip()
|
||||||
if not email:
|
if not email:
|
||||||
raise RuntimeError("missing verified email address")
|
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
|
# Always enforce email verification in Keycloak itself (even if the portal
|
||||||
# already verified an external email before approval).
|
# already verified an external email before approval).
|
||||||
required_actions = ["UPDATE_PASSWORD", "VERIFY_EMAIL", "CONFIGURE_TOTP"]
|
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):
|
if admin_client().ready() and admin_client().find_user(username):
|
||||||
return jsonify({"error": "username already exists"}), 409
|
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:
|
try:
|
||||||
with connect() as conn:
|
with connect() as conn:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user