7fc3b3ce76531995f0978618fbb5c5e1fccf31cf braney Sun Sep 20 06:58:05 2026 -0700 hgcentralregress: a central database that tests may write to Every hgcentral a developer can reach is shared. hgcentraltest holds thousands of rows of other people's sandbox sessions, and the production centrals are not reachable from hgwdev at all, so a test that needs to create a session, a login or an api key has had nowhere to put it. Every test written for #38391 so far only reads, and that is the limit this lifts. makeHgCentralRegress.sh creates the database and its tables, copying each schema from the central the caller's hg.conf already names, so a column added to namedSessionDb reaches it on the next run and there is no second copy of the schema in the tree to forget. It is idempotent and meant to run before a test. ONE GRANT IS STILL MISSING and no test uses the database yet. The account the browser uses for the central has global SELECT, INSERT, CREATE and DROP, but UPDATE and DELETE only on databases it has been granted them on individually. So a test can create rows here and cannot take them down: the delete comes back 1142. The script says what to ask for. Measured rather than assumed, and one guess along the way was wrong: the hgcentraltest prefix carries no wildcard grant, hgcentraltestregress behaves the same as any other new name. hubSpaceKeysTester is written against it and is deliberately NOT in the test target, so nothing here goes red while it waits. It covers the rules an api key lives by -- one key per user, a new key revokes the old, an adopted key from a peer mirror replaces both, a revoked key names nobody -- and the signature a peer checks before accepting a sync. refs #38323. refs #38391 diff --git src/hg/lib/tests/hubSpaceKeysTester.c src/hg/lib/tests/hubSpaceKeysTester.c new file mode 100644 index 00000000000..a6369eb8467 --- /dev/null +++ src/hg/lib/tests/hubSpaceKeysTester.c @@ -0,0 +1,122 @@ +/* hubSpaceKeysTester - check how an api key is stored, replaced, adopted and revoked. + * + * An api key lets hubtools upload to hubSpace and lets a script past cloudflare, so the rules + * about it are security rules: one key per user, a new key revokes the old one, and a revoked + * key names nobody. #38323 added the adopt path, hubSpaceSetApiKey, so that a key made on one + * geo mirror works on all of them instead of the user finding their key rejected on whichever + * mirror geography sent them to. + * + * Nothing here is visible. A key that quietly still works after being replaced, or a revoke + * that does not take, looks exactly like a key that behaves, right up until somebody uses the + * old one. + * + * This is the first test in the tree that WRITES to a central database, so it writes to + * hgcentralregress, which exists for tests and which nothing else reads. It creates its own + * rows and removes them at both ends, since the next run must not depend on what this one + * left. See makeHgCentralRegress.sh beside this file. + * + * The keys themselves are random, so nothing prints one. What is printed is the relationship + * between them -- same, different, gone -- which is what the rules are about. + * + * refs #38323 */ + +/* Copyright (C) 2026 The Regents of the University of California + * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */ + +#include "common.h" +#include "hash.h" +#include "jksql.h" +#include "hdb.h" +#include "hgConfig.h" +#include "errCatch.h" +#include "hubSpaceKeys.h" + +/* Recognisable in a stray row, and not a name anyone would log in with. */ +#define TEST_USER "regressTest@example.org" +#define PEER_KEY "a-key-another-mirror-made" + +static void cleanUp() +/* Take this test's rows out again, at both ends of the run. */ +{ +struct sqlConnection *conn = hConnectCentral(); +char query[512]; +sqlSafef(query, sizeof query, "delete from %s where userName = '%s'", + cfgOptionDefault("authTableName", AUTH_TABLE_DEFAULT), TEST_USER); +sqlUpdate(conn, query); +hDisconnectCentral(&conn); +} + +static void refuseNull(char *what, char *userName, char *apiKey, boolean isSet) +/* The calls that must abort rather than write a row with a hole in it. */ +{ +struct errCatch *errCatch = errCatchNew(); +if (errCatchStart(errCatch)) + { + if (isSet) + hubSpaceSetApiKey(userName, apiKey); + else + hubSpaceGenerateApiKey(userName); + } +errCatchEnd(errCatch); +printf(" %-44s %s\n", what, errCatch->gotError ? "aborted" : "went through -- FAIL"); +errCatchFree(&errCatch); +} + +int main(int argc, char *argv[]) +{ +if (!sameString(cfgOption("central.db"), "hgcentralregress")) + errAbort("this test writes rows, so it must be pointed at hgcentralregress, not %s", + cfgOption("central.db")); +cleanUp(); + +printf("a new key\n"); +char *first = cloneString(hubSpaceGenerateApiKey(TEST_USER)); +printf(" %-44s %s\n", "the key comes back for that user", + sameOk(first, hubSpaceGetApiKey(TEST_USER)) ? "yes" : "no -- FAIL"); +printf(" %-44s %s\n", "and the key names the user", + sameOk(TEST_USER, hubSpaceUserNameForApiKey(NULL, first)) ? "yes" : "no -- FAIL"); + +printf("\na second key replaces the first\n"); +char *second = cloneString(hubSpaceGenerateApiKey(TEST_USER)); +printf(" %-44s %s\n", "the two keys differ", + sameString(first, second) ? "no -- FAIL" : "yes"); +printf(" %-44s %s\n", "the old key now names nobody", + hubSpaceUserNameForApiKey(NULL, first) == NULL ? "yes" : "no -- FAIL"); +printf(" %-44s %s\n", "the new key names the user", + sameOk(TEST_USER, hubSpaceUserNameForApiKey(NULL, second)) ? "yes" : "no -- FAIL"); + +printf("\nadopting a key another mirror made\n"); +hubSpaceSetApiKey(TEST_USER, PEER_KEY); +printf(" %-44s %s\n", "the stored key is exactly the one given", + sameOk(PEER_KEY, hubSpaceGetApiKey(TEST_USER)) ? "yes" : "no -- FAIL"); +printf(" %-44s %s\n", "the mirror's own second key is gone", + hubSpaceUserNameForApiKey(NULL, second) == NULL ? "yes" : "no -- FAIL"); + +printf("\nthe signature a peer checks\n"); +printf(" %-44s %s\n", "over the adopted key", + hubSpaceApiKeySyncSig(TEST_USER, PEER_KEY)); +printf(" %-44s %s\n", "over a revoke, which carries no key", + hubSpaceApiKeySyncSig(TEST_USER, "")); +printf(" %-44s %s\n", "a different key signs differently", + sameString(hubSpaceApiKeySyncSig(TEST_USER, PEER_KEY), + hubSpaceApiKeySyncSig(TEST_USER, "another-key")) ? "no -- FAIL" : "yes"); +printf(" %-44s %s\n", "a different user signs differently", + sameString(hubSpaceApiKeySyncSig(TEST_USER, PEER_KEY), + hubSpaceApiKeySyncSig("someoneElse", PEER_KEY)) ? "no -- FAIL" : "yes"); + +printf("\nrevoking\n"); +hubSpaceRevokeApiKey(TEST_USER); +printf(" %-44s %s\n", "the user has no key", + hubSpaceGetApiKey(TEST_USER) == NULL ? "yes" : "no -- FAIL"); +printf(" %-44s %s\n", "the revoked key names nobody", + hubSpaceUserNameForApiKey(NULL, PEER_KEY) == NULL ? "yes" : "no -- FAIL"); + +printf("\nrefused\n"); +refuseNull("generate with no user name", NULL, NULL, FALSE); +refuseNull("adopt with no user name", NULL, PEER_KEY, TRUE); +refuseNull("adopt with no key", TEST_USER, NULL, TRUE); + +cleanUp(); +printf("\n"); +return 0; +}