mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
- Copy pg-knoe-auth/ wholesale from upstream/knoe-db/20260523 (Task 1 of docs/plans/junie/upstream-knoe-db-20260523-integration.md). - Extension: PG18 OAUTHBEARER JWT validator using libcurl + OpenSSL RS256. - knoe-db/Dockerfile: add libcurl4-openssl-dev to dev deps; COPY src/ and build with make USE_PGXS=1 install after tds_fdw. - NOT enabled in the default database build (absent from 20_create_extensions.sh). To enable: CREATE EXTENSION pg_knoe_auth; (requires pg_hba.conf oauth_issuer). Closes Task 1 of upstream-knoe-db-20260523-integration.md.
176 lines
5.6 KiB
C
176 lines
5.6 KiB
C
/*
|
|
* test_aud_pipeline.c — unit tests for the full aud claim pipeline.
|
|
*
|
|
* Tests the complete payload_json → json_get_aud → aud_contains path.
|
|
* This test MUST FAIL against rev1 code (before §2.2.4-FIX) because
|
|
* json_get_string bails on '[' and the array branch of aud_contains
|
|
* is never reached. It MUST PASS after §2.2.4-FIX (json_get_aud).
|
|
*
|
|
* 8 cases per §2.2.4-FIX table.
|
|
* See docs/plans/junie/pg_knoe_auth-rename-harden-modularize.md §2.2.4-FIX
|
|
*/
|
|
#include "test_helpers.h"
|
|
|
|
/* ── Inline copies of the functions under test ───────────────────────────── */
|
|
|
|
/*
|
|
* json_get_aud — copied verbatim from pg_knoe_auth.c so we can test it
|
|
* without linking against PostgreSQL.
|
|
*/
|
|
static char *
|
|
json_get_aud(const char *json)
|
|
{
|
|
char search[16];
|
|
const char *p, *start;
|
|
int depth;
|
|
|
|
snprintf(search, sizeof(search), "\"aud\"");
|
|
p = strstr(json, search);
|
|
if (!p) return NULL;
|
|
p += strlen(search);
|
|
|
|
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
|
|
if (*p != ':') return NULL;
|
|
p++;
|
|
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
|
|
|
|
if (*p == '"') {
|
|
/* String form — return content between quotes */
|
|
p++;
|
|
start = p;
|
|
while (*p && *p != '"') {
|
|
if (*p == '\\') p++;
|
|
if (*p) p++;
|
|
}
|
|
return pnstrdup(start, (size_t)(p - start));
|
|
}
|
|
|
|
if (*p == '[') {
|
|
/* Array form — return [...] verbatim including brackets */
|
|
start = p;
|
|
depth = 1;
|
|
p++;
|
|
while (*p && depth > 0) {
|
|
if (*p == '"') {
|
|
p++;
|
|
while (*p && *p != '"') {
|
|
if (*p == '\\') p++;
|
|
if (*p) p++;
|
|
}
|
|
if (*p == '"') p++;
|
|
} else if (*p == '[') {
|
|
depth++;
|
|
p++;
|
|
} else if (*p == ']') {
|
|
depth--;
|
|
p++;
|
|
} else {
|
|
p++;
|
|
}
|
|
}
|
|
return pnstrdup(start, (size_t)(p - start));
|
|
}
|
|
|
|
/* Unknown shape (number, null, etc.) — caller treats as missing. */
|
|
return NULL;
|
|
}
|
|
|
|
/*
|
|
* aud_contains — copied verbatim from pg_knoe_auth.c.
|
|
*/
|
|
static bool
|
|
aud_contains(const char *aud_value, const char *expected)
|
|
{
|
|
if (!aud_value || !expected)
|
|
return false;
|
|
|
|
/* Array form: ["val1","val2",...] */
|
|
if (aud_value[0] == '[') {
|
|
const char *p = aud_value + 1;
|
|
while (*p) {
|
|
const char *elem_start, *elem_end;
|
|
size_t elem_len;
|
|
|
|
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',')
|
|
p++;
|
|
if (*p == ']' || *p == '\0')
|
|
break;
|
|
if (*p != '"')
|
|
break;
|
|
p++;
|
|
|
|
elem_start = p;
|
|
while (*p && *p != '"') {
|
|
if (*p == '\\') p++;
|
|
if (*p) p++;
|
|
}
|
|
elem_end = p;
|
|
if (*p == '"') p++;
|
|
|
|
elem_len = (size_t)(elem_end - elem_start);
|
|
if (elem_len == strlen(expected) &&
|
|
strncmp(elem_start, expected, elem_len) == 0)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* String form: exact equality */
|
|
return strcmp(aud_value, expected) == 0;
|
|
}
|
|
|
|
/* ── Helper: run the full pipeline ──────────────────────────────────────── */
|
|
static bool
|
|
pipeline(const char *payload_json, const char *expected_aud)
|
|
{
|
|
char *claim_aud = json_get_aud(payload_json);
|
|
bool result = aud_contains(claim_aud, expected_aud);
|
|
if (claim_aud) pfree(claim_aud);
|
|
return result;
|
|
}
|
|
|
|
int main(void)
|
|
{
|
|
/* Case 1: string form — exact match → accept */
|
|
ASSERT( pipeline("{\"iss\":\"https://auth.0.knoe.dev\",\"aud\":\"pg.0.knoe.dev\",\"sub\":\"u1\"}",
|
|
"pg.0.knoe.dev"),
|
|
"string aud exact match passes");
|
|
|
|
/* Case 2: array form, single element matching → accept (THE KEY BUG CASE) */
|
|
ASSERT( pipeline("{\"iss\":\"https://auth.0.knoe.dev\",\"aud\":[\"pg.0.knoe.dev\"],\"sub\":\"u1\"}",
|
|
"pg.0.knoe.dev"),
|
|
"array aud single element match passes");
|
|
|
|
/* Case 3: array form, multiple elements, one matches → accept */
|
|
ASSERT( pipeline("{\"aud\":[\"other\",\"pg.0.knoe.dev\",\"more\"]}",
|
|
"pg.0.knoe.dev"),
|
|
"array aud multi-element match passes");
|
|
|
|
/* Case 4: array form, no element matches → reject */
|
|
ASSERT(!pipeline("{\"aud\":[\"other\",\"also-other\"]}",
|
|
"pg.0.knoe.dev"),
|
|
"array aud no match is rejected");
|
|
|
|
/* Case 5: string form — substring bypass attempt → reject */
|
|
ASSERT(!pipeline("{\"aud\":\"evil.pg.0.knoe.dev\"}",
|
|
"pg.0.knoe.dev"),
|
|
"string aud substring bypass is rejected");
|
|
|
|
/* Case 6: array form — substring bypass attempt → reject */
|
|
ASSERT(!pipeline("{\"aud\":[\"evil.pg.0.knoe.dev\"]}",
|
|
"pg.0.knoe.dev"),
|
|
"array aud substring bypass is rejected");
|
|
|
|
/* Case 7: numeric aud (e.g. aud:42) — json_get_aud returns NULL → reject */
|
|
ASSERT(!pipeline("{\"aud\":42,\"sub\":\"u1\"}",
|
|
"pg.0.knoe.dev"),
|
|
"numeric aud is rejected");
|
|
|
|
/* Case 8: missing aud claim entirely → reject */
|
|
ASSERT(!pipeline("{\"iss\":\"https://auth.0.knoe.dev\",\"sub\":\"u1\",\"exp\":9999999999}",
|
|
"pg.0.knoe.dev"),
|
|
"missing aud claim is rejected");
|
|
|
|
TEST_SUMMARY();
|
|
}
|