Skip to content

Commit 0cba469

Browse files
committed
RBAC tests: run mutations — cancel + idempotencyKeys.reset (TRI-8735)
Two routes with different resource shapes: - POST /api/v2/runs/:runParam/cancel action: write, resource: { type: "runs", id: params.runParam } Single id-keyed resource — id-specific scopes work. - POST /api/v1/idempotencyKeys/:key/reset action: write, resource: { type: "runs" } (collection-level) Id-specific scopes don't grant blanket access; only type-level write:runs (or super-scopes) work. Pre-TRI-8719 the empty- resource path rejected ALL JWTs; post-migration write:runs passes. Coverage locks in the new behaviour. Cancel (api.v2.runs.$runParam.cancel) — 9 cases: - missing auth → 401 - invalid API key → 401 - private API key on real run → auth passes - JWT write:runs (type-level) → auth passes - JWT write:runs:<exact> → auth passes - JWT write:runs:<other> → 403 - JWT read:runs (action mismatch) → 403 - JWT write:all → auth passes - JWT admin → auth passes idempotencyKeys.reset (api.v1.idempotencyKeys.$key.reset) — 7 cases: - missing auth → 401 - invalid API key → 401 - private API key → auth passes - JWT write:runs → auth passes (pinned regression: pre-TRI-8719 this returned 403 due to the empty-resource bug) - JWT read:runs → 403 (action mismatch) - JWT write:all → auth passes - JWT admin → auth passes Verification: typecheck clean. Test execution still blocked by the e2e.full webapp-boot issue noted on TRI-8731.
1 parent 3911b7a commit 0cba469

1 file changed

Lines changed: 265 additions & 0 deletions

