Unified · Split
| @@ -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"); |
| @@ -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" }); |
| @@ -1,7 +1,7 @@ | |||
| 1 | import type { Doc, Id } from "../../convex/_generated/dataModel.d.ts"; | 1 | import type { Doc, Id } from "../../convex/_generated/dataModel.d.ts"; |
| 2 | import type { MutationCtx, QueryCtx } from "../../convex/_generated/server"; | 2 | import type { MutationCtx, QueryCtx } from "../../convex/_generated/server"; |
| 3 | import { internal } from "../../convex/_generated/api"; | 3 | import { internal } from "../../convex/_generated/api"; |
| 4 | import { getOrganizationMembership, slugify } from "../../../core/src/identity"; | 4 | import { getOrganizationMembership, isAdminRole, slugify } from "../../../core/src/identity"; |
| 5 | import { ensureUniqueSlug } from "../../../core/src/slug"; | 5 | import { ensureUniqueSlug } from "../../../core/src/slug"; |
| 6 | import { upsertOrganizationMembership } from "../auth/memberships"; | 6 | import { upsertOrganizationMembership } from "../auth/memberships"; |
| 7 | import { safeRunAfter } from "../lib/scheduler"; | 7 | import { 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); |