Unified · Split
| @@ -458,6 +458,23 @@ describe("workspace access controls", () => { · 457 unmodified lines | ||
| 458 | 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 | 478 | test("workspace owner can upsert access policies", async () => { |
| 462 | 479 | const t = setup(); |
| 463 | 480 | const owner = await seedUser(t, { subject: "ws-owner-policy" }); |
| @@ -528,6 +545,23 @@ describe("workspace access controls", () => { · 64 unmodified lines | ||
| 528 | 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 | 565 | test("regular member cannot delete tool sources (admin-only)", async () => { |
| 532 | 566 | const t = setup(); |
| 533 | 567 | const owner = await seedUser(t, { subject: "del-tool-owner" }); |
| @@ -1256,6 +1290,45 @@ describe("credential security", () => { · 722 unmodified lines | ||
| 1256 | 1290 | }), |
| 1257 | 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 | 1334 | describe("role hierarchy validation", () => { |