Skip to content

Commit ce586da

Browse files
committed
RBAC tests: batch retrieve + realtime (TRI-8737)
Per-batch endpoints share a single-id resource config: resource: { type: "batch", id: batch.friendlyId } Notable: the resource type is "batch", NOT "runs". The legacy literal-match escape that let read:runs JWTs hit batch endpoints no longer applies post-TRI-8719. Tests pin this down. The list endpoint (GET /api/v1/batches) was deleted on the s3- switchover branch — list-section coverage is N/A on this branch. If/when the list endpoint returns, add a list-side describe. api.v1.batches.$batchId — 10 cases: - missing auth → 401 - invalid API key → 401 - private API key on real batch → auth passes - JWT read:batch:<friendlyId> matching → passes - JWT read:batch:<other> → 403 - JWT read:batch (type-level) → passes - JWT read:runs → 403 (resource type is "batch", not "runs" — pre-TRI-8719 this passed via legacy literal-match escape; locking in the post-migration strict behaviour) - JWT read:all → passes - JWT admin → passes - cross-env: env A's JWT cannot read env B's batch → not 200 api.v2.batches.$batchId — 2-case sanity (config identical to v1). realtime.v1.batches.$batchId — 2-case sanity. If a route in this family ever diverges from the canonical pattern, add a dedicated describe. Reuses seedTestRun({ withBatch: true }) — helper already creates the BatchTaskRun + linked TaskRun for us. Verification: typecheck clean. Test execution still blocked by the e2e.full webapp-boot issue noted on TRI-8731.
1 parent 167e681 commit ce586da

1 file changed

Lines changed: 221 additions & 0 deletions

