256 lines · 8.8 KB · TypeScript
RawDownload
// End-to-end coverage for secret isolation *through the real HTTP API*.//// Complements tenant-isolation.node.test.ts (which already covers plain// cross-org isolation at the org scope) by exercising the two-scope stack// the cloud app actually ships: `[userOrgScope, orgScope]`. The harness// builds the same shape `apps/cloud/src/services/executor.ts#createScopedExecutor`// builds in production, and every request goes through `HttpApiClient` →// `fetch` → the real `ProtectedCloudApi` → real postgres adapter.//// Invariants the product is staking on:////   1. Users in different orgs can't see each other's secret metadata.//   2. Users in the same org can't see each other's user-scoped secret//      metadata (per-user OAuth tokens etc. don't leak to co-workers).//   3. Org-scoped secret metadata IS visible to every user in that org//      — an admin writing a shared API key serves the whole tenant.//   4. The same user id in different orgs gets distinct per-user scopes —//      the userOrgScope id bakes in the org id on purpose.//   5. secrets.set rejects a scope id outside the caller's executor stack.//// NOTE: "per-user override shadows org default" cross-scope co-existence// is NOT covered here. `executor.secrets.set` currently deletes secret// metadata rows across the full scope stack before re-inserting at the// target scope (see executor.ts `secretsSet`), so an overrider writing// at their user-org scope wipes the org-level default rather than// shadowing it. If the product wants both rows to coexist, that's an// SDK-level change — coverage for it belongs after the fix. import { describe, expect, it } from "@effect/vitest";import { Effect } from "effect"; import { ScopeId, SecretId } from "@executor-js/sdk"; import {  asUser,  testUserOrgScopeId,} from "./services/__test-harness__/api-harness"; const uniq = () => crypto.randomUUID().slice(0, 8);const nextOrgId = () => `org_iso_${uniq()}`;const nextUserId = () => `user_iso_${uniq()}`; describe("cloud secret isolation (HTTP, user-org scope stack)", () => {  it.effect(    "users in different orgs cannot read each other's org-scoped secrets",    () =>      Effect.gen(function* () {        const orgA = nextOrgId();        const orgB = nextOrgId();        const alice = nextUserId();        const charlie = nextUserId();        const id = `sec_${uniq()}`;         yield* asUser(alice, orgA, (client) =>          client.secrets.set({            params: { scopeId: ScopeId.make(orgA) },            payload: {              id: SecretId.make(id),              name: "Shared",              value: "alice-org-secret",            },          }),        );         const charlieStatus = yield* asUser(charlie, orgB, (client) =>          client.secrets.status({            params: { scopeId: ScopeId.make(orgB), secretId: SecretId.make(id) },          }),        );        expect(charlieStatus.status).toBe("missing");         const charlieList = yield* asUser(charlie, orgB, (client) =>          client.secrets.list({ params: { scopeId: ScopeId.make(orgB) } }),        );        expect(charlieList.map((s) => s.id)).not.toContain(id);       }),  );   it.effect(    "users in same org cannot read each other's user-scoped secrets",    () =>      Effect.gen(function* () {        const orgId = nextOrgId();        const aliceId = nextUserId();        const bobId = nextUserId();        const id = `sec_${uniq()}`;         // Alice writes at her per-user scope — where OAuth tokens land.        yield* asUser(aliceId, orgId, (client) =>          client.secrets.set({            params: { scopeId: ScopeId.make(testUserOrgScopeId(aliceId, orgId)) },            payload: {              id: SecretId.make(id),              name: "Alice's token",              value: "alice-token-value",            },          }),        );         // Bob is in the same org — his user-org scope differs. He should        // not see the token in a list.        const bobList = yield* asUser(bobId, orgId, (client) =>          client.secrets.list({            params: { scopeId: ScopeId.make(testUserOrgScopeId(bobId, orgId)) },          }),        );        expect(bobList.map((s) => s.id)).not.toContain(id);         const bobStatus = yield* asUser(bobId, orgId, (client) =>          client.secrets.status({            params: {              scopeId: ScopeId.make(testUserOrgScopeId(bobId, orgId)),              secretId: SecretId.make(id),            },          }),        );        expect(bobStatus.status).toBe("missing");         // And Alice still sees her own token metadata.        const aliceStatus = yield* asUser(aliceId, orgId, (client) =>          client.secrets.status({            params: {              scopeId: ScopeId.make(testUserOrgScopeId(aliceId, orgId)),              secretId: SecretId.make(id),            },          }),        );        expect(aliceStatus.status).toBe("resolved");      }),  );   it.effect(    "org-scoped secrets are visible to every user in that org",    () =>      Effect.gen(function* () {        const orgId = nextOrgId();        const adminId = nextUserId();        const memberId = nextUserId();        const id = `sec_${uniq()}`;         yield* asUser(adminId, orgId, (client) =>          client.secrets.set({            params: { scopeId: ScopeId.make(orgId) },            payload: {              id: SecretId.make(id),              name: "Org API Key",              value: "shared-org-key",            },          }),        );         const adminStatus = yield* asUser(adminId, orgId, (client) =>          client.secrets.status({            params: { scopeId: ScopeId.make(orgId), secretId: SecretId.make(id) },          }),        );        const memberStatus = yield* asUser(memberId, orgId, (client) =>          client.secrets.status({            params: { scopeId: ScopeId.make(orgId), secretId: SecretId.make(id) },          }),        );        expect(adminStatus.status).toBe("resolved");        expect(memberStatus.status).toBe("resolved");      }),  );   it.effect(    "same userId in different orgs gets distinct per-user scopes",    () =>      Effect.gen(function* () {        const userId = nextUserId();        const orgA = nextOrgId();        const orgB = nextOrgId();        const id = `sec_${uniq()}`;         yield* asUser(userId, orgA, (client) =>          client.secrets.set({            params: { scopeId: ScopeId.make(testUserOrgScopeId(userId, orgA)) },            payload: {              id: SecretId.make(id),              name: "A token",              value: "value-in-a",            },          }),        );         // Same user id, different org → distinct user-org scope. The        // secret written in org A must not be visible when the same user        // logs into org B.        const listInB = yield* asUser(userId, orgB, (client) =>          client.secrets.list({            params: { scopeId: ScopeId.make(testUserOrgScopeId(userId, orgB)) },          }),        );        expect(listInB.map((s) => s.id)).not.toContain(id);         const statusInB = yield* asUser(userId, orgB, (client) =>          client.secrets.status({            params: {              scopeId: ScopeId.make(testUserOrgScopeId(userId, orgB)),              secretId: SecretId.make(id),            },          }),        );        expect(statusInB.status).toBe("missing");         // Sanity: the original write is still visible under the org-A        // user-org scope.        const statusInA = yield* asUser(userId, orgA, (client) =>          client.secrets.status({            params: {              scopeId: ScopeId.make(testUserOrgScopeId(userId, orgA)),              secretId: SecretId.make(id),            },          }),        );        expect(statusInA.status).toBe("resolved");      }),  );   it.effect("secrets.set rejects a scope outside the executor's stack", () =>    Effect.gen(function* () {      const orgId = nextOrgId();      const userId = nextUserId();      const foreignOrg = nextOrgId();       const result = yield* asUser(userId, orgId, (client) =>        client.secrets          .set({            params: { scopeId: ScopeId.make(foreignOrg) },            payload: {              id: SecretId.make("wrong-scope"),              name: "x",              value: "should not land",            },          })          .pipe(Effect.result),      );      expect(result._tag).toBe("Failure");       // And nothing landed in the foreign org — a fresh session pointed      // at that org must not see `wrong-scope`.      const foreignUser = nextUserId();      const leaked = yield* asUser(foreignUser, foreignOrg, (client) =>        client.secrets.status({          params: {            scopeId: ScopeId.make(foreignOrg),            secretId: SecretId.make("wrong-scope"),          },        }),      );      expect(leaked.status).toBe("missing");    }),  );});