Skip to content

Commit c3abfda

Browse files
committed
RBAC tests: deployments + query (TRI-8739)
Read-only family with distinct resource types per route: - GET /api/v1/deployments { type: "deployments", id: "list" } - GET /api/v1/query/schema { type: "query", id: "schema" } - GET /api/v1/query/dashboards { type: "query", id: "dashboards" } - POST /api/v1/query body-derived via detectTables(query) → tables.length > 0 ? tables.map(id => ({type:"query", id})) : { type: "query", id: "all" } Coverage: Deployments list (7 cases): - missing auth → 401 - private API key → passes - JWT read:deployments → passes - JWT read:all → passes - JWT admin → passes - JWT read:runs → 403 (type mismatch) - JWT write:deployments → 403 (action mismatch) Query schema sanity (3 cases): - missing auth → 401 - JWT read:query → passes - JWT read:deployments → 403 (type mismatch) Query dashboards sanity (2 cases): - missing auth → 401 - JWT read:query → passes Query ad-hoc body-derived (6 cases): - missing auth → 401 - body "SELECT * FROM runs" + JWT read:query:runs → passes (any-match against the body-derived array) - body "SELECT 1" (no detectable tables) + JWT read:query → passes (defaults to id="all"; type-level scope matches) - body with 'runs' + JWT read:query:other_table → 403 - JWT admin → passes regardless of body - JWT write:query → 403 (action mismatch) Verification: typecheck clean. Test execution still blocked by the e2e.full webapp-boot issue noted on TRI-8731. This closes the TRI-8731 test family (8733, 8734, 8735, 8736, 8737, 8738, 8739, 8740, 8741, 8742, 8743 — all done across today's commits).
1 parent 21f5827 commit c3abfda

1 file changed

Lines changed: 245 additions & 0 deletions

File tree

apps/webapp/test/auth-api.e2e.full.test.ts

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,251 @@ describe("API", () => {
20202020
});
20212021
});
20222022

