Unified · Split
| @@ -458,6 +458,23 @@ describe("workspace access controls", () => { · 457 unmodified lines | |||
| 458 | expect(policy.resourcePattern).toBe("*.delete"); | 458 | expect(policy.resourcePattern).toBe("*.delete"); |
| 459 | }); | 459 | }); |
| 460 | 460 | ||
| 461 | test("account-scoped policy target must be active org member", async () => { | ||
| 462 | const t = setup(); | ||
| 463 | const owner = await seedUser(t, { subject: "policy-target-owner" }); | ||
| 464 | const outsider = await seedUser(t, { subject: "policy-target-outsider" }); | ||
| 465 | const authedOwner = t.withIdentity({ subject: "policy-target-owner" }); | ||
| 466 | |||
| 467 | await expect( | ||
| 468 | authedOwner.mutation(api.workspace.upsertAccessPolicy, { | ||
| 469 | workspaceId: owner.workspaceId, | ||
| 470 | scopeType: "account", | ||
| 471 | targetAccountId: outsider.accountId, | ||
| 472 | resourcePattern: "*", | ||
| 473 | effect: "allow", | ||
| 474 | }), | ||
| 475 | ).rejects.toThrow("targetAccountId must be an active member of this organization"); | ||
| 476 | }); | ||
| 477 | |||
| 461 | test("workspace owner can upsert access policies", async () => { | 478 | test("workspace owner can upsert access policies", async () => { |
| 462 | const t = setup(); | 479 | const t = setup(); |
| 463 | const owner = await seedUser(t, { subject: "ws-owner-policy" }); | 480 | const owner = await seedUser(t, { subject: "ws-owner-policy" }); |
| @@ -528,6 +545,23 @@ describe("workspace access controls", () => { · 64 unmodified lines | |||
| 528 | ).rejects.toThrow("Only organization admins can perform this action"); | 545 | ).rejects.toThrow("Only organization admins can perform this action"); |
| 529 | }); | 546 | }); |
| 530 | 547 | ||
| 548 | test("account-scoped credential target must be active org member", async () => { | ||
| 549 | const t = setup(); | ||
| 550 | const owner = await seedUser(t, { subject: "cred-target-owner" }); | ||
| 551 | const outsider = await seedUser(t, { subject: "cred-target-outsider" }); | ||
| 552 | const authedOwner = t.withIdentity({ subject: "cred-target-owner" }); | ||
| 553 | |||
| 554 | await expect( | ||
| 555 | authedOwner.mutation(api.workspace.upsertCredential, { | ||
| 556 | workspaceId: owner.workspaceId, | ||
| 557 | sourceKey: "openapi:github", | ||
| 558 | scopeType: "account", | ||
| 559 | accountId: outsider.accountId, | ||
| 560 | secretJson: { token: "ghp_test" }, | ||
| 561 | }), | ||
| 562 | ).rejects.toThrow("accountId must be an active member of this organization"); | ||
| 563 | }); | ||
| 564 | |||
| 531 | test("regular member cannot delete tool sources (admin-only)", async () => { | 565 | test("regular member cannot delete tool sources (admin-only)", async () => { |
| 532 | const t = setup(); | 566 | const t = setup(); |
| 533 | const owner = await seedUser(t, { subject: "del-tool-owner" }); | 567 | const owner = await seedUser(t, { subject: "del-tool-owner" }); |
| @@ -1256,6 +1290,45 @@ describe("credential security", () => { · 722 unmodified lines | |||
| 1256 | }), | 1290 | }), |
| 1257 | ).rejects.toThrow("Only organization admins can perform this action"); | 1291 | ).rejects.toThrow("Only organization admins can perform this action"); |
| 1258 | }); | 1292 | }); |
| 1293 | |||
| 1294 | test("non-admin tool source listing redacts auth secret values", async () => { | ||
| 1295 | const t = setup(); | ||
| 1296 | const owner = await seedUser(t, { subject: "source-redact-owner" }); | ||
| 1297 | const member = await seedUser(t, { subject: "source-redact-member" }); | ||
| 1298 | |||
| 1299 | await addOrgMember(t, { | ||
| 1300 | organizationId: owner.organizationId, | ||
| 1301 | accountId: member.accountId, | ||
| 1302 | role: "member", | ||
| 1303 | }); | ||
| 1304 | |||
| 1305 | const authedOwner = t.withIdentity({ subject: "source-redact-owner" }); | ||
| 1306 | await authedOwner.mutation(api.workspace.upsertToolSource, { | ||
| 1307 | workspaceId: owner.workspaceId, | ||
| 1308 | name: "secure-source", | ||
| 1309 | type: "openapi", | ||
| 1310 | config: { | ||
| 1311 | spec: "https://example.com/openapi.json", | ||
| 1312 | baseUrl: "https://example.com", | ||
| 1313 | auth: { | ||
| 1314 | type: "bearer", | ||
| 1315 | mode: "workspace", | ||
| 1316 | token: "super-secret-token", | ||
| 1317 | }, | ||
| 1318 | }, | ||
| 1319 | }); | ||
| 1320 | |||
| 1321 | const authedMember = t.withIdentity({ subject: "source-redact-member" }); | ||
| 1322 | const sources = await authedMember.query(api.workspace.listToolSources, { | ||
| 1323 | workspaceId: owner.workspaceId, | ||
| 1324 | }); | ||
| 1325 | |||
| 1326 | expect(sources.length).toBe(1); | ||
| 1327 | const auth = (sources[0]!.config.auth ?? {}) as Record<string, unknown>; | ||
| 1328 | expect(auth.type).toBe("bearer"); | ||
| 1329 | expect(auth.mode).toBe("workspace"); | ||
| 1330 | expect(auth.token).toBeUndefined(); | ||
| 1331 | }); | ||
| 1259 | }); | 1332 | }); |
| 1260 | 1333 | ||
| 1261 | describe("role hierarchy validation", () => { | 1334 | describe("role hierarchy validation", () => { |