File tree

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

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,4 +1539,225 @@ describe("API", () => {
15391539
expect(res.status).not.toBe(403);
15401540
});
15411541
});
1542+
1543+
// Batch resources (TRI-8737). Per-batch retrieve + realtime
1544+
// endpoints — single-id resource `{ type: "batch", id: batch.friendlyId }`.
1545+
// The list endpoint (`GET /api/v1/batches`) is currently absent
1546+
// from this branch (deleted in s3-switchover), so the list-
1547+
// section of the matrix is N/A here. If/when the list endpoint
1548+
// returns, add a list-side describe.
1549+
//
1550+
// Notable behaviour: the route's resource is `{ type: "batch" }`,
1551+
// NOT `{ type: "runs" }`. The legacy literal-match escape that
1552+
// let `read:runs` JWTs hit batch endpoints no longer applies.
1553+
// Tests pin this down (a `read:runs` scope on a `{ type: "batch" }`
1554+
// resource is a type mismatch → 403).
1555+
describe("Batch retrieve — GET /api/v1/batches/:batchId", () => {
1556+
const pathFor = (batchId: string) => `/api/v1/batches/${batchId}`;
1557+
1558+
async function seedRunWithBatch() {
1559+
const server = getTestServer();
1560+
const seed = await seedTestEnvironment(server.prisma);
1561+
const seeded = await seedTestRun(server.prisma, {
1562+
environmentId: seed.environment.id,
1563+
projectId: seed.project.id,
1564+
withBatch: true,
1565+
});
1566+
// batchFriendlyId is guaranteed when withBatch is set.
1567+
if (!seeded.batchFriendlyId) {
1568+
throw new Error("seedTestRun({ withBatch: true }) didn't return a batchFriendlyId");
1569+
}
1570+
return { ...seed, batchFriendlyId: seeded.batchFriendlyId };
1571+
}
1572+
1573+
const get = (path: string, headers: Record<string, string>) =>
1574+
getTestServer().webapp.fetch(path, { headers });
1575+
1576+
it("missing auth: 401", async () => {
1577+
const res = await get(pathFor("batch_anything"), {});
1578+
expect(res.status).toBe(401);
1579+
});
1580+
1581+
it("invalid API key: 401", async () => {
1582+
const res = await get(pathFor("batch_anything"), {
1583+
Authorization: "Bearer tr_dev_invalid",
1584+
});
1585+
expect(res.status).toBe(401);
1586+
});
1587+
1588+
it("private API key on real batch: auth passes", async () => {
1589+
const { batchFriendlyId, apiKey } = await seedRunWithBatch();
1590+
const res = await get(pathFor(batchFriendlyId), {
1591+
Authorization: `Bearer ${apiKey}`,
1592+
});
1593+
expect(res.status).not.toBe(401);
1594+
expect(res.status).not.toBe(403);
1595+
});
1596+
1597+
it("JWT read:batch:<friendlyId> matching: auth passes", async () => {
1598+
const { batchFriendlyId, apiKey, environment } = await seedRunWithBatch();
1599+
const jwt = await generateJWT({
1600+
secretKey: apiKey,
1601+
payload: {
1602+
pub: true,
1603+
sub: environment.id,
1604+
scopes: [`read:batch:${batchFriendlyId}`],
1605+
},
1606+
expirationTime: "15m",
1607+
});
1608+
const res = await get(pathFor(batchFriendlyId), { Authorization: `Bearer ${jwt}` });
1609+
expect(res.status).not.toBe(401);
1610+
expect(res.status).not.toBe(403);
1611+
});
1612+
1613+
it("JWT read:batch:<other>: 403", async () => {
1614+
const { batchFriendlyId, apiKey, environment } = await seedRunWithBatch();
1615+
const jwt = await generateJWT({
1616+
secretKey: apiKey,
1617+
payload: {
1618+
pub: true,
1619+
sub: environment.id,
1620+
scopes: ["read:batch:batch_someoneelse00000000"],
1621+
},
1622+
expirationTime: "15m",
1623+
});
1624+
const res = await get(pathFor(batchFriendlyId), { Authorization: `Bearer ${jwt}` });
1625+
expect(res.status).toBe(403);
1626+
});
1627+
1628+
it("JWT read:batch (type-level): auth passes", async () => {
1629+
const { batchFriendlyId, apiKey, environment } = await seedRunWithBatch();
1630+
const jwt = await generateJWT({
1631+
secretKey: apiKey,
1632+
payload: { pub: true, sub: environment.id, scopes: ["read:batch"] },
1633+
expirationTime: "15m",
1634+
});
1635+
const res = await get(pathFor(batchFriendlyId), { Authorization: `Bearer ${jwt}` });
1636+
expect(res.status).not.toBe(401);
1637+
expect(res.status).not.toBe(403);
1638+
});
1639+
1640+
it("JWT read:runs: 403 (resource type is 'batch', not 'runs')", async () => {
1641+
const { batchFriendlyId, apiKey, environment } = await seedRunWithBatch();
1642+
const jwt = await generateJWT({
1643+
secretKey: apiKey,
1644+
payload: { pub: true, sub: environment.id, scopes: ["read:runs"] },
1645+
expirationTime: "15m",
1646+
});
1647+
const res = await get(pathFor(batchFriendlyId), { Authorization: `Bearer ${jwt}` });
1648+
// Pre-TRI-8719 the legacy literal-match escape granted
1649+
// read:runs access to batch endpoints. Post-migration the
1650+
// resource type is strictly { type: "batch" } and read:runs
1651+
// doesn't match. Lock this in — if SDKs were issuing
1652+
// read:runs:* JWTs for batch lookups, that's a regression to
1653+
// catch.
1654+
expect(res.status).toBe(403);
1655+
});
1656+
1657+
it("JWT read:all super-scope: auth passes", async () => {
1658+
const { batchFriendlyId, apiKey, environment } = await seedRunWithBatch();
1659+
const jwt = await generateJWT({
1660+
secretKey: apiKey,
1661+
payload: { pub: true, sub: environment.id, scopes: ["read:all"] },
1662+
expirationTime: "15m",
1663+
});
1664+
const res = await get(pathFor(batchFriendlyId), { Authorization: `Bearer ${jwt}` });
1665+
expect(res.status).not.toBe(401);
1666+
expect(res.status).not.toBe(403);
1667+
});
1668+
1669+
it("JWT admin: auth passes", async () => {
1670+
const { batchFriendlyId, apiKey, environment } = await seedRunWithBatch();
1671+
const jwt = await generateJWT({
1672+
secretKey: apiKey,
1673+
payload: { pub: true, sub: environment.id, scopes: ["admin"] },
1674+
expirationTime: "15m",
1675+
});
1676+
const res = await get(pathFor(batchFriendlyId), { Authorization: `Bearer ${jwt}` });
1677+
expect(res.status).not.toBe(401);
1678+
expect(res.status).not.toBe(403);
1679+
});
1680+
1681+
it("cross-env: env A's JWT cannot read env B's batch: not 200", async () => {
1682+
const server = getTestServer();
1683+
const a = await seedTestEnvironment(server.prisma);
1684+
const b = await seedRunWithBatch();
1685+
const jwt = await generateJWT({
1686+
secretKey: a.apiKey,
1687+
payload: {
1688+
pub: true,
1689+
sub: a.environment.id,
1690+
scopes: [`read:batch:${b.batchFriendlyId}`],
1691+
},
1692+
expirationTime: "15m",
1693+
});
1694+
const res = await get(pathFor(b.batchFriendlyId), { Authorization: `Bearer ${jwt}` });
1695+
// Critical: env A's JWT can't see env B's batch (env-scoped
1696+
// findResource returns null). NOT 200.
1697+
expect(res.status).not.toBe(200);
1698+
});
1699+
});
1700+
1701+
// Sanity: api.v2 and realtime.v1 share the exact same authorization
1702+
// config as v1. Don't duplicate the full matrix; just verify the
1703+
// wiring is alive on each.
1704+
describe("Batch retrieve — GET /api/v2/batches/:batchId (sanity)", () => {
1705+
it("missing auth: 401", async () => {
1706+
const res = await getTestServer().webapp.fetch("/api/v2/batches/batch_anything");
1707+
expect(res.status).toBe(401);
1708+
});
1709+
1710+
it("JWT read:batch (type-level): auth passes on real batch", async () => {
1711+
const server = getTestServer();
1712+
const seed = await seedTestEnvironment(server.prisma);
1713+
const seeded = await seedTestRun(server.prisma, {
1714+
environmentId: seed.environment.id,
1715+
projectId: seed.project.id,
1716+
withBatch: true,
1717+
});
1718+
const jwt = await generateJWT({
1719+
secretKey: seed.apiKey,
1720+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:batch"] },
1721+
expirationTime: "15m",
1722+
});
1723+
const res = await getTestServer().webapp.fetch(
1724+
`/api/v2/batches/${seeded.batchFriendlyId}`,
1725+
{ headers: { Authorization: `Bearer ${jwt}` } }
1726+
);
1727+
expect(res.status).not.toBe(401);
1728+
expect(res.status).not.toBe(403);
1729+
});
1730+
});
1731+
1732+
describe("Batch retrieve — GET /realtime/v1/batches/:batchId (sanity)", () => {
1733+
it("missing auth: 401", async () => {
1734+
const res = await getTestServer().webapp.fetch("/realtime/v1/batches/batch_anything");
1735+
expect(res.status).toBe(401);
1736+
});
1737+
1738+
it("JWT read:batch:<friendlyId>: auth passes on real batch", async () => {
1739+
const server = getTestServer();
1740+
const seed = await seedTestEnvironment(server.prisma);
1741+
const seeded = await seedTestRun(server.prisma, {
1742+
environmentId: seed.environment.id,
1743+
projectId: seed.project.id,
1744+
withBatch: true,
1745+
});
1746+
const jwt = await generateJWT({
1747+
secretKey: seed.apiKey,
1748+
payload: {
1749+
pub: true,
1750+
sub: seed.environment.id,
1751+
scopes: [`read:batch:${seeded.batchFriendlyId}`],
1752+
},
1753+
expirationTime: "15m",
1754+
});
1755+
const res = await getTestServer().webapp.fetch(
1756+
`/realtime/v1/batches/${seeded.batchFriendlyId}`,
1757+
{ headers: { Authorization: `Bearer ${jwt}` } }
1758+
);
1759+
expect(res.status).not.toBe(401);
1760+
expect(res.status).not.toBe(403);
1761+
});
1762+
});
15421763
});

0 commit comments

Comments
 (0)