Skip to content

Commit 0fcced7

Browse files
committed
feat(webapp): Owner-gate org/project management API routes (manage:organization / manage:project)
1 parent 9573c59 commit 0fcced7

4 files changed

Lines changed: 46 additions & 6 deletions

File tree

apps/webapp/app/routes/api.v1.orgs.$orgParam.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { z } from "zod";
44
import { prisma } from "~/db.server";
55
import { DeleteOrganizationService } from "~/services/deleteOrganization.server";
66
import { logger } from "~/services/logger.server";
7-
import { resolveOrganizationForApiUser } from "~/services/organizationApiAccess.server";
7+
import {
8+
authorizePatOrganizationAccess,
9+
resolveOrganizationForApiUser,
10+
} from "~/services/organizationApiAccess.server";
811
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
912

1013
const ParamsSchema = z.object({
@@ -34,8 +37,6 @@ export async function action({ request, params }: ActionFunctionArgs) {
3437
return json({ error: "Invalid or Missing Access Token" }, { status: 401 });
3538
}
3639

37-
// Org delete/rename are membership-scoped only — the dashboard settings
38-
// route gates them on membership, not an RBAC ability.
3940
const organization = await resolveOrganizationForApiUser({
4041
orgParam: parsedParams.data.orgParam,
4142
userId: authenticationResult.userId,
@@ -45,6 +46,18 @@ export async function action({ request, params }: ActionFunctionArgs) {
4546
return json({ error: "Organization not found" }, { status: 404 });
4647
}
4748

49+
// Rename/delete are Owner-only (via manage:all). Membership resolution above
50+
// is the OSS floor; this is the role gate enforced when the RBAC plugin runs.
51+
const denied = await authorizePatOrganizationAccess({
52+
request,
53+
organizationId: organization.id,
54+
resource: "organization",
55+
action: "manage",
56+
});
57+
if (denied) {
58+
return denied;
59+
}
60+
4861
if (method === "DELETE") {
4962
try {
5063
await new DeleteOrganizationService().call({

apps/webapp/app/routes/api.v1.projects.$projectRef.default-region.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { z } from "zod";
55
import { prisma } from "~/db.server";
66
import { RegionsPresenter } from "~/presenters/v3/RegionsPresenter.server";
77
import { logger } from "~/services/logger.server";
8+
import { authorizePatOrganizationAccess } from "~/services/organizationApiAccess.server";
89
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
910
import { ServiceValidationError } from "~/v3/services/baseService.server";
1011
import { SetDefaultRegionService } from "~/v3/services/setDefaultRegion.server";
@@ -47,13 +48,24 @@ export async function action({ request, params }: ActionFunctionArgs) {
4748
},
4849
deletedAt: null,
4950
},
50-
select: { id: true, slug: true },
51+
select: { id: true, slug: true, organizationId: true },
5152
});
5253

5354
if (!project) {
5455
return json({ error: "Project not found" }, { status: 404 });
5556
}
5657

58+
// Setting a project's region is Owner-only (via manage:all).
59+
const denied = await authorizePatOrganizationAccess({
60+
request,
61+
organizationId: project.organizationId,
62+
resource: "project",
63+
action: "manage",
64+
});
65+
if (denied) {
66+
return denied;
67+
}
68+
5769
let rawBody: unknown;
5870
try {
5971
rawBody = await request.json();

apps/webapp/app/routes/api.v1.projects.$projectRef.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { z } from "zod";
55
import { prisma } from "~/db.server";
66
import { DeleteProjectService } from "~/services/deleteProject.server";
77
import { logger } from "~/services/logger.server";
8+
import { authorizePatOrganizationAccess } from "~/services/organizationApiAccess.server";
89
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
910
import { ProjectSettingsService } from "~/services/projectSettings.server";
1011

@@ -106,13 +107,25 @@ export async function action({ request, params }: ActionFunctionArgs) {
106107
organization: { deletedAt: null, members: { some: { userId: authenticationResult.userId } } },
107108
deletedAt: null,
108109
},
109-
select: { id: true },
110+
select: { id: true, organizationId: true },
110111
});
111112

112113
if (!project) {
113114
return json({ error: "Project not found" }, { status: 404 });
114115
}
115116

117+
// Rename/delete are Owner-only (via manage:all); membership resolve above is
118+
// the OSS floor, this is the role gate when the RBAC plugin runs.
119+
const denied = await authorizePatOrganizationAccess({
120+
request,
121+
organizationId: project.organizationId,
122+
resource: "project",
123+
action: "manage",
124+
});
125+
if (denied) {
126+
return denied;
127+
}
128+
116129
try {
117130
if (method === "DELETE") {
118131
await new DeleteProjectService().call({

apps/webapp/app/services/organizationApiAccess.server.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { isUserActorToken } from "@trigger.dev/rbac";
33
import { prisma } from "~/db.server";
44
import { rbac } from "~/services/rbac.server";
55

6-
type OrganizationScopedResource = "members";
6+
type OrganizationScopedResource = "members" | "organization" | "project";
77

88
const RESOURCE_LABELS: Record<OrganizationScopedResource, string> = {
99
members: "members",
10+
organization: "organization",
11+
project: "projects",
1012
};
1113

1214
/**

0 commit comments

Comments
 (0)