2023+
// Deployments + query routes (TRI-8739). Read-only family with
2024+
// distinct resource types per route:
2025+
// - GET /api/v1/deployments { type: "deployments", id: "list" }
2026+
// - GET /api/v1/query/schema { type: "query", id: "schema" }
2027+
// - GET /api/v1/query/dashboards { type: "query", id: "dashboards" }
2028+
// - POST /api/v1/query body-derived: detectTables(query) →
2029+
// [{ type: "query", id }] or
2030+
// { type: "query", id: "all" } if none
2031+
describe("Deployments list — GET /api/v1/deployments", () => {
2032+
const path = "/api/v1/deployments";
2033+
const get = (headers: Record<string, string>) =>
2034+
getTestServer().webapp.fetch(path, { headers });
2035+
2036+
it("missing auth: 401", async () => {
2037+
const res = await getTestServer().webapp.fetch(path);
2038+
expect(res.status).toBe(401);
2039+
});
2040+
2041+
it("private API key: auth passes", async () => {
2042+
const server = getTestServer();
2043+
const seed = await seedTestEnvironment(server.prisma);
2044+
const res = await get({ Authorization: `Bearer ${seed.apiKey}` });
2045+
expect(res.status).not.toBe(401);
2046+
expect(res.status).not.toBe(403);
2047+
});
2048+
2049+
it("JWT read:deployments: auth passes", async () => {
2050+
const server = getTestServer();
2051+
const seed = await seedTestEnvironment(server.prisma);
2052+
const jwt = await generateJWT({
2053+
secretKey: seed.apiKey,
2054+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:deployments"] },
2055+
expirationTime: "15m",
2056+
});
2057+
const res = await get({ Authorization: `Bearer ${jwt}` });
2058+
expect(res.status).not.toBe(401);
2059+
expect(res.status).not.toBe(403);
2060+
});
2061+
2062+
it("JWT read:all: auth passes", async () => {
2063+
const server = getTestServer();
2064+
const seed = await seedTestEnvironment(server.prisma);
2065+
const jwt = await generateJWT({
2066+
secretKey: seed.apiKey,
2067+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:all"] },
2068+
expirationTime: "15m",
2069+
});
2070+
const res = await get({ Authorization: `Bearer ${jwt}` });
2071+
expect(res.status).not.toBe(401);
2072+
expect(res.status).not.toBe(403);
2073+
});
2074+
2075+
it("JWT admin: auth passes", async () => {
2076+
const server = getTestServer();
2077+
const seed = await seedTestEnvironment(server.prisma);
2078+
const jwt = await generateJWT({
2079+
secretKey: seed.apiKey,
2080+
payload: { pub: true, sub: seed.environment.id, scopes: ["admin"] },
2081+
expirationTime: "15m",
2082+
});
2083+
const res = await get({ Authorization: `Bearer ${jwt}` });
2084+
expect(res.status).not.toBe(401);
2085+
expect(res.status).not.toBe(403);
2086+
});
2087+
2088+
it("JWT read:runs (type mismatch): 403", async () => {
2089+
const server = getTestServer();
2090+
const seed = await seedTestEnvironment(server.prisma);
2091+
const jwt = await generateJWT({
2092+
secretKey: seed.apiKey,
2093+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:runs"] },
2094+
expirationTime: "15m",
2095+
});
2096+
const res = await get({ Authorization: `Bearer ${jwt}` });
2097+
expect(res.status).toBe(403);
2098+
});
2099+
2100+
it("JWT write:deployments (action mismatch — read route): 403", async () => {
2101+
const server = getTestServer();
2102+
const seed = await seedTestEnvironment(server.prisma);
2103+
const jwt = await generateJWT({
2104+
secretKey: seed.apiKey,
2105+
payload: { pub: true, sub: seed.environment.id, scopes: ["write:deployments"] },
2106+
expirationTime: "15m",
2107+
});
2108+
const res = await get({ Authorization: `Bearer ${jwt}` });
2109+
expect(res.status).toBe(403);
2110+
});
2111+
});
2112+
2113+
describe("Query schema — GET /api/v1/query/schema (sanity)", () => {
2114+
const path = "/api/v1/query/schema";
2115+
2116+
it("missing auth: 401", async () => {
2117+
const res = await getTestServer().webapp.fetch(path);
2118+
expect(res.status).toBe(401);
2119+
});
2120+
2121+
it("JWT read:query (type-level): auth passes", async () => {
2122+
const server = getTestServer();
2123+
const seed = await seedTestEnvironment(server.prisma);
2124+
const jwt = await generateJWT({
2125+
secretKey: seed.apiKey,
2126+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:query"] },
2127+
expirationTime: "15m",
2128+
});
2129+
const res = await server.webapp.fetch(path, {
2130+
headers: { Authorization: `Bearer ${jwt}` },
2131+
});
2132+
expect(res.status).not.toBe(401);
2133+
expect(res.status).not.toBe(403);
2134+
});
2135+
2136+
it("JWT read:deployments (type mismatch): 403", async () => {
2137+
const server = getTestServer();
2138+
const seed = await seedTestEnvironment(server.prisma);
2139+
const jwt = await generateJWT({
2140+
secretKey: seed.apiKey,
2141+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:deployments"] },
2142+
expirationTime: "15m",
2143+
});
2144+
const res = await server.webapp.fetch(path, {
2145+
headers: { Authorization: `Bearer ${jwt}` },
2146+
});
2147+
expect(res.status).toBe(403);
2148+
});
2149+
});
2150+
2151+
describe("Query dashboards — GET /api/v1/query/dashboards (sanity)", () => {
2152+
const path = "/api/v1/query/dashboards";
2153+
2154+
it("missing auth: 401", async () => {
2155+
const res = await getTestServer().webapp.fetch(path);
2156+
expect(res.status).toBe(401);
2157+
});
2158+
2159+
it("JWT read:query: auth passes", async () => {
2160+
const server = getTestServer();
2161+
const seed = await seedTestEnvironment(server.prisma);
2162+
const jwt = await generateJWT({
2163+
secretKey: seed.apiKey,
2164+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:query"] },
2165+
expirationTime: "15m",
2166+
});
2167+
const res = await server.webapp.fetch(path, {
2168+
headers: { Authorization: `Bearer ${jwt}` },
2169+
});
2170+
expect(res.status).not.toBe(401);
2171+
expect(res.status).not.toBe(403);
2172+
});
2173+
});
2174+
2175+
describe("Query ad-hoc — POST /api/v1/query (body-derived resource)", () => {
2176+
const path = "/api/v1/query";
2177+
const post = (body: object, headers: Record<string, string>) =>
2178+
getTestServer().webapp.fetch(path, {
2179+
method: "POST",
2180+
headers: { "Content-Type": "application/json", ...headers },
2181+
body: JSON.stringify(body),
2182+
});
2183+
2184+
it("missing auth: 401", async () => {
2185+
const res = await post({ query: "SELECT * FROM runs" }, {});
2186+
expect(res.status).toBe(401);
2187+
});
2188+
2189+
it("body with table 'runs' + JWT read:query:runs: auth passes (any-match)", async () => {
2190+
// detectTables pulls 'runs' from FROM-clause. Resource becomes
2191+
// [{ type: "query", id: "runs" }]. Scope read:query:runs matches.
2192+
const server = getTestServer();
2193+
const seed = await seedTestEnvironment(server.prisma);
2194+
const jwt = await generateJWT({
2195+
secretKey: seed.apiKey,
2196+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:query:runs"] },
2197+
expirationTime: "15m",
2198+
});
2199+
const res = await post({ query: "SELECT * FROM runs" }, {
2200+
Authorization: `Bearer ${jwt}`,
2201+
});
2202+
expect(res.status).not.toBe(401);
2203+
expect(res.status).not.toBe(403);
2204+
});
2205+
2206+
it("body with no detectable tables (defaults id='all') + JWT read:query: auth passes", async () => {
2207+
// A query with no FROM clause → detectTables returns [] →
2208+
// resource is { type: "query", id: "all" }. Type-level read:query
2209+
// matches.
2210+
const server = getTestServer();
2211+
const seed = await seedTestEnvironment(server.prisma);
2212+
const jwt = await generateJWT({
2213+
secretKey: seed.apiKey,
2214+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:query"] },
2215+
expirationTime: "15m",
2216+
});
2217+
const res = await post({ query: "SELECT 1" }, { Authorization: `Bearer ${jwt}` });
2218+
expect(res.status).not.toBe(401);
2219+
expect(res.status).not.toBe(403);
2220+
});
2221+
2222+
it("body with table 'runs' + JWT read:query:other_table: 403", async () => {
2223+
const server = getTestServer();
2224+
const seed = await seedTestEnvironment(server.prisma);
2225+
const jwt = await generateJWT({
2226+
secretKey: seed.apiKey,
2227+
payload: {
2228+
pub: true,
2229+
sub: seed.environment.id,
2230+
scopes: ["read:query:other_table"],
2231+
},
2232+
expirationTime: "15m",
2233+
});
2234+
const res = await post({ query: "SELECT * FROM runs" }, {
2235+
Authorization: `Bearer ${jwt}`,
2236+
});
2237+
expect(res.status).toBe(403);
2238+
});
2239+
2240+
it("JWT admin: auth passes regardless of body", async () => {
2241+
const server = getTestServer();
2242+
const seed = await seedTestEnvironment(server.prisma);
2243+
const jwt = await generateJWT({
2244+
secretKey: seed.apiKey,
2245+
payload: { pub: true, sub: seed.environment.id, scopes: ["admin"] },
2246+
expirationTime: "15m",
2247+
});
2248+
const res = await post({ query: "SELECT * FROM runs" }, {
2249+
Authorization: `Bearer ${jwt}`,
2250+
});
2251+
expect(res.status).not.toBe(401);
2252+
expect(res.status).not.toBe(403);
2253+
});
2254+
2255+
it("JWT write:query (action mismatch): 403", async () => {
2256+
const server = getTestServer();
2257+
const seed = await seedTestEnvironment(server.prisma);
2258+
const jwt = await generateJWT({
2259+
secretKey: seed.apiKey,
2260+
payload: { pub: true, sub: seed.environment.id, scopes: ["write:query"] },
2261+
expirationTime: "15m",
2262+
});
2263+
const res = await post({ query: "SELECT 1" }, { Authorization: `Bearer ${jwt}` });
2264+
expect(res.status).toBe(403);
2265+
});
2266+
});
2267+
20232268
describe("Batch retrieve — GET /realtime/v1/batches/:batchId (sanity)", () => {
20242269
it("missing auth: 401", async () => {
20252270
const res = await getTestServer().webapp.fetch("/realtime/v1/batches/batch_anything");

0 commit comments

Comments
 (0)