Skip to content

Commit 61a3288

Browse files
committed
fix(webapp): scope the org-creation RBAC check to user-actor tokens
1 parent 20c8f9e commit 61a3288

2 files changed

Lines changed: 49 additions & 5 deletions

File tree

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,33 @@ export const loader = createLoaderPATApiRoute(
4040
}
4141
);
4242

43-
// No org exists yet, so there is nothing to scope the gate to; any authenticated user can create
44-
// an org and becomes its ADMIN. The gate is still declared so a narrowly-capped delegated token
45-
// (which cannot `manage`) is refused rather than inheriting its user's full reach.
43+
// No org exists yet, so there is nothing to scope a route-level gate to; any authenticated user
44+
// can create an org and becomes its ADMIN. A narrowly-capped delegated token (which cannot
45+
// `manage`) is still refused rather than inheriting its user's full reach — but only for
46+
// user-actor tokens, so an ordinary PAT is unaffected.
4647
export const action = createActionPATApiRoute(
4748
{
4849
method: "POST",
4950
body: CreateOrgRequestBody,
50-
authorization: { action: "manage", resource: () => ({ type: "organization" }) },
5151
},
52-
async ({ body, authentication }) => {
52+
async ({ body, authentication, ability }) => {
5353
if (env.ORG_CREATION_API_ENABLED !== "1") {
5454
return json({ error: "Not found" }, { status: 404 });
5555
}
5656

57+
// After the env gate: an install with the API disabled should 404, not 403.
58+
if (authentication.userActor && !ability.can("manage", { type: "organization" })) {
59+
return json(
60+
{
61+
error: "Unauthorized",
62+
code: "unauthorized",
63+
param: "access_token",
64+
type: "authorization",
65+
},
66+
{ status: 403 }
67+
);
68+
}
69+
5770
// Mirror the dashboard: stash companyUrl/companySize as onboarding data and
5871
// derive the org avatar from the company domain's favicon.
5972
const onboardingData: Record<string, string> = {};

apps/webapp/test/contextlessPatRoutes.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,30 @@ async function createOrg(cap: string[]): Promise<{ status: number; body: any }>
7777
return { status: response.status, body: await response.json() };
7878
}
7979

80+
// An ordinary PAT, paired with an ability that denies everything. Nothing on this route may
81+
// consult it — the route has no org to scope a gate to, and on cloud the plugin returns a
82+
// deny-shaped ability when there is no org context.
83+
async function createOrgWithPat(): Promise<{ status: number; body: any }> {
84+
mocks.authenticatePat.mockImplementation(async () => ({
85+
ok: true,
86+
userId: USER_ID,
87+
tokenId: "pat_1",
88+
lastAccessedAt: new Date(),
89+
ability: { can: () => false, canSuper: () => false },
90+
}));
91+
92+
const response = await action({
93+
request: new Request("https://api.trigger.dev/api/v1/orgs", {
94+
method: "POST",
95+
headers: { Authorization: "Bearer tr_pat_1234", "Content-Type": "application/json" },
96+
body: JSON.stringify({ title: "New Org" }),
97+
}),
98+
params: {},
99+
context: {},
100+
} as any);
101+
return { status: response.status, body: await response.json() };
102+
}
103+
80104
const AGENT_ENVIRONMENT_ID = "env_dev";
81105

82106
async function listProjects(): Promise<{ status: number; body: any }> {
@@ -146,6 +170,13 @@ describe("creating an organization over the API", () => {
146170
expect(mocks.createOrganization).not.toHaveBeenCalled();
147171
});
148172

173+
it("admits an ordinary PAT without consulting its ability", async () => {
174+
const result = await createOrgWithPat();
175+
176+
expect(result.status).toBe(201);
177+
expect(result.body.slug).toBe("new-org");
178+
});
179+
149180
it("still admits a token that carries the universal grant", async () => {
150181
const result = await createOrg(["admin"]);
151182

0 commit comments

Comments
 (0)