14ecff0730596f8be147b255afc8acb99e9095e2 max Fri Sep 18 09:06:06 2026 -0700 hgHubConnect: an api key made on one geo mirror now works on all of them, behind syncHubApiKeys #Preview2 week - bugs introduced now will need a build patch to fix #Preview2 week - bugs introduced now will need a build patch to fix Api keys live in the hgcentral of whichever mirror issued them, so a key made on genome.ucsc.edu was rejected on genome-euro and genome-asia, and the botDelay error told the user to go and make a second one. hgHubConnect now tells the other nodes about a key as soon as it generates or revokes one: cjGenerateApiKey and cjRevokeApiKey call syncApiKeyToOtherNodes(), which posts a hgHubSyncApiKey cartJson request to each peer, and cjSyncApiKey() on the far side writes it into that mirror's own table. The peers come from hgcentral.gbNode via the new geoMirrorNotifyOtherNodes(), and the request is signed with login.cookieSalt, which all of a site's mirrors already share - it is what makes the login cookie verifiable on each of them. So neither a list of mirror addresses nor a new shared secret needs provisioning. The notify is best effort: a peer that is down is warn()ed about and skipped, never failing the local generate or revoke, which has already committed by then. All of it is off unless hg.conf says syncHubApiKeys=on. With the gate off the sender returns at once, the receiver refuses the request outright rather than merely being unreachable, and botDelay keeps printing the old server-specific wording, which off is still the truth. Registered in hgConfCatalog.py as a gate, to be flipped once this is released. refs #38323 diff --git src/hg/hgHubConnect/trackHubWizard.c src/hg/hgHubConnect/trackHubWizard.c index 6d444dc0ad5..3fd7a1c776b 100644 --- src/hg/hgHubConnect/trackHubWizard.c +++ src/hg/hgHubConnect/trackHubWizard.c @@ -11,30 +11,31 @@ #include "md5.h" #include "trashDir.h" #include "hgHubConnect.h" #include "jsHelper.h" #include "web.h" #include "wikiLink.h" #include "customTrack.h" #include "userdata.h" #include "jsonWrite.h" #include "cartJson.h" #include "hubSpace.h" #include "hubSpaceKeys.h" #include "hubConnect.h" #include "trackHub.h" #include "htmshell.h" +#include "geoMirror.h" #include #include "errCatch.h" void removeOneFile(char *userName, char *cgiFileName, char *fullPath, char *db, char *fileType) /* Remove one single file for userName */ { // prefixUserFile returns a canonicalized path, or NULL if the // canonicalized path does not begin with the hg.conf specified userDataDir // TODO: make the debug information from stderr go to stdout so the user // can know there is a mistake somewhere, and only print the debug // information in the event that the filename actually begins with the // userDataDir so we don't tell hackers what files do and do not exist char *fileName = prefixUserFile(userName, fullPath, NULL); if (fileName) { @@ -165,67 +166,134 @@ jsonWriteBoolean(jw, "isLoggedIn", getUserName() ? TRUE : FALSE); jsonWriteString(jw, "hubNameDefault", defaultHubNameForUser(getUserName())); // if the user is not logged, the 0 for the quota is ignored jsonWriteNumber(jw, "userQuota", getUserName() ? checkUserQuota(getUserName()) : 0); jsonWriteNumber(jw, "maxQuota", getUserName() ? getMaxUserQuota(getUserName()) : HUB_SPACE_DEFAULT_QUOTA); jsonWriteObjectEnd(jw); } void getHubSpaceUIState(struct cartJson *cj, struct hash *paramHash) /* Get all the data we need to make a users hubSpace UI table. The cartJson library * deals with printing the json */ { outUiDataForUser(cj->jw); } +static void syncApiKeyToOtherNodes(char *userName, char *apiKey) +/* Tell every other geo mirror node about this user's new key (apiKey non-NULL) or that it was + * revoked (apiKey NULL), so a key generated on any UCSC mirror works on all of them. Best + * effort: a peer that is slow or down is logged and skipped, never fails the local action, + * which has already succeeded by the time this is called. */ +{ +if (!cfgOptionBooleanDefault("syncHubApiKeys", FALSE)) + return; +char *apiKeyOrEmpty = apiKey ? apiKey : ""; +char *sig = hubSpaceApiKeySyncSig(userName, apiKeyOrEmpty); +struct jsonWrite *jw = jsonWriteNew(); +jsonWriteObjectStart(jw, NULL); +jsonWriteObjectStart(jw, hgHubSyncApiKey); +jsonWriteString(jw, "userName", userName); +jsonWriteString(jw, "apiKey", apiKeyOrEmpty); +jsonWriteString(jw, "sig", sig); +jsonWriteObjectEnd(jw); +jsonWriteObjectEnd(jw); +struct slPair *cgiVars = slPairNew(CARTJSON_COMMAND, jw->dy->string); +geoMirrorNotifyOtherNodes("hgHubConnect", cgiVars); +slPairFree(&cgiVars); +jsonWriteFree(&jw); +} + void cjRevokeApiKey(struct cartJson *cj, struct hash *paramHash) /* Wrapper for cartJson to call lib function revokeApiKey, removes any api keys for the user */ { struct errCatch *errCatch = errCatchNew(); +char *userName = getUserName(); if (errCatchStart(errCatch)) { - char *userName = getUserName(); hubSpaceRevokeApiKey(userName); } errCatchEnd(errCatch); if (!(errCatch->gotError)) + { jsonWriteString(cj->jw, "revoke", "true"); + syncApiKeyToOtherNodes(userName, NULL); + } else jsonWriteStringf(cj->jw, "error", "revokeApiKey() error: '%s'", errCatch->message->string); errCatchFree(&errCatch); } void cjGenerateApiKey(struct cartJson *cj, struct hash *paramHash) /* Wrapper for cartJson to call lib function generateApiKey, makes a random (but not crypto-secure api key for use of hubtools to upload to hubspace, or for skipping cloudflare */ { struct errCatch *errCatch = errCatchNew(); char *apiKey = NULL; +char *userName = getUserName(); if (errCatchStart(errCatch)) { - char *userName = getUserName(); apiKey = hubSpaceGenerateApiKey(userName); } errCatchEnd(errCatch); if (apiKey) + { jsonWriteString(cj->jw, "apiKey", apiKey); + syncApiKeyToOtherNodes(userName, apiKey); + } else if (errCatch->gotError) jsonWriteStringf(cj->jw, "error", "generateApiKey() error: '%s'", errCatch->message->string); errCatchFree(&errCatch); } +void cjSyncApiKey(struct cartJson *cj, struct hash *paramHash) +/* Adopt an api key (or a revocation) that a peer geo mirror is telling us about, so the key + * works the same on every UCSC mirror. Only ever called by geoMirrorNotifyOtherNodes() on + * another mirror -- never call syncApiKeyToOtherNodes() from in here, or mirrors would keep + * re-notifying each other forever. Rejects the request unless sig proves it was signed with + * this site's login.cookieSalt, which every geo mirror of a site already shares. */ +{ +if (!cfgOptionBooleanDefault("syncHubApiKeys", FALSE)) + { + jsonWriteString(cj->jw, "error", "hgHubSyncApiKey: not enabled on this site"); + return; + } +char *userName = cartJsonRequiredParam(paramHash, "userName", cj->jw, "hgHubSyncApiKey"); +char *apiKey = cartJsonRequiredParam(paramHash, "apiKey", cj->jw, "hgHubSyncApiKey"); +char *sig = cartJsonRequiredParam(paramHash, "sig", cj->jw, "hgHubSyncApiKey"); +if (!userName || !apiKey || !sig) + return; + +struct errCatch *errCatch = errCatchNew(); +if (errCatchStart(errCatch)) + { + char *expectedSig = hubSpaceApiKeySyncSig(userName, apiKey); + if (!sameString(sig, expectedSig)) + errAbort("hgHubSyncApiKey: bad signature"); + if (isNotEmpty(apiKey)) + hubSpaceSetApiKey(userName, apiKey); + else + hubSpaceRevokeApiKey(userName); + } +errCatchEnd(errCatch); +if (!(errCatch->gotError)) + jsonWriteString(cj->jw, "synced", "true"); +else + jsonWriteStringf(cj->jw, "error", "hgHubSyncApiKey error: '%s'", errCatch->message->string); +errCatchFree(&errCatch); +} + void doTrackHubWizard(char *database) /* Offer an upload form so users can upload all their hub files */ { jsIncludeFile("utils.js", NULL); jsIncludeFile("ajax.js", NULL); jsIncludeFile("lodash.3.10.0.compat.min.js", NULL); jsIncludeFile("cart.js", NULL); jsIncludeFile("autocompleteCat.js",NULL); webIncludeResourceFile("font-awesome.min.css"); webIncludeResourceFile("dataTables-2.2.2.min.css"); jsIncludeFile("dataTables-2.2.2.min.js", NULL); webIncludeResourceFile("dataTables.buttons-3.2.2.min.css"); jsIncludeFile("dataTables.buttons-3.2.2.min.js", NULL); webIncludeResourceFile("dataTables.select-3.0.0.min.css");