Skip to content

Commit f9432cc

Browse files
Copilotbbockelm
andauthored
Add keycache load, metadata, and delete APIs (#194)
* Add keycache load, metadata, and delete APIs with tests * Replace magic number with named constant for next_update offset --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Brian P Bockelman <bockelman@gmail.com>
1 parent d14923a commit f9432cc

File tree

5 files changed

+514
-2
lines changed

5 files changed

+514
-2
lines changed

src/scitokens.cpp

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,80 @@ int keycache_stop_background_refresh(char **err_msg) {
11341134
return keycache_set_background_refresh(0, err_msg);
11351135
}
11361136

1137+
int keycache_load_jwks(const char *issuer, char **jwks, char **err_msg) {
1138+
if (!issuer) {
1139+
if (err_msg) {
1140+
*err_msg = strdup("Issuer may not be a null pointer");
1141+
}
1142+
return -1;
1143+
}
1144+
if (!jwks) {
1145+
if (err_msg) {
1146+
*err_msg = strdup("JWKS output pointer may not be null.");
1147+
}
1148+
return -1;
1149+
}
1150+
try {
1151+
*jwks = strdup(scitokens::Validator::load_jwks(issuer).c_str());
1152+
} catch (std::exception &exc) {
1153+
if (err_msg) {
1154+
*err_msg = strdup(exc.what());
1155+
}
1156+
return -1;
1157+
}
1158+
return 0;
1159+
}
1160+
1161+
int keycache_get_jwks_metadata(const char *issuer, char **metadata,
1162+
char **err_msg) {
1163+
if (!issuer) {
1164+
if (err_msg) {
1165+
*err_msg = strdup("Issuer may not be a null pointer");
1166+
}
1167+
return -1;
1168+
}
1169+
if (!metadata) {
1170+
if (err_msg) {
1171+
*err_msg = strdup("Metadata output pointer may not be null.");
1172+
}
1173+
return -1;
1174+
}
1175+
try {
1176+
*metadata =
1177+
strdup(scitokens::Validator::get_jwks_metadata(issuer).c_str());
1178+
} catch (std::exception &exc) {
1179+
if (err_msg) {
1180+
*err_msg = strdup(exc.what());
1181+
}
1182+
return -1;
1183+
}
1184+
return 0;
1185+
}
1186+
1187+
int keycache_delete_jwks(const char *issuer, char **err_msg) {
1188+
if (!issuer) {
1189+
if (err_msg) {
1190+
*err_msg = strdup("Issuer may not be a null pointer");
1191+
}
1192+
return -1;
1193+
}
1194+
try {
1195+
if (!scitokens::Validator::delete_jwks(issuer)) {
1196+
if (err_msg) {
1197+
*err_msg =
1198+
strdup("Failed to delete JWKS cache entry for issuer.");
1199+
}
1200+
return -1;
1201+
}
1202+
} catch (std::exception &exc) {
1203+
if (err_msg) {
1204+
*err_msg = strdup(exc.what());
1205+
}
1206+
return -1;
1207+
}
1208+
return 0;
1209+
}
1210+
11371211
int config_set_int(const char *key, int value, char **err_msg) {
11381212
return scitoken_config_set_int(key, value, err_msg);
11391213
}

src/scitokens.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,36 @@ int keycache_set_background_refresh(int enabled, char **err_msg);
309309
*/
310310
int keycache_stop_background_refresh(char **err_msg);
311311

312+
/**
313+
* Load the JWKS from the keycache for a given issuer, refreshing only if
314+
* needed.
315+
* - Returns 0 if successful, nonzero on failure.
316+
* - If the existing JWKS has not expired, this will return the cached JWKS
317+
* without triggering a download.
318+
* - If the JWKS has expired or does not exist, this will attempt to refresh
319+
* it from the issuer.
320+
* - `jwks` is an output variable set to the contents of the JWKS.
321+
*/
322+
int keycache_load_jwks(const char *issuer, char **jwks, char **err_msg);
323+
324+
/**
325+
* Get metadata for a cached JWKS entry.
326+
* - Returns 0 if successful, nonzero on failure.
327+
* - `metadata` is an output variable set to a JSON string containing:
328+
* - "expires": expiration time (Unix epoch seconds)
329+
* - "next_update": next update time (Unix epoch seconds)
330+
* - If the issuer does not exist in the cache, returns an error.
331+
*/
332+
int keycache_get_jwks_metadata(const char *issuer, char **metadata,
333+
char **err_msg);
334+
335+
/**
336+
* Delete a JWKS entry from the keycache.
337+
* - Returns 0 if successful, nonzero on failure.
338+
* - If the issuer does not exist in the cache, this is not considered an error.
339+
*/
340+
int keycache_delete_jwks(const char *issuer, char **err_msg);
341+
312342
/**
313343
* APIs for managing scitokens configuration parameters.
314344
*/

src/scitokens_cache.cpp

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ namespace {
2222
// This handles concurrent access from multiple threads/processes
2323
constexpr int SQLITE_BUSY_TIMEOUT_MS = 5000;
2424

25+
// Default time before expiry when next_update should occur (4 hours)
26+
constexpr int64_t DEFAULT_NEXT_UPDATE_OFFSET_S = 4 * 3600;
27+
2528
void initialize_cachedb(const std::string &keycache_file) {
2629

2730
sqlite3 *db;
@@ -257,7 +260,7 @@ bool scitokens::Validator::get_public_keys_from_db(const std::string issuer,
257260
sqlite3_close(db);
258261
iter = top_obj.find("next_update");
259262
if (iter == top_obj.end() || !iter->second.is<int64_t>()) {
260-
next_update = expiry - 4 * 3600;
263+
next_update = expiry - DEFAULT_NEXT_UPDATE_OFFSET_S;
261264
} else {
262265
next_update = iter->second.get<int64_t>();
263266
}
@@ -406,7 +409,7 @@ scitokens::Validator::get_all_issuers_from_db(int64_t now) {
406409
if (next_update_iter == top_obj.end() ||
407410
!next_update_iter->second.is<int64_t>()) {
408411
// If next_update is not set, default to 4 hours before expiry
409-
next_update = expiry - 4 * 3600;
412+
next_update = expiry - DEFAULT_NEXT_UPDATE_OFFSET_S;
410413
} else {
411414
next_update = next_update_iter->second.get<int64_t>();
412415
}
@@ -425,3 +428,138 @@ scitokens::Validator::get_all_issuers_from_db(int64_t now) {
425428
sqlite3_close(db);
426429
return result;
427430
}
431+
432+
std::string scitokens::Validator::load_jwks(const std::string &issuer) {
433+
auto now = std::time(NULL);
434+
picojson::value jwks;
435+
int64_t next_update;
436+
437+
try {
438+
// Try to get from cache
439+
if (get_public_keys_from_db(issuer, now, jwks, next_update)) {
440+
// Check if refresh is needed (expired based on next_update)
441+
if (now <= next_update) {
442+
// Still valid, return cached version
443+
return jwks.serialize();
444+
}
445+
// Past next_update, need to refresh
446+
}
447+
} catch (const NegativeCacheHitException &) {
448+
// Negative cache hit - return empty keys
449+
return std::string("{\"keys\": []}");
450+
}
451+
452+
// Either not in cache or past next_update - refresh
453+
if (!refresh_jwks(issuer)) {
454+
throw CurlException("Failed to load JWKS for issuer: " + issuer);
455+
}
456+
457+
// Get the newly refreshed JWKS
458+
return get_jwks(issuer);
459+
}
460+
461+
std::string scitokens::Validator::get_jwks_metadata(const std::string &issuer) {
462+
auto now = std::time(NULL);
463+
int64_t next_update = -1;
464+
int64_t expires = -1;
465+
466+
// Get the metadata from database without expiry check
467+
auto cache_fname = get_cache_file();
468+
if (cache_fname.size() == 0) {
469+
throw std::runtime_error("Unable to access cache file");
470+
}
471+
472+
sqlite3 *db;
473+
int rc = sqlite3_open(cache_fname.c_str(), &db);
474+
if (rc) {
475+
sqlite3_close(db);
476+
throw std::runtime_error("Failed to open cache database");
477+
}
478+
sqlite3_busy_timeout(db, SQLITE_BUSY_TIMEOUT_MS);
479+
480+
sqlite3_stmt *stmt;
481+
rc = sqlite3_prepare_v2(db, "SELECT keys from keycache where issuer = ?",
482+
-1, &stmt, NULL);
483+
if (rc != SQLITE_OK) {
484+
sqlite3_close(db);
485+
throw std::runtime_error("Failed to prepare database query");
486+
}
487+
488+
if (sqlite3_bind_text(stmt, 1, issuer.c_str(), issuer.size(),
489+
SQLITE_STATIC) != SQLITE_OK) {
490+
sqlite3_finalize(stmt);
491+
sqlite3_close(db);
492+
throw std::runtime_error("Failed to bind issuer to query");
493+
}
494+
495+
rc = sqlite3_step(stmt);
496+
if (rc == SQLITE_ROW) {
497+
const unsigned char *data = sqlite3_column_text(stmt, 0);
498+
std::string metadata(reinterpret_cast<const char *>(data));
499+
sqlite3_finalize(stmt);
500+
sqlite3_close(db);
501+
502+
picojson::value json_obj;
503+
auto err = picojson::parse(json_obj, metadata);
504+
if (!err.empty() || !json_obj.is<picojson::object>()) {
505+
throw JsonException("Invalid JSON in cache entry");
506+
}
507+
508+
auto top_obj = json_obj.get<picojson::object>();
509+
510+
// Extract expires
511+
auto iter = top_obj.find("expires");
512+
if (iter != top_obj.end() && iter->second.is<int64_t>()) {
513+
expires = iter->second.get<int64_t>();
514+
}
515+
516+
// Extract next_update
517+
iter = top_obj.find("next_update");
518+
if (iter != top_obj.end() && iter->second.is<int64_t>()) {
519+
next_update = iter->second.get<int64_t>();
520+
} else if (expires != -1) {
521+
// Default next_update to 4 hours before expiry
522+
next_update = expires - DEFAULT_NEXT_UPDATE_OFFSET_S;
523+
}
524+
525+
// Build metadata JSON (add future keys at top level if needed)
526+
picojson::object metadata_obj;
527+
if (expires != -1) {
528+
metadata_obj["expires"] = picojson::value(expires);
529+
}
530+
if (next_update != -1) {
531+
metadata_obj["next_update"] = picojson::value(next_update);
532+
}
533+
534+
return picojson::value(metadata_obj).serialize();
535+
} else {
536+
sqlite3_finalize(stmt);
537+
sqlite3_close(db);
538+
throw std::runtime_error("Issuer not found in cache");
539+
}
540+
}
541+
542+
bool scitokens::Validator::delete_jwks(const std::string &issuer) {
543+
auto cache_fname = get_cache_file();
544+
if (cache_fname.size() == 0) {
545+
return false;
546+
}
547+
548+
sqlite3 *db;
549+
int rc = sqlite3_open(cache_fname.c_str(), &db);
550+
if (rc) {
551+
sqlite3_close(db);
552+
return false;
553+
}
554+
sqlite3_busy_timeout(db, SQLITE_BUSY_TIMEOUT_MS);
555+
556+
// Use the existing remove_issuer_entry function
557+
// Note: remove_issuer_entry closes the database on error
558+
if (remove_issuer_entry(db, issuer, true) != 0) {
559+
// Database already closed by remove_issuer_entry
560+
return false;
561+
}
562+
563+
sqlite3_close(db);
564+
return true;
565+
}

src/scitokens_internal.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,6 +1309,26 @@ class Validator {
13091309
static std::vector<std::pair<std::string, int64_t>>
13101310
get_all_issuers_from_db(int64_t now);
13111311

1312+
/**
1313+
* Load JWKS for a given issuer, refreshing only if needed.
1314+
* Returns the JWKS string. If refresh is needed and fails, throws
1315+
* exception.
1316+
*/
1317+
static std::string load_jwks(const std::string &issuer);
1318+
1319+
/**
1320+
* Get metadata for a cached JWKS entry.
1321+
* Returns a JSON string with expires, next_update, and extra fields.
1322+
* Throws exception if issuer not found in cache.
1323+
*/
1324+
static std::string get_jwks_metadata(const std::string &issuer);
1325+
1326+
/**
1327+
* Delete a JWKS entry from the keycache.
1328+
* Returns true on success, false on failure.
1329+
*/
1330+
static bool delete_jwks(const std::string &issuer);
1331+
13121332
private:
13131333
static std::unique_ptr<AsyncStatus>
13141334
get_public_key_pem(const std::string &issuer, const std::string &kid,

0 commit comments

Comments
 (0)