Skip to content

Commit b9d119a

Browse files
committed
RBAC plugin: Result types on mutation methods + OSS fallback (TRI-8747)
Mutation methods on RoleBaseAccessController now return discriminated Result unions instead of throwing on expected error paths: - RoleMutationResult — { ok: true; role: Role } | { ok: false; error } for createRole, updateRole. - RoleAssignmentResult — { ok: true } | { ok: false; error: string } for deleteRole, setUserRole, removeUserRole, setTokenRole, removeTokenRole. The cloud webapp surfaces the 'error' strings directly to users (system role edits, plan-tier gating, validation conflicts), so a thrown exception now signals only an unexpected failure (DB outage, bug). Read methods (getUserRole, getTokenRole, allRoles, allPermissions) are unchanged. OSS fallback returns { ok: false, error: 'RBAC plugin not installed' } for every mutation — matches the prior behaviour (createRole/updateRole already threw with this message; the others were silent no-ops, which made misuse hard to detect). The LazyController in @internal/rbac forwards the new return types verbatim. Changeset: patch. Customer-facing surface: only public type widening of mutation method return types — no runtime behaviour change for OSS callers (they get a Result error instead of a thrown error or silent no-op; both indicate 'do not call these without the enterprise plugin').
1 parent bb5079e commit b9d119a

5 files changed

Lines changed: 78 additions & 25 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/plugins": patch
3+
---
4+
5+
RBAC plugin: mutation methods on `RoleBaseAccessController` now return discriminated `Result` types instead of throwing on expected error paths. `createRole` and `updateRole` return `RoleMutationResult` (`{ ok: true; role: Role } | { ok: false; error: string }`); `deleteRole`, `setUserRole`, `removeUserRole`, `setTokenRole`, and `removeTokenRole` return `RoleAssignmentResult` (`{ ok: true } | { ok: false; error: string }`). The webapp surfaces the `error` strings directly to users (system role edits, plan-tier gating, validation conflicts) so a thrown exception now signals only an unexpected failure (DB outage, bug). Read methods (`getUserRole`, `getTokenRole`, `allRoles`, `allPermissions`) are unchanged.

internal-packages/rbac/src/fallback.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import type {
77
RbacResource,
88
BearerAuthResult,
99
SessionAuthResult,
10+
RoleAssignmentResult,
1011
RoleBaseAccessController,
12+
RoleMutationResult,
1113
} from "@trigger.dev/plugins";
1214
import type { PrismaClient } from "@trigger.dev/database";
1315
import { validateJWT } from "@trigger.dev/core/v3/jwt";
@@ -163,29 +165,41 @@ class RoleBaseAccessFallbackController implements RoleBaseAccessController {
163165
return [];
164166
}
165167

