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
125 throw new Error("Workspace not found");125 throw new Error("Workspace not found");
126 }126 }
127 127
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
128 const organizationMembership = await getOrganizationMembership(ctx, workspace.organizationId, account._id);133 const organizationMembership = await getOrganizationMembership(ctx, workspace.organizationId, account._id);
129 if (!organizationMembership || organizationMembership.status !== "active") {134 if (!organizationMembership || organizationMembership.status !== "active") {
130 throw new Error("You are not a member of this workspace");135 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
375 ).rejects.toThrow("You are not a member of this workspace");375 ).rejects.toThrow("You are not a member of this workspace");
376 });376 });
377 377
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
378 test("removed member cannot access workspace", async () => {398 test("removed member cannot access workspace", async () => {
379 const t = setup();399 const t = setup();
380 const owner = await seedUser(t, { subject: "ws-owner-3" });400 const owner = await seedUser(t, { subject: "ws-owner-3" });
@@ -987,6 +1007,27 @@ describe("workspace creation", () => { · 606 unmodified lines
987 ).rejects.toThrow("You are not a member of this organization");1007 ).rejects.toThrow("You are not a member of this organization");
988 });1008 });
989 1009
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
990 test("workspace name must be at least 2 characters", async () => {1031 test("workspace name must be at least 2 characters", async () => {
991 const t = setup();1032 const t = setup();
992 const user = await seedUser(t, { subject: "short-name" });1033 const user = await seedUser(t, { subject: "short-name" });
executor/packages/database/src/workspaces/handlers.ts+4−1
@@ -1,7 +1,7 @@
1import type { Doc, Id } from "../../convex/_generated/dataModel.d.ts";1import type { Doc, Id } from "../../convex/_generated/dataModel.d.ts";
2import type { MutationCtx, QueryCtx } from "../../convex/_generated/server";2import type { MutationCtx, QueryCtx } from "../../convex/_generated/server";
3import { internal } from "../../convex/_generated/api";3import { internal } from "../../convex/_generated/api";
4import { getOrganizationMembership, slugify } from "../../../core/src/identity";4import { getOrganizationMembership, isAdminRole, slugify } from "../../../core/src/identity";
5import { ensureUniqueSlug } from "../../../core/src/slug";5import { ensureUniqueSlug } from "../../../core/src/slug";
6import { upsertOrganizationMembership } from "../auth/memberships";6import { upsertOrganizationMembership } from "../auth/memberships";
7import { safeRunAfter } from "../lib/scheduler";7import { safeRunAfter } from "../lib/scheduler";
@@ -89,6 +89,9 @@ export async function createWorkspaceHandler( · 81 unmodified lines
89 if (!membership || membership.status !== "active") {89 if (!membership || membership.status !== "active") {
90 throw new Error("You are not a member of this organization");90 throw new Error("You are not a member of this organization");
91 }91 }
92 if (!isAdminRole(membership.role)) {
93 throw new Error("Only organization admins can create workspaces in this organization");
94 }
92 } else {95 } else {
93 const now = Date.now();96 const now = Date.now();
94 const organizationSlug = await ensureUniqueOrganizationSlug(ctx, name);97 const organizationSlug = await ensureUniqueOrganizationSlug(ctx, name);