History

tighten workspace creation and inactive org access checks

Rhys Sullivan committed 8 months ago

Commit 514e00b1 parent e643d8d3 files · +50 −1Browse files

Unified · Split

3 files
  1. executor/packages/core/src/identity.ts +5 −0
  2. executor/packages/database/convex/access-controls.test.ts +41 −0
  3. executor/packages/database/src/workspaces/handlers.ts +4 −1
executor/packages/core/src/identity.ts+5−0
@@ -125,6 +125,11 @@ export async function requireWorkspaceAccessForAccount( · 124 unmodified lines
125125 throw new Error("Workspace not found");
126126 }
127127
128+ const organization = await ctx.db.get(workspace.organizationId);
129+ if (!organization || organization.status !== "active") {
130+ throw new Error("Workspace organization is inactive");
131+ }
132+
128133 const organizationMembership = await getOrganizationMembership(ctx, workspace.organizationId, account._id);
129134 if (!organizationMembership || organizationMembership.status !== "active") {
130135 throw new Error("You are not a member of this workspace");
executor/packages/database/convex/access-controls.test.ts+41−0
@@ -375,6 +375,26 @@ describe("workspace access controls", () => { · 374 unmodified lines
375375 ).rejects.toThrow("You are not a member of this workspace");
376376 });
377377
378+ test("workspace access is denied when organization is inactive", async () => {
379+ const t = setup();
380+ const owner = await seedUser(t, { subject: "inactive-org-owner" });
381+
382+ await t.run(async (ctx) => {
383+ await ctx.db.patch(owner.organizationId, {
384+ status: "deleted",
385+ updatedAt: Date.now(),
386+ });
387+ });
388+
389+ const authedOwner = t.withIdentity({ subject: "inactive-org-owner" });
390+
391+ await expect(
392+ authedOwner.query(api.workspace.listTasks, {
393+ workspaceId: owner.workspaceId,
394+ }),
395+ ).rejects.toThrow("Workspace organization is inactive");
396+ });
397+
378398 test("removed member cannot access workspace", async () => {
379399 const t = setup();
380400 const owner = await seedUser(t, { subject: "ws-owner-3" });
@@ -987,6 +1007,27 @@ describe("workspace creation", () => { · 606 unmodified lines
9871007 ).rejects.toThrow("You are not a member of this organization");
9881008 });
9891009
1010+ test("org member cannot create workspace without admin role", async () => {
1011+ const t = setup();
1012+ const owner = await seedUser(t, { subject: "ws-org-owner" });
1013+ const member = await seedUser(t, { subject: "ws-org-member" });
1014+
1015+ await addOrgMember(t, {
1016+ organizationId: owner.organizationId,
1017+ accountId: member.accountId,
1018+ role: "member",
1019+ });
1020+
1021+ const authedMember = t.withIdentity({ subject: "ws-org-member" });
1022+
1023+ await expect(
1024+ authedMember.mutation(api.workspaces.create, {
1025+ name: "Member Workspace",
1026+ organizationId: owner.organizationId,
1027+ }),
1028+ ).rejects.toThrow("Only organization admins can create workspaces in this organization");
1029+ });
1030+
9901031 test("workspace name must be at least 2 characters", async () => {
9911032 const t = setup();
9921033 const user = await seedUser(t, { subject: "short-name" });
executor/packages/database/src/workspaces/handlers.ts+4−1
@@ -1,7 +1,7 @@
11 import type { Doc, Id } from "../../convex/_generated/dataModel.d.ts";
22 import type { MutationCtx, QueryCtx } from "../../convex/_generated/server";
33 import { internal } from "../../convex/_generated/api";
4−import { getOrganizationMembership, slugify } from "../../../core/src/identity";
4+import { getOrganizationMembership, isAdminRole, slugify } from "../../../core/src/identity";
55 import { ensureUniqueSlug } from "../../../core/src/slug";
66 import { upsertOrganizationMembership } from "../auth/memberships";
77 import { safeRunAfter } from "../lib/scheduler";
@@ -89,6 +89,9 @@ export async function createWorkspaceHandler( · 81 unmodified lines
8989 if (!membership || membership.status !== "active") {
9090 throw new Error("You are not a member of this organization");
9191 }
92+ if (!isAdminRole(membership.role)) {
93+ throw new Error("Only organization admins can create workspaces in this organization");
94+ }
9295 } else {
9396 const now = Date.now();
9497 const organizationSlug = await ensureUniqueOrganizationSlug(ctx, name);