File tree

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

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,4 +1019,269 @@ describe("API", () => {
10191019
expect(res.status).toBe(403);
10201020
});
10211021
});
1022+
1023+
// Run mutations (TRI-8735). Two routes:
1024+
// - POST /api/v2/runs/:runParam/cancel
1025+
// action: write, resource: { type: "runs", id: params.runParam }
1026+
// — single id-keyed resource, supports id-specific scopes.
1027+
// - POST /api/v1/idempotencyKeys/:key/reset
1028+
// action: write, resource: { type: "runs" } (collection-level)
1029+
// — id-specific scopes don't grant blanket access; only
1030+
// type-level write:runs (or super-scopes) work.
1031+
//
1032+
// The legacy idempotencyKeys/:key/reset rejected ALL JWTs due to an
1033+
// empty-resource bug. Post TRI-8719 the empty-resource resolution
1034+
// lets write:runs JWTs through. Tests here lock in the new behaviour.
1035+
describe("Run mutations — cancel (api.v2.runs.$runParam.cancel)", () => {
1036+
const pathFor = (runId: string) => `/api/v2/runs/${runId}/cancel`;
1037+
const post = (path: string, headers: Record<string, string>) =>
1038+
getTestServer().webapp.fetch(path, {
1039+
method: "POST",
1040+
headers: { "Content-Type": "application/json", ...headers },
1041+
body: JSON.stringify({}),
1042+
});
1043+
1044+
it("missing auth: 401", async () => {
1045+
const res = await post(pathFor("run_anything"), {});
1046+
expect(res.status).toBe(401);
1047+
});
1048+
1049+
it("invalid API key: 401", async () => {
1050+
const res = await post(pathFor("run_anything"), {
1051+
Authorization: "Bearer tr_dev_definitely_not_real_key",
1052+
});
1053+
expect(res.status).toBe(401);
1054+
});
1055+
1056+
it("private API key on real run: auth passes", async () => {
1057+
const server = getTestServer();
1058+
const seed = await seedTestEnvironment(server.prisma);
1059+
const { runFriendlyId } = await seedTestRun(server.prisma, {
1060+
environmentId: seed.environment.id,
1061+
projectId: seed.project.id,
1062+
});
1063+
const res = await post(pathFor(runFriendlyId), {
1064+
Authorization: `Bearer ${seed.apiKey}`,
1065+
});
1066+
// Auth + findResource passed; handler may return any 2xx/4xx
1067+
// depending on run state. We only care: not 401/403.
1068+
expect(res.status).not.toBe(401);
1069+
expect(res.status).not.toBe(403);
1070+
});
1071+
1072+
it("JWT with write:runs (type-level): auth passes", async () => {
1073+
const server = getTestServer();
1074+
const seed = await seedTestEnvironment(server.prisma);
1075+
const { runFriendlyId } = await seedTestRun(server.prisma, {
1076+
environmentId: seed.environment.id,
1077+
projectId: seed.project.id,
1078+
});
1079+
const jwt = await generateJWT({
1080+
secretKey: seed.apiKey,
1081+
payload: { pub: true, sub: seed.environment.id, scopes: ["write:runs"] },
1082+
expirationTime: "15m",
1083+
});
1084+
const res = await post(pathFor(runFriendlyId), {
1085+
Authorization: `Bearer ${jwt}`,
1086+
});
1087+
expect(res.status).not.toBe(401);
1088+
expect(res.status).not.toBe(403);
1089+
});
1090+
1091+
it("JWT with write:runs:<exact runId>: auth passes", async () => {
1092+
const server = getTestServer();
1093+
const seed = await seedTestEnvironment(server.prisma);
1094+
const { runFriendlyId } = await seedTestRun(server.prisma, {
1095+
environmentId: seed.environment.id,
1096+
projectId: seed.project.id,
1097+
});
1098+
const jwt = await generateJWT({
1099+
secretKey: seed.apiKey,
1100+
payload: {
1101+
pub: true,
1102+
sub: seed.environment.id,
1103+
scopes: [`write:runs:${runFriendlyId}`],
1104+
},
1105+
expirationTime: "15m",
1106+
});
1107+
const res = await post(pathFor(runFriendlyId), {
1108+
Authorization: `Bearer ${jwt}`,
1109+
});
1110+
expect(res.status).not.toBe(401);
1111+
expect(res.status).not.toBe(403);
1112+
});
1113+
1114+
it("JWT with write:runs:<other>: 403", async () => {
1115+
const server = getTestServer();
1116+
const seed = await seedTestEnvironment(server.prisma);
1117+
const { runFriendlyId } = await seedTestRun(server.prisma, {
1118+
environmentId: seed.environment.id,
1119+
projectId: seed.project.id,
1120+
});
1121+
const jwt = await generateJWT({
1122+
secretKey: seed.apiKey,
1123+
payload: {
1124+
pub: true,
1125+
sub: seed.environment.id,
1126+
scopes: ["write:runs:run_someoneelse00000000000"],
1127+
},
1128+
expirationTime: "15m",
1129+
});
1130+
const res = await post(pathFor(runFriendlyId), {
1131+
Authorization: `Bearer ${jwt}`,
1132+
});
1133+
expect(res.status).toBe(403);
1134+
});
1135+
1136+
it("JWT with read:runs (action mismatch): 403", async () => {
1137+
const server = getTestServer();
1138+
const seed = await seedTestEnvironment(server.prisma);
1139+
const { runFriendlyId } = await seedTestRun(server.prisma, {
1140+
environmentId: seed.environment.id,
1141+
projectId: seed.project.id,
1142+
});
1143+
const jwt = await generateJWT({
1144+
secretKey: seed.apiKey,
1145+
payload: {
1146+
pub: true,
1147+
sub: seed.environment.id,
1148+
scopes: [`read:runs:${runFriendlyId}`],
1149+
},
1150+
expirationTime: "15m",
1151+
});
1152+
const res = await post(pathFor(runFriendlyId), {
1153+
Authorization: `Bearer ${jwt}`,
1154+
});
1155+
expect(res.status).toBe(403);
1156+
});
1157+
1158+
it("JWT with write:all super-scope: auth passes", async () => {
1159+
const server = getTestServer();
1160+
const seed = await seedTestEnvironment(server.prisma);
1161+
const { runFriendlyId } = await seedTestRun(server.prisma, {
1162+
environmentId: seed.environment.id,
1163+
projectId: seed.project.id,
1164+
});
1165+
const jwt = await generateJWT({
1166+
secretKey: seed.apiKey,
1167+
payload: { pub: true, sub: seed.environment.id, scopes: ["write:all"] },
1168+
expirationTime: "15m",
1169+
});
1170+
const res = await post(pathFor(runFriendlyId), {
1171+
Authorization: `Bearer ${jwt}`,
1172+
});
1173+
expect(res.status).not.toBe(401);
1174+
expect(res.status).not.toBe(403);
1175+
});
1176+
1177+
it("JWT with admin: auth passes", async () => {
1178+
const server = getTestServer();
1179+
const seed = await seedTestEnvironment(server.prisma);
1180+
const { runFriendlyId } = await seedTestRun(server.prisma, {
1181+
environmentId: seed.environment.id,
1182+
projectId: seed.project.id,
1183+
});
1184+
const jwt = await generateJWT({
1185+
secretKey: seed.apiKey,
1186+
payload: { pub: true, sub: seed.environment.id, scopes: ["admin"] },
1187+
expirationTime: "15m",
1188+
});
1189+
const res = await post(pathFor(runFriendlyId), {
1190+
Authorization: `Bearer ${jwt}`,
1191+
});
1192+
expect(res.status).not.toBe(401);
1193+
expect(res.status).not.toBe(403);
1194+
});
1195+
});
1196+
1197+
describe("Run mutations — idempotencyKeys.reset (api.v1.idempotencyKeys.$key.reset)", () => {
1198+
// Collection-level resource { type: "runs" } — id-specific
1199+
// write:runs:<runId> scopes don't help here (no id to match).
1200+
// The legacy version of this route rejected ALL JWTs due to an
1201+
// empty-resource bug; the post-TRI-8719 path lets write:runs
1202+
// through. Tests below pin that down.
1203+
const path = "/api/v1/idempotencyKeys/some-key/reset";
1204+
const validBody = JSON.stringify({ taskIdentifier: "test-task" });
1205+
1206+
const post = (headers: Record<string, string>, body = validBody) =>
1207+
getTestServer().webapp.fetch(path, {
1208+
method: "POST",
1209+
headers: { "Content-Type": "application/json", ...headers },
1210+
body,
1211+
});
1212+
1213+
it("missing auth: 401", async () => {
1214+
const res = await post({});
1215+
expect(res.status).toBe(401);
1216+
});
1217+
1218+
it("invalid API key: 401", async () => {
1219+
const res = await post({ Authorization: "Bearer tr_dev_invalid" });
1220+
expect(res.status).toBe(401);
1221+
});
1222+
1223+
it("private API key: auth passes", async () => {
1224+
const server = getTestServer();
1225+
const seed = await seedTestEnvironment(server.prisma);
1226+
const res = await post({ Authorization: `Bearer ${seed.apiKey}` });
1227+
// Handler may 404/204 depending on whether the idempotency key
1228+
// exists. Auth-passed assertion only.
1229+
expect(res.status).not.toBe(401);
1230+
expect(res.status).not.toBe(403);
1231+
});
1232+
1233+
it("JWT with write:runs (type-level): auth passes — locks in TRI-8719 fix", async () => {
1234+
const server = getTestServer();
1235+
const seed = await seedTestEnvironment(server.prisma);
1236+
const jwt = await generateJWT({
1237+
secretKey: seed.apiKey,
1238+
payload: { pub: true, sub: seed.environment.id, scopes: ["write:runs"] },
1239+
expirationTime: "15m",
1240+
});
1241+
const res = await post({ Authorization: `Bearer ${jwt}` });
1242+
// PRE-TRI-8719: this returned 403 (legacy empty-resource bug
1243+
// rejected all JWTs). POST-TRI-8719: write:runs grants access.
1244+
// Locking in the new behaviour.
1245+
expect(res.status).not.toBe(401);
1246+
expect(res.status).not.toBe(403);
1247+
});
1248+
1249+
it("JWT with read:runs (action mismatch): 403", async () => {
1250+
const server = getTestServer();
1251+
const seed = await seedTestEnvironment(server.prisma);
1252+
const jwt = await generateJWT({
1253+
secretKey: seed.apiKey,
1254+
payload: { pub: true, sub: seed.environment.id, scopes: ["read:runs"] },
1255+
expirationTime: "15m",
1256+
});
1257+
const res = await post({ Authorization: `Bearer ${jwt}` });
1258+
expect(res.status).toBe(403);
1259+
});
1260+
1261+
it("JWT with write:all: auth passes", async () => {
1262+
const server = getTestServer();
1263+
const seed = await seedTestEnvironment(server.prisma);
1264+
const jwt = await generateJWT({
1265+
secretKey: seed.apiKey,
1266+
payload: { pub: true, sub: seed.environment.id, scopes: ["write:all"] },
1267+
expirationTime: "15m",
1268+
});
1269+
const res = await post({ Authorization: `Bearer ${jwt}` });
1270+
expect(res.status).not.toBe(401);
1271+
expect(res.status).not.toBe(403);
1272+
});
1273+
1274+
it("JWT with admin: auth passes", async () => {
1275+
const server = getTestServer();
1276+
const seed = await seedTestEnvironment(server.prisma);
1277+
const jwt = await generateJWT({
1278+
secretKey: seed.apiKey,
1279+
payload: { pub: true, sub: seed.environment.id, scopes: ["admin"] },
1280+
expirationTime: "15m",
1281+
});
1282+
const res = await post({ Authorization: `Bearer ${jwt}` });
1283+
expect(res.status).not.toBe(401);
1284+
expect(res.status).not.toBe(403);
1285+
});
1286+
});
10221287
});

0 commit comments

Comments
 (0)