Compare commits
2
Commits
1b6682eee5
...
7eb57acdbe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7eb57acdbe | ||
|
|
ea14038355 |
@@ -80,6 +80,9 @@ TEST_MODE_ENV = "GRANTHI_LINK_ALLOW_TEST_MODE"
|
||||
|
||||
# Sentinel: create_user hit a 409 (someone else created the login first).
|
||||
USER_CREATE_CONFLICT = object()
|
||||
# Sentinel: the address already belongs to another forge account, which is a
|
||||
# 409 the caller can act on -- not a 502 that reads like the service is down.
|
||||
EMAIL_IN_USE = object()
|
||||
|
||||
LOGIN_SAFE = re.compile(r"[^a-zA-Z0-9._-]+")
|
||||
|
||||
@@ -292,6 +295,14 @@ def check_config_perms(path, euid=None):
|
||||
# Identity map: zitadel sub -> gitea login (JSON, 0600, atomic writes)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def _email_in_use_message(login, userinfo):
|
||||
email = userinfo.get("email") or "your address"
|
||||
return (f"cannot create the account '{login}': {email} already belongs to "
|
||||
f"a different account on this forge. If that other account is "
|
||||
f"yours, an operator must bind your identity to it; if it is not, "
|
||||
f"use a different address.")
|
||||
|
||||
|
||||
class IdentityStore:
|
||||
"""Persistent map of Zitadel `sub` -> Gitea login binding records.
|
||||
|
||||
@@ -677,6 +688,14 @@ class LinkService:
|
||||
headers=self._admin_hdr(), body=body)
|
||||
if status == 409:
|
||||
return USER_CREATE_CONFLICT
|
||||
if status == 422 and "e-mail already in use" in str(resp).lower():
|
||||
# The address belongs to a DIFFERENT forge account. Reported as its
|
||||
# own case because the generic path turns it into a bare 502, and
|
||||
# "502" sends the person looking for an outage when the real answer
|
||||
# is "that address is already somebody's account here". Hit live on
|
||||
# 2026-08-23: an operator moved an email onto another account and
|
||||
# the next sign-in failed with nothing but the number.
|
||||
return EMAIL_IN_USE
|
||||
if status != 201:
|
||||
return f"gitea admin user create failed (HTTP {status}): {resp}"
|
||||
return None
|
||||
@@ -733,6 +752,8 @@ class LinkService:
|
||||
"was not created by this service; refusing "
|
||||
"to re-create"}, None
|
||||
err = self.create_user(login, userinfo)
|
||||
if err is EMAIL_IN_USE:
|
||||
return 409, {"error": _email_in_use_message(login, userinfo)}, None
|
||||
if err and err is not USER_CREATE_CONFLICT:
|
||||
return 502, {"error": err}, None
|
||||
LOG.info("re-created service-managed gitea user %s", login)
|
||||
@@ -758,6 +779,8 @@ class LinkService:
|
||||
return 409, {"error": "login exists and is not linked "
|
||||
"to this identity"}, None
|
||||
LOG.info("user %s created concurrently; continuing", login)
|
||||
elif err is EMAIL_IN_USE:
|
||||
return 409, {"error": _email_in_use_message(login, userinfo)}, None
|
||||
elif err:
|
||||
return 502, {"error": err}, None
|
||||
else:
|
||||
|
||||
@@ -77,6 +77,10 @@ class StubUpstream(BaseHTTPRequestHandler):
|
||||
if self.path == "/api/v1/admin/users":
|
||||
if body["username"] in st["users"]:
|
||||
return self._json(409, {"message": "user already exists"})
|
||||
if body.get("email") in st["users"].values():
|
||||
# real Gitea: emails are unique across accounts
|
||||
return self._json(422, {"message":
|
||||
f"e-mail already in use [email: {body['email']}]"})
|
||||
st["users"][body["username"]] = body["email"]
|
||||
st["created"].append(body)
|
||||
return self._json(201, {"login": body["username"]})
|
||||
@@ -1001,6 +1005,34 @@ class TestInviteSurvivesAFailedGrant(ServiceTestBase):
|
||||
self.assertEqual([g["repo"] for g in self.svc.state.peek_invites("[email protected]")],
|
||||
["alice/reports"])
|
||||
|
||||
|
||||
class TestDuplicateEmailIsExplained(ServiceTestBase):
|
||||
"""A 502 sends someone looking for an outage. The real answer is that the
|
||||
address already belongs to another account here -- say so. (Hit live on
|
||||
2026-08-23 when an operator moved an email onto a different account.)"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.enable_test_mode()
|
||||
# an existing account already holds the address
|
||||
StubUpstream.state["users"]["existing"] = "[email protected]"
|
||||
|
||||
def test_it_is_a_409_that_names_the_problem(self):
|
||||
status, resp = self.stub_link("s-new", "brandnew", email="[email protected]",
|
||||
verified=True, device_id="dev-x")
|
||||
self.assertEqual(status, 409, resp)
|
||||
msg = resp["error"]
|
||||
self.assertIn("[email protected]", msg)
|
||||
self.assertIn("already belongs to a different account", msg)
|
||||
self.assertIn("brandnew", msg) # names the login it tried
|
||||
self.assertNotIn("502", msg)
|
||||
|
||||
def test_a_normal_create_is_unaffected(self):
|
||||
status, resp = self.stub_link("s-ok", "fresh", email="[email protected]",
|
||||
verified=True, device_id="dev-y")
|
||||
self.assertEqual(status, 200, resp)
|
||||
self.assertIn("token", resp)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user