755541f8af03c4956b3fe167dddc9f27d8ad9b62
hiram
  Wed Jul 22 15:34:31 2026 -0700
improve the login/logout function refs #31811

diff --git src/hg/hubApi/liftOver.c src/hg/hubApi/liftOver.c
index 8d1bde4027e..848c6dd9bbe 100644
--- src/hg/hubApi/liftOver.c
+++ src/hg/hubApi/liftOver.c
@@ -161,68 +161,139 @@
                 {
                 jsonWriteBoolean(jw, "pending", TRUE);
                 jsonWriteNumber(jw, "pendingStatus", sqlSigned(row[1]));
                 jsonWriteString(jw, "pendingRequestTime", row[2]);
                 }
             sqlFreeResult(&sr);
             }
         hDisconnectOtto(&ottoConn);
         }
     }
 
 apiFinishOutput(0, NULL, jw);
 hDisconnectCentral(&conn);
 }
 
+static boolean inUcscEduDomain()
+/* Return TRUE if this host is anywhere under the ucsc.edu domain. */
+{
+char *httpHost = hHttpHost();
+return (httpHost != NULL && endsWith(httpHost, ".ucsc.edu"));
+}
+
+static boolean onGenomeRRMachine()
+/* Return TRUE if running on one of the genome.ucsc.edu round-robin
+ * machines (hgw0, hgw1, hgw2, ...), which carry SQL grants on
+ * hgcentral.gbMembers.  Only meaningful within inUcscEduDomain(). */
+{
+if (hIsPrivateHost())	// stay on localhost for hgwdev and hgwbeta
+    return TRUE;
+char *httpHost = hHttpHost();
+if (httpHost == NULL || !startsWith("hgw", httpHost))
+    return FALSE;
+char afterHgw = httpHost[3];
+return (afterHgw >= '0' && afterHgw <= '9');
+}
+
+static boolean fetchGbMembersFromCentral(char *userName, char **retEmail, char **retRealName)
+/* Relay to genome.ucsc.edu's own loginStatus endpoint to get email/realName
+ * for userName, forwarding this request's Cookie header so genome.ucsc.edu
+ * authenticates the same session.  Used on ucsc.edu hosts that lack SQL
+ * grants on hgcentral.gbMembers.  Returns FALSE on any failure. */
+{
+char *cookieHeader = getenv("HTTP_COOKIE");
+if (isEmpty(cookieHeader))
+    return FALSE;
+
+struct dyString *reqHeader = dyStringNew(0);
+dyStringPrintf(reqHeader, "Cookie: %s\r\n", cookieHeader);
+char *url = "https://genome.ucsc.edu/cgi-bin/hubApi/liftOver/loginStatus";
+int sd = netOpenHttpExt(url, "GET", reqHeader->string);
+dyStringFree(&reqHeader);
+if (sd < 0)
+    return FALSE;
+
+char *redirectedUrl = NULL;
+if (!netSkipHttpHeaderLinesWithRedirect(sd, url, &redirectedUrl))
+    {
+    close(sd);
+    return FALSE;
+    }
+
+struct dyString *body = netSlurpFile(sd);
+close(sd);
+struct jsonElement *json = jsonParse(body->string);
+dyStringFree(&body);
+if (json == NULL)
+    return FALSE;
+
+char *email = jsonStringField(json, "email");
+char *realName = jsonStringField(json, "realName");
+*retEmail = cloneString(email ? email : "");
+*retRealName = cloneString(realName ? realName : "");
+return TRUE;
+}
+
 static void loginStatus()
 /* output current user login status as JSON */
 {
 /* wikiLinkUserName() handles all cookie validation internally */
 char *userName = wikiLinkUserName();
 struct jsonWrite *jw = apiStartOutput();
 char hgLoginLink[2048];
 boolean privateHost = hIsPrivateHost();
 /* can not use hgcentral hglogin from hgwdev/genome-test */
 if (privateHost)
     safef(hgLoginLink, sizeof(hgLoginLink), "%shgLogin", hLocalHostCgiBinUrl());
 else
     safef(hgLoginLink, sizeof(hgLoginLink), "%shgLogin", hLoginHostCgiBinUrl());
 
 if (userName != NULL)
     {
     // Get both email and realName from gbMembers table
+    char *email = NULL;
+    char *realName = NULL;
+
+    if (inUcscEduDomain() && !onGenomeRRMachine())
+        {
+        // dev sandboxes, hgwbeta, etc: no local grants on gbMembers,
+        // relay to genome.ucsc.edu instead
+        fetchGbMembersFromCentral(userName, &email, &realName);
+        }
+    else
+        {
+        // RR machines, and anything entirely outside ucsc.edu: unchanged
         struct sqlConnection *sc = NULL;
         if (privateHost)
 	    sc = hConnectCentral();
         else
 	    sc = hConnectOtto();
         struct dyString *query = sqlDyStringCreate("select email, realName from gbMembers where userName = '%s'", userName);
         struct sqlResult *sr = sqlGetResult(sc, dyStringCannibalize(&query));
         char **row = sqlNextRow(sr);
 
-    char *email = NULL;
-    char *realName = NULL;
         if (row != NULL)
             {
             email = cloneString(row[0] ? row[0] : "");
             realName = cloneString(row[1] ? row[1] : "");
             }
         sqlFreeResult(&sr);
         if (privateHost)
 	    hDisconnectCentral(&sc);
         else
 	    hDisconnectOtto(&sc);
+        }
 
     // Build logout URL with returnto parameter
     char *returnTo = cgiOptionalString("returnTo");
     struct dyString *logoutUrl = dyStringNew(0);
     dyStringPrintf(logoutUrl, "%s?hgLogin.do.displayLogout=1", hgLoginLink);
     if (isNotEmpty(returnTo))
         {
         char *encodedReturnUrl = cgiEncodeFull(returnTo);
 
         dyStringPrintf(logoutUrl, "&returnto=%s", encodedReturnUrl);
         freeMem(encodedReturnUrl);
         }
 
     jsonWriteString(jw, "userName", userName);
     jsonWriteString(jw, "email", email ? email : "");