|
9 | 9 | */ |
10 | 10 |
|
11 | 11 | import { json } from "@remix-run/server-runtime"; |
12 | | -import { type RbacAbility, scopesWithinAbility, type UserActorClaims } from "@trigger.dev/rbac"; |
| 12 | +import { |
| 13 | + buildJwtAbility, |
| 14 | + type RbacAbility, |
| 15 | + scopesWithinAbility, |
| 16 | + type UserActorClaims, |
| 17 | +} from "@trigger.dev/rbac"; |
13 | 18 | import { $replica } from "~/db.server"; |
14 | 19 |
|
15 | 20 | export const FORBIDDEN_ENVIRONMENT_CODE = "forbidden_environment"; |
@@ -112,20 +117,28 @@ export async function resolveUserActorEnvironmentScope( |
112 | 117 | /** Mirrors the RBAC fallback's own default. */ |
113 | 118 | const CAPLESS_USER_ACTOR_SCOPES = ["read:all"]; |
114 | 119 |
|
115 | | -/** A delegated token must never mint something more capable than itself, so it is the ceiling. */ |
| 120 | +/** |
| 121 | + * A delegated token must never mint something more capable than itself. Two ceilings apply: |
| 122 | + * the actor's own ability (their role) and the token's `cap`. The role alone is not enough — |
| 123 | + * a read-only agent token belongs to a user who may well be allowed to write. |
| 124 | + */ |
116 | 125 | export function clampUserActorScopes( |
117 | 126 | requestedScopes: string[] | undefined, |
118 | 127 | userActor: UserActorClaims, |
119 | 128 | ability: RbacAbility |
120 | 129 | ): { scopes: string[]; deniedScopes: string[] } { |
121 | | - const requested = |
122 | | - requestedScopes && requestedScopes.length > 0 |
123 | | - ? requestedScopes |
124 | | - : (userActor.cap ?? CAPLESS_USER_ACTOR_SCOPES); |
| 130 | + const cap = userActor.cap ?? CAPLESS_USER_ACTOR_SCOPES; |
| 131 | + const requested = requestedScopes && requestedScopes.length > 0 ? requestedScopes : cap; |
125 | 132 |
|
126 | | - const { deniedScopes } = scopesWithinAbility(requested, ability); |
| 133 | + const denied = new Set([ |
| 134 | + ...scopesWithinAbility(requested, ability).deniedScopes, |
| 135 | + ...scopesWithinAbility(requested, buildJwtAbility(cap)).deniedScopes, |
| 136 | + ]); |
127 | 137 |
|
128 | | - return { scopes: requested.filter((scope) => !deniedScopes.includes(scope)), deniedScopes }; |
| 138 | + return { |
| 139 | + scopes: requested.filter((scope) => !denied.has(scope)), |
| 140 | + deniedScopes: [...denied], |
| 141 | + }; |
129 | 142 | } |
130 | 143 |
|
131 | 144 | function assertClaimIsOptional(userActor: UserActorClaims): void { |
|
0 commit comments