|
1 | 1 | import type { |
2 | 2 | Permission, |
3 | 3 | Role, |
| 4 | + RbacEnvironment, |
| 5 | + RbacUser, |
| 6 | + RbacSubject, |
| 7 | + RbacResource, |
| 8 | + BearerAuthResult, |
| 9 | + SessionAuthResult, |
4 | 10 | RoleBaseAccessController, |
5 | | - RoleBasedAccessControlPlugin, |
6 | 11 | } from "@trigger.dev/plugins"; |
| 12 | +import type { PrismaClient } from "@trigger.dev/database"; |
| 13 | +import { buildFallbackAbility, permissiveAbility } from "./ability.js"; |
7 | 14 |
|
8 | | -export class RoleBaseAccessFallback implements RoleBasedAccessControlPlugin { |
9 | | - async create() { |
10 | | - return new RoleBaseAccessFallbackController(); |
| 15 | +export class RoleBaseAccessFallback { |
| 16 | + constructor(private readonly prisma: PrismaClient) {} |
| 17 | + |
| 18 | + create( |
| 19 | + helpers: { getSessionUserId: (request: Request) => Promise<string | null> } |
| 20 | + ): RoleBaseAccessFallbackController { |
| 21 | + return new RoleBaseAccessFallbackController(this.prisma, helpers); |
11 | 22 | } |
12 | 23 | } |
13 | 24 |
|
14 | | -const accountWildcard: Permission = { |
15 | | - name: "*:account", |
16 | | - description: "Full abilities for an account", |
17 | | -}; |
| 25 | +class RoleBaseAccessFallbackController implements RoleBaseAccessController { |
| 26 | + constructor( |
| 27 | + private readonly prisma: PrismaClient, |
| 28 | + private readonly helpers: { getSessionUserId: (request: Request) => Promise<string | null> } |
| 29 | + ) {} |
| 30 | + |
| 31 | + async authenticateBearer(request: Request): Promise<BearerAuthResult> { |
| 32 | + const apiKey = request.headers.get("Authorization")?.replace(/^Bearer /, "").trim(); |
| 33 | + if (!apiKey) return { ok: false, status: 401, error: "Invalid or Missing API key" }; |
18 | 34 |
|
19 | | -const superWildcard: Permission = { |
20 | | - name: "*:super", |
21 | | - description: "Full abilities for a super user", |
22 | | -}; |
| 35 | + const env = await this.prisma.runtimeEnvironment.findFirst({ |
| 36 | + where: { apiKey }, |
| 37 | + include: { |
| 38 | + project: true, |
| 39 | + organization: true, |
| 40 | + orgMember: { select: { userId: true } }, |
| 41 | + }, |
| 42 | + }); |
23 | 43 |
|
24 | | -const owner: Role = { |
25 | | - name: "owner", |
26 | | - description: "Full access to all features", |
27 | | - permissions: [accountWildcard, superWildcard], |
28 | | -}; |
| 44 | + if (!env || env.project.deletedAt !== null) { |
| 45 | + return { ok: false, status: 401, error: "Invalid API key" }; |
| 46 | + } |
29 | 47 |
|
30 | | -const superAdmin: Role = { |
31 | | - name: "super_admin", |
32 | | - description: "Full access to all features and the ability to manage the Trigger.dev platform", |
33 | | - permissions: [accountWildcard, superWildcard], |
34 | | -}; |
| 48 | + const subject: RbacSubject = { |
| 49 | + type: "user", |
| 50 | + userId: env.orgMember?.userId ?? "", |
| 51 | + organizationId: env.organizationId, |
| 52 | + projectId: env.projectId, |
| 53 | + }; |
| 54 | + |
| 55 | + return { |
| 56 | + ok: true, |
| 57 | + environment: toRbacEnvironment(env), |
| 58 | + subject, |
| 59 | + ability: permissiveAbility, |
| 60 | + }; |
| 61 | + } |
| 62 | + |
| 63 | + async authenticateSession( |
| 64 | + request: Request, |
| 65 | + context: { organizationId?: string; projectId?: string } |
| 66 | + ): Promise<SessionAuthResult> { |
| 67 | + const userId = await this.helpers.getSessionUserId(request); |
| 68 | + if (!userId) return { ok: false, reason: "unauthenticated" }; |
| 69 | + |
| 70 | + const user = await this.prisma.user.findFirst({ where: { id: userId } }); |
| 71 | + if (!user) return { ok: false, reason: "unauthenticated" }; |
| 72 | + |
| 73 | + const subject: RbacSubject = { |
| 74 | + type: "user", |
| 75 | + userId: user.id, |
| 76 | + organizationId: context.organizationId ?? "", |
| 77 | + projectId: context.projectId, |
| 78 | + }; |
| 79 | + |
| 80 | + return { |
| 81 | + ok: true, |
| 82 | + user: toRbacUser(user), |
| 83 | + subject, |
| 84 | + ability: buildFallbackAbility(user.admin), |
| 85 | + }; |
| 86 | + } |
| 87 | + |
| 88 | + async authenticateAuthorizeBearer( |
| 89 | + request: Request, |
| 90 | + check: { action: string; resource: RbacResource } |
| 91 | + ): Promise<BearerAuthResult> { |
| 92 | + const auth = await this.authenticateBearer(request); |
| 93 | + if (!auth.ok) return auth; |
| 94 | + if (!auth.ability.can(check.action, check.resource)) { |
| 95 | + return { ok: false, status: 403, error: "Unauthorized" }; |
| 96 | + } |
| 97 | + return auth; |
| 98 | + } |
| 99 | + |
| 100 | + async authenticateAuthorizeSession( |
| 101 | + request: Request, |
| 102 | + context: { organizationId?: string; projectId?: string }, |
| 103 | + check: { action: string; resource: RbacResource } |
| 104 | + ): Promise<SessionAuthResult> { |
| 105 | + const auth = await this.authenticateSession(request, context); |
| 106 | + if (!auth.ok) return auth; |
| 107 | + if (!auth.ability.can(check.action, check.resource)) { |
| 108 | + return { ok: false, reason: "unauthorized" }; |
| 109 | + } |
| 110 | + return auth; |
| 111 | + } |
35 | 112 |
|
36 | | -class RoleBaseAccessFallbackController implements RoleBaseAccessController { |
37 | 113 | async allPermissions(): Promise<Permission[]> { |
38 | | - return [owner, superAdmin]; |
| 114 | + return []; |
39 | 115 | } |
40 | 116 |
|
41 | 117 | async allRoles(): Promise<Role[]> { |
42 | | - return [owner, superAdmin]; |
| 118 | + return []; |
43 | 119 | } |
| 120 | + |
| 121 | + async createRole(): Promise<Role> { |
| 122 | + throw new Error("RBAC plugin not installed"); |
| 123 | + } |
| 124 | + |
| 125 | + async updateRole(): Promise<Role> { |
| 126 | + throw new Error("RBAC plugin not installed"); |
| 127 | + } |
| 128 | + |
| 129 | + async deleteRole(): Promise<void> {} |
| 130 | + |
| 131 | + async getUserRole(): Promise<Role | null> { |
| 132 | + return null; |
| 133 | + } |
| 134 | + |
| 135 | + async setUserRole(): Promise<void> {} |
| 136 | + async removeUserRole(): Promise<void> {} |
| 137 | + |
| 138 | + async getTokenRole(): Promise<Role | null> { |
| 139 | + return null; |
| 140 | + } |
| 141 | + |
| 142 | + async setTokenRole(): Promise<void> {} |
| 143 | + async removeTokenRole(): Promise<void> {} |
| 144 | +} |
| 145 | + |
| 146 | +function toRbacEnvironment( |
| 147 | + env: { |
| 148 | + id: string; |
| 149 | + slug: string; |
| 150 | + type: string; |
| 151 | + apiKey: string; |
| 152 | + pkApiKey: string; |
| 153 | + organizationId: string; |
| 154 | + projectId: string; |
| 155 | + organization: { id: string; slug: string; title: string }; |
| 156 | + project: { id: string; slug: string; name: string; externalRef: string }; |
| 157 | + } |
| 158 | +): RbacEnvironment { |
| 159 | + return { |
| 160 | + id: env.id, |
| 161 | + slug: env.slug, |
| 162 | + type: env.type, |
| 163 | + apiKey: env.apiKey, |
| 164 | + pkApiKey: env.pkApiKey, |
| 165 | + organizationId: env.organizationId, |
| 166 | + projectId: env.projectId, |
| 167 | + organization: { |
| 168 | + id: env.organization.id, |
| 169 | + slug: env.organization.slug, |
| 170 | + title: env.organization.title, |
| 171 | + }, |
| 172 | + project: { |
| 173 | + id: env.project.id, |
| 174 | + slug: env.project.slug, |
| 175 | + name: env.project.name, |
| 176 | + externalRef: env.project.externalRef, |
| 177 | + }, |
| 178 | + }; |
| 179 | +} |
| 180 | + |
| 181 | +function toRbacUser(user: { |
| 182 | + id: string; |
| 183 | + email: string; |
| 184 | + name: string | null; |
| 185 | + displayName: string | null; |
| 186 | + avatarUrl: string | null; |
| 187 | + admin: boolean; |
| 188 | + confirmedBasicDetails: boolean; |
| 189 | +}): RbacUser { |
| 190 | + return { |
| 191 | + id: user.id, |
| 192 | + email: user.email, |
| 193 | + name: user.name, |
| 194 | + displayName: user.displayName, |
| 195 | + avatarUrl: user.avatarUrl, |
| 196 | + admin: user.admin, |
| 197 | + confirmedBasicDetails: user.confirmedBasicDetails, |
| 198 | + isImpersonating: false, |
| 199 | + }; |
44 | 200 | } |
0 commit comments