@checkstack/backend-api 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/package.json +1 -1
- package/src/assertions.test.ts +128 -0
- package/src/assertions.ts +77 -2
- package/src/chart-metadata.ts +1 -24
- package/src/hooks.ts +6 -6
- package/src/notification-strategy.ts +5 -5
- package/src/plugin-admin-contract.ts +13 -15
- package/src/plugin-system.ts +6 -3
- package/src/rpc.test.ts +530 -0
- package/src/rpc.ts +165 -150
- package/src/test-utils.ts +1 -1
- package/src/types.ts +11 -11
package/src/rpc.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { CollectorRegistry } from "./collector-registry";
|
|
|
5
5
|
import { QueuePluginRegistry, QueueManager } from "@checkstack/queue-api";
|
|
6
6
|
import {
|
|
7
7
|
ProcedureMetadata,
|
|
8
|
-
|
|
8
|
+
qualifyAccessRuleId,
|
|
9
9
|
qualifyResourceType,
|
|
10
10
|
} from "@checkstack/common";
|
|
11
11
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
@@ -77,12 +77,18 @@ export type { ProcedureMetadata } from "@checkstack/common";
|
|
|
77
77
|
*
|
|
78
78
|
* Automatically enforces based on contract metadata:
|
|
79
79
|
* 1. User type (from meta.userType):
|
|
80
|
-
* - "anonymous": No authentication required, no
|
|
81
|
-
* - "public": Anyone can attempt,
|
|
80
|
+
* - "anonymous": No authentication required, no access checks
|
|
81
|
+
* - "public": Anyone can attempt, access checked based on user type
|
|
82
82
|
* - "user": Only real users (frontend authenticated)
|
|
83
83
|
* - "service": Only services (backend-to-backend)
|
|
84
84
|
* - "authenticated": Either users or services, but must be authenticated (default)
|
|
85
|
-
* 2.
|
|
85
|
+
* 2. Access rules (from meta.access): unified access rules + resource-level access control
|
|
86
|
+
*
|
|
87
|
+
* Access Control Logic:
|
|
88
|
+
* - Rules WITHOUT instanceAccess: require global access
|
|
89
|
+
* - Rules WITH instanceAccess: S2S call to auth-backend decides based on:
|
|
90
|
+
* - Global access OR team grants (when resource is NOT teamOnly)
|
|
91
|
+
* - Team grants only (when resource IS teamOnly)
|
|
86
92
|
*
|
|
87
93
|
* Use this in backend routers: `implement(contract).$context<RpcContext>().use(autoAuthMiddleware)`
|
|
88
94
|
*/
|
|
@@ -90,65 +96,82 @@ export const autoAuthMiddleware = os.middleware(
|
|
|
90
96
|
async ({ next, context, procedure }, input: unknown) => {
|
|
91
97
|
const meta = procedure["~orpc"]?.meta as ProcedureMetadata | undefined;
|
|
92
98
|
const requiredUserType = meta?.userType || "authenticated";
|
|
93
|
-
const
|
|
94
|
-
const resourceAccessConfigs = meta?.resourceAccess || [];
|
|
99
|
+
const accessRules = meta?.access || [];
|
|
95
100
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
// Build qualified access rule IDs for each access rule
|
|
102
|
+
const qualifiedRules = accessRules.map((rule) => ({
|
|
103
|
+
...rule,
|
|
104
|
+
qualifiedId: qualifyAccessRuleId(context.pluginMetadata, rule),
|
|
105
|
+
qualifiedResourceType: qualifyResourceType(
|
|
106
|
+
context.pluginMetadata.pluginId,
|
|
107
|
+
rule.resource
|
|
108
|
+
),
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
// Separate rules by type
|
|
112
|
+
const globalOnlyRules = qualifiedRules.filter((r) => !r.instanceAccess);
|
|
113
|
+
const instanceRules = qualifiedRules.filter((r) => r.instanceAccess);
|
|
114
|
+
const singleResourceRules = instanceRules.filter(
|
|
115
|
+
(r) => r.instanceAccess?.idParam
|
|
116
|
+
);
|
|
117
|
+
const listResourceRules = instanceRules.filter(
|
|
118
|
+
(r) => r.instanceAccess?.listKey
|
|
100
119
|
);
|
|
101
120
|
|
|
102
|
-
// 1. Handle anonymous endpoints - no auth required, no
|
|
121
|
+
// 1. Handle anonymous endpoints - no auth required, no access checks
|
|
103
122
|
if (requiredUserType === "anonymous") {
|
|
104
123
|
return next({});
|
|
105
124
|
}
|
|
106
125
|
|
|
107
|
-
// 2. Handle public endpoints - anyone can attempt
|
|
126
|
+
// 2. Handle public endpoints - anyone can attempt
|
|
108
127
|
if (requiredUserType === "public") {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
" or "
|
|
126
|
-
)}`,
|
|
127
|
-
});
|
|
128
|
+
const user = context.user;
|
|
129
|
+
|
|
130
|
+
// Check global-only rules based on user status
|
|
131
|
+
if (globalOnlyRules.length > 0) {
|
|
132
|
+
if (user && (user.type === "user" || user.type === "application")) {
|
|
133
|
+
// Authenticated user - check their global accesss
|
|
134
|
+
const userAccessRules = user.accessRules || [];
|
|
135
|
+
for (const rule of globalOnlyRules) {
|
|
136
|
+
const hasAccess =
|
|
137
|
+
userAccessRules.includes("*") ||
|
|
138
|
+
userAccessRules.includes(rule.qualifiedId);
|
|
139
|
+
if (!hasAccess) {
|
|
140
|
+
throw new ORPCError("FORBIDDEN", {
|
|
141
|
+
message: `Missing access: ${rule.qualifiedId}`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
128
144
|
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
throw new ORPCError("FORBIDDEN", {
|
|
142
|
-
message: `Anonymous access not permitted for this resource`,
|
|
143
|
-
});
|
|
145
|
+
} else if (user && user.type === "service") {
|
|
146
|
+
// Services are trusted
|
|
147
|
+
} else {
|
|
148
|
+
// Anonymous - check anonymous role
|
|
149
|
+
const anonymousAccessRules =
|
|
150
|
+
await context.auth.getAnonymousAccessRules();
|
|
151
|
+
for (const rule of globalOnlyRules) {
|
|
152
|
+
if (!anonymousAccessRules.includes(rule.qualifiedId)) {
|
|
153
|
+
throw new ORPCError("FORBIDDEN", {
|
|
154
|
+
message: `Anonymous access not permitted for this resource`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
144
157
|
}
|
|
145
158
|
}
|
|
146
159
|
}
|
|
147
|
-
|
|
160
|
+
|
|
161
|
+
// For rules WITH instanceAccess on public endpoints:
|
|
162
|
+
// - If user is authenticated, check via S2S (step 6)
|
|
163
|
+
// - If anonymous, they get empty results from list filtering
|
|
164
|
+
// (since they have no team grants - S2S will filter everything)
|
|
165
|
+
|
|
166
|
+
// If there are no instance rules, we can return early for public endpoints
|
|
167
|
+
if (instanceRules.length === 0) {
|
|
168
|
+
return next({});
|
|
169
|
+
}
|
|
170
|
+
// Otherwise, fall through to step 6 for instance-level checks
|
|
148
171
|
}
|
|
149
172
|
|
|
150
173
|
// 3. Enforce authentication for user/service/authenticated types
|
|
151
|
-
if (!context.user) {
|
|
174
|
+
if (requiredUserType !== "public" && !context.user) {
|
|
152
175
|
throw new ORPCError("UNAUTHORIZED", {
|
|
153
176
|
message: "Authentication required",
|
|
154
177
|
});
|
|
@@ -157,84 +180,74 @@ export const autoAuthMiddleware = os.middleware(
|
|
|
157
180
|
const user = context.user;
|
|
158
181
|
|
|
159
182
|
// 4. Enforce user type
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
message: "This endpoint is for services only",
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// 5. Enforce permissions (for real users and applications)
|
|
172
|
-
if (
|
|
173
|
-
(user.type === "user" || user.type === "application") &&
|
|
174
|
-
requiredPermissions.length > 0
|
|
175
|
-
) {
|
|
176
|
-
const userPermissions = user.permissions || [];
|
|
177
|
-
const hasPermission = requiredPermissions.some(
|
|
178
|
-
(p: string) =>
|
|
179
|
-
userPermissions.includes("*") || userPermissions.includes(p)
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
if (!hasPermission) {
|
|
183
|
+
if (user) {
|
|
184
|
+
if (requiredUserType === "user" && user.type !== "user") {
|
|
185
|
+
throw new ORPCError("FORBIDDEN", {
|
|
186
|
+
message: "This endpoint is for users only",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (requiredUserType === "service" && user.type !== "service") {
|
|
183
190
|
throw new ORPCError("FORBIDDEN", {
|
|
184
|
-
message:
|
|
191
|
+
message: "This endpoint is for services only",
|
|
185
192
|
});
|
|
186
193
|
}
|
|
187
194
|
}
|
|
188
195
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
if (resourceAccessConfigs.length === 0 || user.type === "service") {
|
|
196
|
+
// 5. Skip remaining checks for services - they are trusted
|
|
197
|
+
if (user?.type === "service") {
|
|
192
198
|
return next({});
|
|
193
199
|
}
|
|
194
200
|
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
201
|
+
// 6. Check access rules (for real users, applications, and anonymous on public endpoints)
|
|
202
|
+
const userId = user?.id;
|
|
203
|
+
const userType = user?.type as "user" | "application" | undefined;
|
|
204
|
+
const userAccessRules = user?.accessRules || [];
|
|
205
|
+
|
|
206
|
+
// Check global-only rules (for non-public endpoints - public already handled above)
|
|
207
|
+
if (requiredUserType !== "public") {
|
|
208
|
+
for (const rule of globalOnlyRules) {
|
|
209
|
+
const hasAccess =
|
|
210
|
+
userAccessRules.includes("*") ||
|
|
211
|
+
userAccessRules.includes(rule.qualifiedId);
|
|
212
|
+
if (!hasAccess) {
|
|
213
|
+
throw new ORPCError("FORBIDDEN", {
|
|
214
|
+
message: `Missing access: ${rule.qualifiedId}`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
213
219
|
|
|
214
|
-
// Pre-check: Single resource access
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const resourceId = getNestedValue(input,
|
|
220
|
+
// Pre-check: Single resource access rules
|
|
221
|
+
// For these, user MUST have either global access OR team grant
|
|
222
|
+
for (const rule of singleResourceRules) {
|
|
223
|
+
const resourceId = getNestedValue(input, rule.instanceAccess!.idParam!);
|
|
218
224
|
if (!resourceId) continue;
|
|
219
225
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
// If no user (anonymous on public endpoint), deny access to single resources
|
|
227
|
+
// (they can't have team grants)
|
|
228
|
+
if (!userId || !userType) {
|
|
229
|
+
throw new ORPCError("FORBIDDEN", {
|
|
230
|
+
message: `Authentication required to access ${rule.resource}:${resourceId}`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const hasGlobalAccess =
|
|
235
|
+
userAccessRules.includes("*") ||
|
|
236
|
+
userAccessRules.includes(rule.qualifiedId);
|
|
224
237
|
|
|
225
238
|
const hasAccess = await checkResourceAccessViaS2S({
|
|
226
239
|
auth: context.auth,
|
|
227
240
|
userId,
|
|
228
241
|
userType,
|
|
229
|
-
resourceType:
|
|
242
|
+
resourceType: rule.qualifiedResourceType,
|
|
230
243
|
resourceId,
|
|
231
|
-
action,
|
|
232
|
-
|
|
244
|
+
action: rule.level,
|
|
245
|
+
hasGlobalAccess,
|
|
233
246
|
});
|
|
234
247
|
|
|
235
248
|
if (!hasAccess) {
|
|
236
249
|
throw new ORPCError("FORBIDDEN", {
|
|
237
|
-
message: `Access denied to resource ${
|
|
250
|
+
message: `Access denied to resource ${rule.resource}:${resourceId}`,
|
|
238
251
|
});
|
|
239
252
|
}
|
|
240
253
|
}
|
|
@@ -243,28 +256,21 @@ export const autoAuthMiddleware = os.middleware(
|
|
|
243
256
|
const result = await next({});
|
|
244
257
|
|
|
245
258
|
// Post-filter: List endpoints
|
|
259
|
+
// For these, return only resources user has access to (via global perm OR team grant)
|
|
246
260
|
if (
|
|
247
|
-
|
|
261
|
+
listResourceRules.length > 0 &&
|
|
248
262
|
result.output &&
|
|
249
263
|
typeof result.output === "object"
|
|
250
264
|
) {
|
|
251
265
|
const mutableOutput = result.output as Record<string, unknown>;
|
|
252
266
|
|
|
253
|
-
for (const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
`resourceAccess: filterMode "list" requires outputKey`
|
|
257
|
-
);
|
|
258
|
-
throw new ORPCError("INTERNAL_SERVER_ERROR", {
|
|
259
|
-
message: "Invalid resource access configuration",
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const items = mutableOutput[config.outputKey];
|
|
267
|
+
for (const rule of listResourceRules) {
|
|
268
|
+
const outputKey = rule.instanceAccess!.listKey!;
|
|
269
|
+
const items = mutableOutput[outputKey];
|
|
264
270
|
|
|
265
271
|
if (items === undefined) {
|
|
266
272
|
context.logger.error(
|
|
267
|
-
`resourceAccess: expected "${
|
|
273
|
+
`resourceAccess: expected "${outputKey}" in response but not found`
|
|
268
274
|
);
|
|
269
275
|
throw new ORPCError("INTERNAL_SERVER_ERROR", {
|
|
270
276
|
message: "Invalid response shape for filtered endpoint",
|
|
@@ -273,34 +279,52 @@ export const autoAuthMiddleware = os.middleware(
|
|
|
273
279
|
|
|
274
280
|
if (!Array.isArray(items)) {
|
|
275
281
|
context.logger.error(
|
|
276
|
-
`resourceAccess: "${
|
|
282
|
+
`resourceAccess: "${outputKey}" must be an array`
|
|
277
283
|
);
|
|
278
284
|
throw new ORPCError("INTERNAL_SERVER_ERROR", {
|
|
279
285
|
message: "Invalid response shape for filtered endpoint",
|
|
280
286
|
});
|
|
281
287
|
}
|
|
282
288
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
289
|
+
// If no user (anonymous), check if they have global access via anonymous role
|
|
290
|
+
if (!userId || !userType) {
|
|
291
|
+
// Anonymous users can't have team grants, but may have global access
|
|
292
|
+
const anonymousAccessRules =
|
|
293
|
+
await context.auth.getAnonymousAccessRules();
|
|
294
|
+
const hasGlobalAccess =
|
|
295
|
+
anonymousAccessRules.includes("*") ||
|
|
296
|
+
anonymousAccessRules.includes(rule.qualifiedId);
|
|
297
|
+
|
|
298
|
+
if (hasGlobalAccess) {
|
|
299
|
+
// Anonymous user has global access - return all items
|
|
300
|
+
continue;
|
|
301
|
+
} else {
|
|
302
|
+
// No global access and can't have team grants - return empty
|
|
303
|
+
mutableOutput[outputKey] = [];
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
287
307
|
|
|
288
308
|
const resourceIds = items
|
|
289
309
|
.map((item) => (item as { id?: string }).id)
|
|
290
310
|
.filter((id): id is string => typeof id === "string");
|
|
291
311
|
|
|
312
|
+
const hasGlobalAccess =
|
|
313
|
+
userAccessRules.includes("*") ||
|
|
314
|
+
userAccessRules.includes(rule.qualifiedId);
|
|
315
|
+
|
|
292
316
|
const accessibleIds = await getAccessibleResourceIdsViaS2S({
|
|
293
317
|
auth: context.auth,
|
|
294
318
|
userId,
|
|
295
319
|
userType,
|
|
296
|
-
resourceType:
|
|
320
|
+
resourceType: rule.qualifiedResourceType,
|
|
297
321
|
resourceIds,
|
|
298
|
-
action,
|
|
299
|
-
|
|
322
|
+
action: rule.level,
|
|
323
|
+
hasGlobalAccess,
|
|
300
324
|
});
|
|
301
325
|
|
|
302
326
|
const accessibleSet = new Set(accessibleIds);
|
|
303
|
-
mutableOutput[
|
|
327
|
+
mutableOutput[outputKey] = items.filter((item) => {
|
|
304
328
|
const id = (item as { id?: string }).id;
|
|
305
329
|
return id && accessibleSet.has(id);
|
|
306
330
|
});
|
|
@@ -325,15 +349,6 @@ function getNestedValue(obj: unknown, path: string): string | undefined {
|
|
|
325
349
|
return typeof current === "string" ? current : undefined;
|
|
326
350
|
}
|
|
327
351
|
|
|
328
|
-
/**
|
|
329
|
-
* Determine action from permission suffix (read vs manage).
|
|
330
|
-
*/
|
|
331
|
-
function inferActionFromPermissions(permissions: string[]): "read" | "manage" {
|
|
332
|
-
const perm = permissions[0] || "";
|
|
333
|
-
if (perm.endsWith(".manage") || perm.endsWith("Manage")) return "manage";
|
|
334
|
-
return "read";
|
|
335
|
-
}
|
|
336
|
-
|
|
337
352
|
/**
|
|
338
353
|
* Check resource access via auth service S2S endpoint.
|
|
339
354
|
*/
|
|
@@ -344,7 +359,7 @@ async function checkResourceAccessViaS2S({
|
|
|
344
359
|
resourceType,
|
|
345
360
|
resourceId,
|
|
346
361
|
action,
|
|
347
|
-
|
|
362
|
+
hasGlobalAccess,
|
|
348
363
|
}: {
|
|
349
364
|
auth: AuthService;
|
|
350
365
|
userId: string;
|
|
@@ -352,7 +367,7 @@ async function checkResourceAccessViaS2S({
|
|
|
352
367
|
resourceType: string;
|
|
353
368
|
resourceId: string;
|
|
354
369
|
action: "read" | "manage";
|
|
355
|
-
|
|
370
|
+
hasGlobalAccess: boolean;
|
|
356
371
|
}): Promise<boolean> {
|
|
357
372
|
try {
|
|
358
373
|
const result = await auth.checkResourceTeamAccess({
|
|
@@ -361,12 +376,12 @@ async function checkResourceAccessViaS2S({
|
|
|
361
376
|
resourceType,
|
|
362
377
|
resourceId,
|
|
363
378
|
action,
|
|
364
|
-
|
|
379
|
+
hasGlobalAccess,
|
|
365
380
|
});
|
|
366
381
|
return result.hasAccess;
|
|
367
382
|
} catch {
|
|
368
|
-
// If team access check fails (e.g., service not available), fall back to global
|
|
369
|
-
return
|
|
383
|
+
// If team access check fails (e.g., service not available), fall back to global access
|
|
384
|
+
return hasGlobalAccess;
|
|
370
385
|
}
|
|
371
386
|
}
|
|
372
387
|
|
|
@@ -380,7 +395,7 @@ async function getAccessibleResourceIdsViaS2S({
|
|
|
380
395
|
resourceType,
|
|
381
396
|
resourceIds,
|
|
382
397
|
action,
|
|
383
|
-
|
|
398
|
+
hasGlobalAccess,
|
|
384
399
|
}: {
|
|
385
400
|
auth: AuthService;
|
|
386
401
|
userId: string;
|
|
@@ -388,7 +403,7 @@ async function getAccessibleResourceIdsViaS2S({
|
|
|
388
403
|
resourceType: string;
|
|
389
404
|
resourceIds: string[];
|
|
390
405
|
action: "read" | "manage";
|
|
391
|
-
|
|
406
|
+
hasGlobalAccess: boolean;
|
|
392
407
|
}): Promise<string[]> {
|
|
393
408
|
if (resourceIds.length === 0) return [];
|
|
394
409
|
|
|
@@ -399,11 +414,11 @@ async function getAccessibleResourceIdsViaS2S({
|
|
|
399
414
|
resourceType,
|
|
400
415
|
resourceIds,
|
|
401
416
|
action,
|
|
402
|
-
|
|
417
|
+
hasGlobalAccess,
|
|
403
418
|
});
|
|
404
419
|
} catch {
|
|
405
|
-
// If team access check fails, fall back to global
|
|
406
|
-
return
|
|
420
|
+
// If team access check fails, fall back to global access behavior
|
|
421
|
+
return hasGlobalAccess ? resourceIds : [];
|
|
407
422
|
}
|
|
408
423
|
}
|
|
409
424
|
|
|
@@ -417,16 +432,16 @@ async function getAccessibleResourceIdsViaS2S({
|
|
|
417
432
|
* All plugin contracts should use this builder. It ensures that:
|
|
418
433
|
* 1. All procedures are authenticated by default
|
|
419
434
|
* 2. User type is enforced based on meta.userType
|
|
420
|
-
* 3.
|
|
435
|
+
* 3. Access rules are enforced based on meta.access
|
|
421
436
|
*
|
|
422
437
|
* @example
|
|
423
438
|
* import { baseContractBuilder } from "@checkstack/backend-api";
|
|
424
|
-
* import {
|
|
439
|
+
* import { access } from "./access";
|
|
425
440
|
*
|
|
426
441
|
* const myContract = {
|
|
427
|
-
* // User-only endpoint with specific
|
|
442
|
+
* // User-only endpoint with specific access rule
|
|
428
443
|
* getItems: baseContractBuilder
|
|
429
|
-
* .meta({ userType: "user",
|
|
444
|
+
* .meta({ userType: "user", accessRules: [access.myPluginRead.id] })
|
|
430
445
|
* .output(z.array(ItemSchema)),
|
|
431
446
|
*
|
|
432
447
|
* // Service-only endpoint (backend-to-backend)
|
package/src/test-utils.ts
CHANGED
|
@@ -34,7 +34,7 @@ export function createMockRpcContext(
|
|
|
34
34
|
auth: {
|
|
35
35
|
authenticate: mock(),
|
|
36
36
|
getCredentials: mock().mockResolvedValue({ headers: {} }),
|
|
37
|
-
|
|
37
|
+
getAnonymousAccessRules: mock().mockResolvedValue([]),
|
|
38
38
|
checkResourceTeamAccess: mock().mockResolvedValue({ hasAccess: true }),
|
|
39
39
|
getAccessibleResourceIds: mock().mockImplementation(
|
|
40
40
|
(params: { resourceIds: string[] }) =>
|
package/src/types.ts
CHANGED
|
@@ -22,14 +22,14 @@ export interface Fetch {
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Real user authenticated via session/token (human users).
|
|
25
|
-
* Has
|
|
25
|
+
* Has access rules and roles from the RBAC system.
|
|
26
26
|
*/
|
|
27
27
|
export interface RealUser {
|
|
28
28
|
type: "user";
|
|
29
29
|
id: string;
|
|
30
30
|
email?: string;
|
|
31
31
|
name?: string;
|
|
32
|
-
|
|
32
|
+
accessRules?: string[];
|
|
33
33
|
roles?: string[];
|
|
34
34
|
teamIds?: string[];
|
|
35
35
|
[key: string]: unknown;
|
|
@@ -37,7 +37,7 @@ export interface RealUser {
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Service user for backend-to-backend calls.
|
|
40
|
-
* Trusted implicitly - no
|
|
40
|
+
* Trusted implicitly - no accesss/roles needed.
|
|
41
41
|
*/
|
|
42
42
|
export interface ServiceUser {
|
|
43
43
|
type: "service";
|
|
@@ -46,13 +46,13 @@ export interface ServiceUser {
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* External application authenticated via API key.
|
|
49
|
-
* Has
|
|
49
|
+
* Has access rules and roles from the RBAC system like RealUser.
|
|
50
50
|
*/
|
|
51
51
|
export interface ApplicationUser {
|
|
52
52
|
type: "application";
|
|
53
53
|
id: string;
|
|
54
54
|
name: string;
|
|
55
|
-
|
|
55
|
+
accessRules?: string[];
|
|
56
56
|
roles?: string[];
|
|
57
57
|
teamIds?: string[];
|
|
58
58
|
}
|
|
@@ -67,11 +67,11 @@ export interface AuthService {
|
|
|
67
67
|
authenticate(request: Request): Promise<AuthUser | undefined>;
|
|
68
68
|
getCredentials(): Promise<{ headers: Record<string, string> }>;
|
|
69
69
|
/**
|
|
70
|
-
* Get
|
|
71
|
-
* Used by autoAuthMiddleware to check
|
|
70
|
+
* Get access rules assigned to the anonymous role.
|
|
71
|
+
* Used by autoAuthMiddleware to check accesss for unauthenticated
|
|
72
72
|
* users on "public" userType endpoints.
|
|
73
73
|
*/
|
|
74
|
-
|
|
74
|
+
getAnonymousAccessRules(): Promise<string[]>;
|
|
75
75
|
/**
|
|
76
76
|
* Check if a user has access to a specific resource via team grants.
|
|
77
77
|
*/
|
|
@@ -81,7 +81,7 @@ export interface AuthService {
|
|
|
81
81
|
resourceType: string;
|
|
82
82
|
resourceId: string;
|
|
83
83
|
action: "read" | "manage";
|
|
84
|
-
|
|
84
|
+
hasGlobalAccess: boolean;
|
|
85
85
|
}): Promise<{ hasAccess: boolean }>;
|
|
86
86
|
/**
|
|
87
87
|
* Get IDs of resources the user can access from a given list.
|
|
@@ -93,7 +93,7 @@ export interface AuthService {
|
|
|
93
93
|
resourceType: string;
|
|
94
94
|
resourceIds: string[];
|
|
95
95
|
action: "read" | "manage";
|
|
96
|
-
|
|
96
|
+
hasGlobalAccess: boolean;
|
|
97
97
|
}): Promise<string[]>;
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -113,7 +113,7 @@ export interface PluginInstaller {
|
|
|
113
113
|
* Options for declarative route definitions (Deprecated, will be replaced by oRPC procedures).
|
|
114
114
|
*/
|
|
115
115
|
export interface RouteOptions {
|
|
116
|
-
|
|
116
|
+
accessRule?: string | string[];
|
|
117
117
|
schema?: ZodSchema;
|
|
118
118
|
}
|
|
119
119
|
|