166-
async createRole(): Promise<Role> {
167-
throw new Error("RBAC plugin not installed");
168+
async createRole(): Promise<RoleMutationResult> {
169+
return { ok: false, error: "RBAC plugin not installed" };
168170
}
169171

170-
async updateRole(): Promise<Role> {
171-
throw new Error("RBAC plugin not installed");
172+
async updateRole(): Promise<RoleMutationResult> {
173+
return { ok: false, error: "RBAC plugin not installed" };
172174
}
173175

174-
async deleteRole(): Promise<void> {}
176+
async deleteRole(): Promise<RoleAssignmentResult> {
177+
return { ok: false, error: "RBAC plugin not installed" };
178+
}
175179

176180
async getUserRole(): Promise<Role | null> {
177181
return null;
178182
}
179183

180-
async setUserRole(): Promise<void> {}
181-
async removeUserRole(): Promise<void> {}
184+
async setUserRole(): Promise<RoleAssignmentResult> {
185+
return { ok: false, error: "RBAC plugin not installed" };
186+
}
187+
188+
async removeUserRole(): Promise<RoleAssignmentResult> {
189+
return { ok: false, error: "RBAC plugin not installed" };
190+
}
182191

183192
async getTokenRole(): Promise<Role | null> {
184193
return null;
185194
}
186195

187-
async setTokenRole(): Promise<void> {}
188-
async removeTokenRole(): Promise<void> {}
196+
async setTokenRole(): Promise<RoleAssignmentResult> {
197+
return { ok: false, error: "RBAC plugin not installed" };
198+
}
199+
200+
async removeTokenRole(): Promise<RoleAssignmentResult> {
201+
return { ok: false, error: "RBAC plugin not installed" };
202+
}
189203
}
190204

191205
function isPublicJWT(token: string): boolean {

internal-packages/rbac/src/index.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import type {
33
RbacAbility,
44
Role,
55
RbacResource,
6+
RoleAssignmentResult,
67
RoleBaseAccessController,
78
RoleBasedAccessControlPlugin,
9+
RoleMutationResult,
810
} from "@trigger.dev/plugins";
911
import type { PrismaClient } from "@trigger.dev/database";
1012
import { RoleBaseAccessFallback } from "./fallback.js";
@@ -121,39 +123,53 @@ class LazyController implements RoleBaseAccessController {
121123
return (await this.c()).allRoles(...args);
122124
}
123125

124-
async createRole(...args: Parameters<RoleBaseAccessController["createRole"]>): Promise<Role> {
126+
async createRole(
127+
...args: Parameters<RoleBaseAccessController["createRole"]>
128+
): Promise<RoleMutationResult> {
125129
return (await this.c()).createRole(...args);
126130
}
127131

128-
async updateRole(...args: Parameters<RoleBaseAccessController["updateRole"]>): Promise<Role> {
132+
async updateRole(
133+
...args: Parameters<RoleBaseAccessController["updateRole"]>
134+
): Promise<RoleMutationResult> {
129135
return (await this.c()).updateRole(...args);
130136
}
131137

132-
async deleteRole(...args: Parameters<RoleBaseAccessController["deleteRole"]>): Promise<void> {
138+
async deleteRole(
139+
...args: Parameters<RoleBaseAccessController["deleteRole"]>
140+
): Promise<RoleAssignmentResult> {
133141
return (await this.c()).deleteRole(...args);
134142
}
135143

136144
async getUserRole(...args: Parameters<RoleBaseAccessController["getUserRole"]>): Promise<Role | null> {
137145
return (await this.c()).getUserRole(...args);
138146
}
139147

140-
async setUserRole(...args: Parameters<RoleBaseAccessController["setUserRole"]>): Promise<void> {
148+
async setUserRole(
149+
...args: Parameters<RoleBaseAccessController["setUserRole"]>
150+
): Promise<RoleAssignmentResult> {
141151
return (await this.c()).setUserRole(...args);
142152
}
143153

144-
async removeUserRole(...args: Parameters<RoleBaseAccessController["removeUserRole"]>): Promise<void> {
154+
async removeUserRole(
155+
...args: Parameters<RoleBaseAccessController["removeUserRole"]>
156+
): Promise<RoleAssignmentResult> {
145157
return (await this.c()).removeUserRole(...args);
146158
}
147159

148160
async getTokenRole(...args: Parameters<RoleBaseAccessController["getTokenRole"]>): Promise<Role | null> {
149161
return (await this.c()).getTokenRole(...args);
150162
}
151163

152-
async setTokenRole(...args: Parameters<RoleBaseAccessController["setTokenRole"]>): Promise<void> {
164+
async setTokenRole(
165+
...args: Parameters<RoleBaseAccessController["setTokenRole"]>
166+
): Promise<RoleAssignmentResult> {
153167
return (await this.c()).setTokenRole(...args);
154168
}
155169

156-
async removeTokenRole(...args: Parameters<RoleBaseAccessController["removeTokenRole"]>): Promise<void> {
170+
async removeTokenRole(
171+
...args: Parameters<RoleBaseAccessController["removeTokenRole"]>
172+
): Promise<RoleAssignmentResult> {
157173
return (await this.c()).removeTokenRole(...args);
158174
}
159175
}

packages/plugins/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export type {
22
RoleBasedAccessControlPlugin,
33
RoleBaseAccessController,
4+
RoleAssignmentResult,
5+
RoleMutationResult,
46
Permission,
57
Role,
68
RbacAbility,

packages/plugins/src/rbac.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -98,24 +98,31 @@ export interface RoleBaseAccessController {
9898
allPermissions(organizationId: string): Promise<Permission[]>;
9999
allRoles(organizationId: string): Promise<Role[]>;
100100

101-
// Role management (throws in OSS fallback)
101+
// Role management. Mutation methods return a discriminated Result
102+
// rather than throwing — the cloud webapp surfaces `error` strings
103+
// directly to the user (system role edits, plan-gating, validation
104+
// conflicts), so a thrown exception is only ever for unexpected
105+
// failures (DB outage, bug). The OSS fallback returns
106+
// `{ ok: false, error: "RBAC plugin not installed" }` for these.
102107
createRole(params: {
103108
organizationId: string;
104109
name: string;
105110
description: string;
106111
permissions: string[];
107-
}): Promise<Role>;
112+
}): Promise<RoleMutationResult>;
108113

109114
updateRole(params: {
110115
roleId: string;
111116
name?: string;
112117
description?: string;
113118
permissions?: string[];
114-
}): Promise<Role>;
119+
}): Promise<RoleMutationResult>;
115120

116-
deleteRole(roleId: string): Promise<void>;
121+
deleteRole(roleId: string): Promise<RoleAssignmentResult>;
117122

118-
// Role assignments (no-ops in OSS fallback)
123+
// Role assignments. Same Result discipline as the role-management
124+
// methods above. The OSS fallback returns
125+
// `{ ok: false, error: "RBAC plugin not installed" }`.
119126
getUserRole(params: {
120127
userId: string;
121128
organizationId: string;
@@ -127,19 +134,28 @@ export interface RoleBaseAccessController {
127134
organizationId: string;
128135
roleId: string;
129136
projectId?: string;
130-
}): Promise<void>;
137+
}): Promise<RoleAssignmentResult>;
131138

132139
removeUserRole(params: {
133140
userId: string;
134141
organizationId: string;
135142
projectId?: string;
136-
}): Promise<void>;
143+
}): Promise<RoleAssignmentResult>;
137144

138145
getTokenRole(tokenId: string): Promise<Role | null>;
139-
setTokenRole(params: { tokenId: string; roleId: string }): Promise<void>;
140-
removeTokenRole(tokenId: string): Promise<void>;
146+
setTokenRole(params: { tokenId: string; roleId: string }): Promise<RoleAssignmentResult>;
147+
removeTokenRole(tokenId: string): Promise<RoleAssignmentResult>;
141148
}
142149

150+
// Mutation result for role create/update — success carries the new
151+
// `role`, failure carries a user-facing `error` string.
152+
export type RoleMutationResult =
153+
| { ok: true; role: Role }
154+
| { ok: false; error: string };
155+
156+
// Result for assignment / deletion mutations that don't return a value.
157+
export type RoleAssignmentResult = { ok: true } | { ok: false; error: string };
158+
143159
export interface RoleBasedAccessControlPlugin {
144160
create(
145161
helpers: { getSessionUserId: (request: Request) => Promise<string | null> }

0 commit comments

Comments
 (0)