271 lines · 9.3 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 secrets — not even//      secrets written at the org scope of the other org.//   2. Users in the same org can't see each other's user-scoped secrets//      (per-user OAuth tokens etc. don't leak to co-workers).//   3. Org-scoped secrets ARE 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/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({            path: { 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({            path: { scopeId: ScopeId.make(orgB), secretId: SecretId.make(id) },          }),        );        expect(charlieStatus.status).toBe("missing");         const charlieList = yield* asUser(charlie, orgB, (client) =>          client.secrets.list({ path: { scopeId: ScopeId.make(orgB) } }),        );        expect(charlieList.map((s) => s.id)).not.toContain(id);         const charlieResolve = yield* asUser(charlie, orgB, (client) =>          client.secrets            .resolve({              path: { scopeId: ScopeId.make(orgB), secretId: SecretId.make(id) },            })            .pipe(Effect.either),        );        expect(charlieResolve._tag).toBe("Left");      }),  );   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({            path: { 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        // see neither the token in a list nor be able to resolve it.        const bobList = yield* asUser(bobId, orgId, (client) =>          client.secrets.list({            path: { scopeId: ScopeId.make(testUserOrgScopeId(bobId, orgId)) },          }),        );        expect(bobList.map((s) => s.id)).not.toContain(id);         const bobResolve = yield* asUser(bobId, orgId, (client) =>          client.secrets            .resolve({              path: {                scopeId: ScopeId.make(testUserOrgScopeId(bobId, orgId)),                secretId: SecretId.make(id),              },            })            .pipe(Effect.either),        );        expect(bobResolve._tag).toBe("Left");         // And Alice still sees her own token.        const aliceResolve = yield* asUser(aliceId, orgId, (client) =>          client.secrets.resolve({            path: {              scopeId: ScopeId.make(testUserOrgScopeId(aliceId, orgId)),              secretId: SecretId.make(id),            },          }),        );        expect(aliceResolve.value).toBe("alice-token-value");      }),  );   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({            path: { scopeId: ScopeId.make(orgId) },            payload: {              id: SecretId.make(id),              name: "Org API Key",              value: "shared-org-key",            },          }),        );         const adminValue = yield* asUser(adminId, orgId, (client) =>          client.secrets.resolve({            path: { scopeId: ScopeId.make(orgId), secretId: SecretId.make(id) },          }),        );        const memberValue = yield* asUser(memberId, orgId, (client) =>          client.secrets.resolve({            path: { scopeId: ScopeId.make(orgId), secretId: SecretId.make(id) },          }),        );        expect(adminValue.value).toBe("shared-org-key");        expect(memberValue.value).toBe("shared-org-key");      }),  );   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({            path: { 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({            path: { scopeId: ScopeId.make(testUserOrgScopeId(userId, orgB)) },          }),        );        expect(listInB.map((s) => s.id)).not.toContain(id);         const resolveInB = yield* asUser(userId, orgB, (client) =>          client.secrets            .resolve({              path: {                scopeId: ScopeId.make(testUserOrgScopeId(userId, orgB)),                secretId: SecretId.make(id),              },            })            .pipe(Effect.either),        );        expect(resolveInB._tag).toBe("Left");         // Sanity: the original write is still readable under the org-A        // user-org scope.        const resolveInA = yield* asUser(userId, orgA, (client) =>          client.secrets.resolve({            path: {              scopeId: ScopeId.make(testUserOrgScopeId(userId, orgA)),              secretId: SecretId.make(id),            },          }),        );        expect(resolveInA.value).toBe("value-in-a");      }),  );   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({            path: { scopeId: ScopeId.make(foreignOrg) },            payload: {              id: SecretId.make("wrong-scope"),              name: "x",              value: "should not land",            },          })          .pipe(Effect.either),      );      expect(result._tag).toBe("Left");       // 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          .resolve({            path: {              scopeId: ScopeId.make(foreignOrg),              secretId: SecretId.make("wrong-scope"),            },          })          .pipe(Effect.either),      );      expect(leaked._tag).toBe("Left");    }),  );});