0ae2a6a36d7b4c77e8e7d7df2a1049f4412e7099 max Fri Sep 18 07:03:44 2026 -0700 hgHubConnect: let a mirror hand out its own API keys, and stop swallowing links to another host #Preview2 week - bugs introduced now will need a build patch to fix Two of the three problems QA found on #38323. The link in the mirror-only Hub Upload message did not go anywhere. The tab handler in hgHubConnect.js catches every hgHubConnect link with a hash and turns it into a tab switch, and 'Go to Hub Upload on genome.ucsc.edu' has a hash that names a tab on the mirror too, so the click just reopened the tab the reader was already on. It now only intercepts links to the page itself. The API key section was decoupled from storeUserFiles, but only for display: the Generate and Revoke buttons are cartJson requests, and both the javascript that sends them and the code in main() that routes them were still inside the storeUserFiles gate. A site with showHubApiKey on and hubSpace off therefore drew two dead buttons. The key functions move out of hgMyData.js into a new hubApiKey.js that the Hub Development tab includes on its own, main() routes a cartJson request when either setting is on, and the hubSpace file commands stay registered only when hubSpace is actually running. The request also goes to this host's hgHubConnect now rather than to the login host. Keys live in the central database of the server that issues them, so a key made on genome-euro belongs in genome-euro's table. refs #38323 diff --git src/hg/hgHubConnect/hgHubConnect.c src/hg/hgHubConnect/hgHubConnect.c index 707ce725e11..4609d9a5080 100644 --- src/hg/hgHubConnect/hgHubConnect.c +++ src/hg/hgHubConnect/hgHubConnect.c @@ -432,30 +432,39 @@ // the 'false' below prevents a few hub-search specific jstree configuration options jsInline("hubSearchTree.init(false);"); } errCatchEnd(errCatch); if (errCatch->gotError || errCatch->gotWarning) { printf("hubCheck timed out after running for %d minute%s. Please try on a Unix command line", hubCheckTimeout / 60, hubCheckTimeout/60 > 1 ? "s" : ""); ret = 1; } errCatchFree(&errCatch); return ret; } void printApiKeySection() { +// This section has to stand on its own: a mirror can set showHubApiKey without setting +// storeUserFiles, and then nothing that hgHubConnectOfferUpload pulls in is on the page. +// jsIncludeFile and webIncludeResourceFile only emit a tag the first time, so asking for +// these again when the upload tab is also up costs nothing. +jsIncludeFile("lodash.3.10.0.compat.min.js", NULL); +jsIncludeFile("cart.js", NULL); +jsIncludeFile("hubApiKey.js", NULL); +webIncludeResourceFile("font-awesome.min.css"); // the spinner shown while a key is made + puts("<div id='apiKeySection' class='tabSection'>"); puts("<h4>API key</h4>"); char *userName = wikiLinkUserName(); char *userId = wikiLinkUserId(); if (userName==NULL || userId==NULL) { char *hgsid = cartSessionId(cart); char *loginUrl = wikiLinkUserLoginUrlReturning(hgsid, wikiLinkEncodeReturnUrl(hgsid, "hgHubConnect", "#dev")); printf("<div class='help'>You are not logged in. Please <a href='%s'>Login</a> now, then this page will show the API key.</div>", loginUrl); } else { char *existingKey = hubSpaceGetApiKey(userName); if (existingKey) { @@ -464,33 +473,35 @@ printf("%s\n", existingKey); puts("</div>"); puts("</div>"); puts("<div id='generateDiv' class='help'>Generate an API key <button id='generateApiKey'>Generate key</button></div>"); } else { puts("<div id='generateDiv' class='help'>Generate an API key to <a href='/FAQ/FAQdownloads.html#CAPTCHA'>bypass the CAPTCHA</a> or use the <tt>hubtools up</tt> command"); puts("<button id='generateApiKey'>Generate key</button></div>"); printf("<div id='apiKeyInstructions' style='display: %s'>To use your API key with hubtools, create a file ~/.hubtools.conf and add the key. This is not necessary for URL use to <a href='/FAQ/FAQdownloads.html#CAPTCHA'>bypass the CAPTCHA</a>.<br><br>\n", existingKey != NULL ? "block" : "none"); puts("<div id='apiKey' style='margin-left: 15px; font-family: monospace'>"); puts("</div></div>"); } printf("<div id='revokeDiv' class='help' style='display: %s'>\nTo revoke any API keys associated with your account, click the revoke button: <button id='revokeApiKeys'>Revoke</button>\n</div>", existingKey != NULL ? "block" : "none"); // add the event handlers for clicking the generate/revoke buttons + // note the namespace: a button's id also lands on window, so a bare generateApiKey + // here would be the button element itself rather than the function jsInlineF("" - "document.getElementById('generateApiKey').addEventListener('click', generateApiKey);\n" - "document.getElementById('revokeApiKeys').addEventListener('click', revokeApiKeys);\n" + "document.getElementById('generateApiKey').addEventListener('click', hubApiKey.generate);\n" + "document.getElementById('revokeApiKeys').addEventListener('click', hubApiKey.revoke);\n" ); } puts("</div>"); // tabSection apiKey } void hgHubConnectDeveloperMode() /* Put up the controls for the "Hub Development" Tab, which includes a button to run the * hubCheck utility on a hub and load a hub with the udcTimeout and measureTiming * variables turned on */ { // put out the top of our page char *hubUrl = cartOptionalString(cart, "validateHubUrl"); // the outer div for all the elements in the tab @@ -1767,42 +1778,49 @@ hgHubConnectDeveloperMode(); if (cfgOptionBooleanDefault("storeUserFiles", FALSE)) hgHubConnectOfferUpload(database); printf("</div>"); // #tabs cartWebEnd(); } void doAsync(struct cart *theCart) /* Execute the async request */ { cart = theCart; struct cartJson *cj = cartJsonNew(cart); +// the file commands are hubSpace's, and a site can hand out API keys without running it +if (cfgOptionBooleanDefault("storeUserFiles", FALSE)) + { cartJsonRegisterHandler(cj, hgHubGetHubSpaceUIState, getHubSpaceUIState); cartJsonRegisterHandler(cj, hgHubDeleteFile, doRemoveFile); cartJsonRegisterHandler(cj, hgHubMoveFile, doMoveFile); + } cartJsonRegisterHandler(cj, hgHubGenerateApiKey, cjGenerateApiKey); cartJsonRegisterHandler(cj, hgHubRevokeApiKey, cjRevokeApiKey); cartJsonExecute(cj); } char *excludeVars[] = {"Submit", "submit", "hc_one_url", hgHubDoHubCheck, hgHubCheckUrl, hgHubDoClear, hgHubDoRefresh, hgHubDoDisconnect,hgHubDoRedirect, hgHubDataText, hgHubConnectRemakeTrackHub, NULL}; int main(int argc, char *argv[]) /* Process command line. */ { long enteredMainTime = clock1000(); oldVars = hashNew(10); cgiSpoof(&argc, argv); -if (cfgOptionBooleanDefault("storeUserFiles", FALSE) && cgiOptionalString(CARTJSON_COMMAND)) +// showHubApiKey counts here as well as storeUserFiles: the Generate/Revoke buttons are +// cartJson requests, and they are offered on sites that do not run hubSpace +if ((cfgOptionBooleanDefault("storeUserFiles", FALSE) || + cfgOptionBooleanDefault("showHubApiKey", FALSE)) && cgiOptionalString(CARTJSON_COMMAND)) cartEmptyShellNoContent(doAsync, hUserCookie(), excludeVars, oldVars); else cartEmptyShell(doMiddle, hUserCookie(), excludeVars, oldVars); cgiExitTime("hgHubConnect", enteredMainTime); return 0; }