@classytic/arc 1.0.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/LICENSE +21 -0
- package/README.md +900 -0
- package/bin/arc.js +344 -0
- package/dist/adapters/index.d.ts +237 -0
- package/dist/adapters/index.js +668 -0
- package/dist/arcCorePlugin-DTPWXcZN.d.ts +273 -0
- package/dist/audit/index.d.ts +195 -0
- package/dist/audit/index.js +319 -0
- package/dist/auth/index.d.ts +47 -0
- package/dist/auth/index.js +174 -0
- package/dist/cli/commands/docs.d.ts +11 -0
- package/dist/cli/commands/docs.js +474 -0
- package/dist/cli/commands/introspect.d.ts +8 -0
- package/dist/cli/commands/introspect.js +338 -0
- package/dist/cli/index.d.ts +43 -0
- package/dist/cli/index.js +520 -0
- package/dist/createApp-pzUAkzbz.d.ts +77 -0
- package/dist/docs/index.d.ts +166 -0
- package/dist/docs/index.js +650 -0
- package/dist/errors-8WIxGS_6.d.ts +122 -0
- package/dist/events/index.d.ts +117 -0
- package/dist/events/index.js +89 -0
- package/dist/factory/index.d.ts +38 -0
- package/dist/factory/index.js +1664 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +199 -0
- package/dist/idempotency/index.d.ts +323 -0
- package/dist/idempotency/index.js +500 -0
- package/dist/index-DkAW8BXh.d.ts +1302 -0
- package/dist/index.d.ts +331 -0
- package/dist/index.js +4734 -0
- package/dist/migrations/index.d.ts +185 -0
- package/dist/migrations/index.js +274 -0
- package/dist/org/index.d.ts +129 -0
- package/dist/org/index.js +220 -0
- package/dist/permissions/index.d.ts +144 -0
- package/dist/permissions/index.js +100 -0
- package/dist/plugins/index.d.ts +46 -0
- package/dist/plugins/index.js +1069 -0
- package/dist/policies/index.d.ts +398 -0
- package/dist/policies/index.js +196 -0
- package/dist/presets/index.d.ts +336 -0
- package/dist/presets/index.js +382 -0
- package/dist/presets/multiTenant.d.ts +39 -0
- package/dist/presets/multiTenant.js +112 -0
- package/dist/registry/index.d.ts +16 -0
- package/dist/registry/index.js +253 -0
- package/dist/testing/index.d.ts +618 -0
- package/dist/testing/index.js +48032 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +8 -0
- package/dist/types-0IPhH_NR.d.ts +143 -0
- package/dist/types-B99TBmFV.d.ts +76 -0
- package/dist/utils/index.d.ts +655 -0
- package/dist/utils/index.js +905 -0
- package/package.json +227 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { FastifyRequest, FastifyReply } from 'fastify';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Policy Interface
|
|
5
|
+
*
|
|
6
|
+
* Pluggable authorization interface for Arc.
|
|
7
|
+
* Apps implement this interface to define custom authorization strategies.
|
|
8
|
+
*
|
|
9
|
+
* @example RBAC Policy
|
|
10
|
+
* ```typescript
|
|
11
|
+
* class RBACPolicy implements PolicyEngine {
|
|
12
|
+
* can(user, operation, context) {
|
|
13
|
+
* return {
|
|
14
|
+
* allowed: user.roles.includes('admin'),
|
|
15
|
+
* reason: 'Admin role required',
|
|
16
|
+
* };
|
|
17
|
+
* }
|
|
18
|
+
* toMiddleware(operation) {
|
|
19
|
+
* return async (request, reply) => {
|
|
20
|
+
* const result = await this.can(request.user, operation);
|
|
21
|
+
* if (!result.allowed) {
|
|
22
|
+
* reply.code(403).send({ error: result.reason });
|
|
23
|
+
* }
|
|
24
|
+
* };
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @example ABAC (Attribute-Based) Policy
|
|
30
|
+
* ```typescript
|
|
31
|
+
* class ABACPolicy implements PolicyEngine {
|
|
32
|
+
* can(user, operation, context) {
|
|
33
|
+
* return {
|
|
34
|
+
* allowed: this.evaluateAttributes(user, operation, context),
|
|
35
|
+
* filters: { department: user.department },
|
|
36
|
+
* fieldMask: { exclude: ['salary', 'ssn'] },
|
|
37
|
+
* };
|
|
38
|
+
* }
|
|
39
|
+
* // ...
|
|
40
|
+
* }
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Policy result returned by can() method
|
|
46
|
+
*/
|
|
47
|
+
interface PolicyResult {
|
|
48
|
+
/**
|
|
49
|
+
* Whether the operation is allowed
|
|
50
|
+
*/
|
|
51
|
+
allowed: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Human-readable reason if denied
|
|
54
|
+
* Returned in 403 error responses
|
|
55
|
+
*/
|
|
56
|
+
reason?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Query filters to apply (for list operations)
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* // Multi-tenant filter
|
|
63
|
+
* { organizationId: user.organizationId }
|
|
64
|
+
*
|
|
65
|
+
* // Ownership filter
|
|
66
|
+
* { userId: user.id }
|
|
67
|
+
*
|
|
68
|
+
* // Complex filter
|
|
69
|
+
* { $or: [{ public: true }, { createdBy: user.id }] }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
filters?: Record<string, any>;
|
|
73
|
+
/**
|
|
74
|
+
* Fields to include/exclude in response
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* // Hide sensitive fields from non-admins
|
|
79
|
+
* { exclude: ['password', 'ssn', 'salary'] }
|
|
80
|
+
*
|
|
81
|
+
* // Only show specific fields
|
|
82
|
+
* { include: ['name', 'email', 'role'] }
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
fieldMask?: {
|
|
86
|
+
include?: string[];
|
|
87
|
+
exclude?: string[];
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Additional context for downstream middleware
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* {
|
|
95
|
+
* auditLog: { action: 'read', resource: 'patient', userId: user.id },
|
|
96
|
+
* rateLimit: { tier: user.subscriptionTier },
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
metadata?: Record<string, any>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Policy context provided to can() method
|
|
104
|
+
*/
|
|
105
|
+
interface PolicyContext {
|
|
106
|
+
/**
|
|
107
|
+
* The document being accessed (for update/delete/get)
|
|
108
|
+
* Populated by fetchDocument middleware
|
|
109
|
+
*/
|
|
110
|
+
document?: any;
|
|
111
|
+
/**
|
|
112
|
+
* Request body (for create/update)
|
|
113
|
+
*/
|
|
114
|
+
body?: any;
|
|
115
|
+
/**
|
|
116
|
+
* Request params (e.g., :id from route)
|
|
117
|
+
*/
|
|
118
|
+
params?: any;
|
|
119
|
+
/**
|
|
120
|
+
* Request query parameters
|
|
121
|
+
*/
|
|
122
|
+
query?: any;
|
|
123
|
+
/**
|
|
124
|
+
* Additional app-specific context
|
|
125
|
+
* Can include anything your policy needs to make decisions
|
|
126
|
+
*/
|
|
127
|
+
[key: string]: any;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Policy Engine Interface
|
|
131
|
+
*
|
|
132
|
+
* Implement this interface to create your own authorization strategy.
|
|
133
|
+
*
|
|
134
|
+
* Arc provides the interface, apps provide the implementation.
|
|
135
|
+
* This follows the same pattern as:
|
|
136
|
+
* - Database drivers (interface: query(), implementation: PostgreSQL, MySQL)
|
|
137
|
+
* - Storage providers (interface: upload(), implementation: S3, Azure)
|
|
138
|
+
* - Authentication strategies (interface: verify(), implementation: JWT, OAuth)
|
|
139
|
+
*
|
|
140
|
+
* @example E-commerce RBAC + Ownership
|
|
141
|
+
* ```typescript
|
|
142
|
+
* class EcommercePolicyEngine implements PolicyEngine {
|
|
143
|
+
* constructor(private config: { roles: Record<string, string[]> }) {}
|
|
144
|
+
*
|
|
145
|
+
* can(user, operation, context) {
|
|
146
|
+
* // Check RBAC
|
|
147
|
+
* const allowedRoles = this.config.roles[operation] || [];
|
|
148
|
+
* if (!user.roles.some(r => allowedRoles.includes(r))) {
|
|
149
|
+
* return { allowed: false, reason: 'Insufficient permissions' };
|
|
150
|
+
* }
|
|
151
|
+
*
|
|
152
|
+
* // Check ownership for update/delete
|
|
153
|
+
* if (['update', 'delete'].includes(operation)) {
|
|
154
|
+
* if (context.document.userId !== user.id) {
|
|
155
|
+
* return { allowed: false, reason: 'Not the owner' };
|
|
156
|
+
* }
|
|
157
|
+
* }
|
|
158
|
+
*
|
|
159
|
+
* // Multi-tenant filter for list
|
|
160
|
+
* if (operation === 'list') {
|
|
161
|
+
* return {
|
|
162
|
+
* allowed: true,
|
|
163
|
+
* filters: { organizationId: user.organizationId },
|
|
164
|
+
* };
|
|
165
|
+
* }
|
|
166
|
+
*
|
|
167
|
+
* return { allowed: true };
|
|
168
|
+
* }
|
|
169
|
+
*
|
|
170
|
+
* toMiddleware(operation) {
|
|
171
|
+
* return async (request, reply) => {
|
|
172
|
+
* const result = await this.can(request.user, operation, {
|
|
173
|
+
* document: request.document,
|
|
174
|
+
* body: request.body,
|
|
175
|
+
* params: request.params,
|
|
176
|
+
* query: request.query,
|
|
177
|
+
* });
|
|
178
|
+
*
|
|
179
|
+
* if (!result.allowed) {
|
|
180
|
+
* reply.code(403).send({ error: result.reason });
|
|
181
|
+
* }
|
|
182
|
+
*
|
|
183
|
+
* // Attach filters/fieldMask to request
|
|
184
|
+
* request.policyResult = result;
|
|
185
|
+
* };
|
|
186
|
+
* }
|
|
187
|
+
* }
|
|
188
|
+
* ```
|
|
189
|
+
*
|
|
190
|
+
* @example HIPAA Compliance
|
|
191
|
+
* ```typescript
|
|
192
|
+
* class HIPAAPolicyEngine implements PolicyEngine {
|
|
193
|
+
* can(user, operation, context) {
|
|
194
|
+
* // Check patient consent
|
|
195
|
+
* // Verify user certifications
|
|
196
|
+
* // Check data sensitivity level
|
|
197
|
+
* // Create audit log entry
|
|
198
|
+
*
|
|
199
|
+
* return {
|
|
200
|
+
* allowed: this.checkHIPAACompliance(user, operation, context),
|
|
201
|
+
* reason: 'HIPAA compliance check failed',
|
|
202
|
+
* metadata: {
|
|
203
|
+
* auditLog: this.createAuditEntry(user, operation),
|
|
204
|
+
* },
|
|
205
|
+
* };
|
|
206
|
+
* }
|
|
207
|
+
*
|
|
208
|
+
* toMiddleware(operation) {
|
|
209
|
+
* // HIPAA-specific middleware with audit logging
|
|
210
|
+
* }
|
|
211
|
+
* }
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
interface PolicyEngine {
|
|
215
|
+
/**
|
|
216
|
+
* Check if user can perform operation
|
|
217
|
+
*
|
|
218
|
+
* @param user - User object from request (request.user)
|
|
219
|
+
* @param operation - Operation name (list, get, create, update, delete, custom)
|
|
220
|
+
* @param context - Additional context (document, body, params, query, etc.)
|
|
221
|
+
* @returns Policy result with allowed/denied and optional filters/fieldMask
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```typescript
|
|
225
|
+
* const result = await policy.can(request.user, 'update', {
|
|
226
|
+
* document: existingDocument,
|
|
227
|
+
* body: request.body,
|
|
228
|
+
* });
|
|
229
|
+
*
|
|
230
|
+
* if (!result.allowed) {
|
|
231
|
+
* throw new Error(result.reason);
|
|
232
|
+
* }
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
can(user: any, operation: string, context?: PolicyContext): PolicyResult | Promise<PolicyResult>;
|
|
236
|
+
/**
|
|
237
|
+
* Generate Fastify middleware for this policy
|
|
238
|
+
*
|
|
239
|
+
* Called during route registration to create preHandler middleware.
|
|
240
|
+
* Middleware should:
|
|
241
|
+
* 1. Call can() with request context
|
|
242
|
+
* 2. Return 403 if denied
|
|
243
|
+
* 3. Attach result to request for downstream use
|
|
244
|
+
*
|
|
245
|
+
* @param operation - Operation name (list, get, create, update, delete)
|
|
246
|
+
* @returns Fastify preHandler middleware
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* const middleware = policy.toMiddleware('update');
|
|
251
|
+
* fastify.put('/products/:id', {
|
|
252
|
+
* preHandler: [authenticate, middleware],
|
|
253
|
+
* }, handler);
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
toMiddleware(operation: string): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Policy factory function signature
|
|
260
|
+
*
|
|
261
|
+
* Policies are typically created via factory functions that accept configuration.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* export function definePolicy(config: PolicyConfig): PolicyEngine {
|
|
266
|
+
* return new MyPolicyEngine(config);
|
|
267
|
+
* }
|
|
268
|
+
*
|
|
269
|
+
* // Usage
|
|
270
|
+
* const productPolicy = definePolicy({
|
|
271
|
+
* resource: 'product',
|
|
272
|
+
* roles: { list: ['user'], create: ['admin'] },
|
|
273
|
+
* ownership: { field: 'userId', operations: ['update', 'delete'] },
|
|
274
|
+
* });
|
|
275
|
+
* ```
|
|
276
|
+
*/
|
|
277
|
+
type PolicyFactory<TConfig = any> = (config: TConfig) => PolicyEngine;
|
|
278
|
+
/**
|
|
279
|
+
* Extended Fastify request with policy result
|
|
280
|
+
*/
|
|
281
|
+
declare module 'fastify' {
|
|
282
|
+
interface FastifyRequest {
|
|
283
|
+
policyResult?: PolicyResult;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Policy Helper Utilities
|
|
289
|
+
*
|
|
290
|
+
* Common operations for working with PolicyEngine implementations.
|
|
291
|
+
*/
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Helper to create Fastify middleware from any PolicyEngine implementation
|
|
295
|
+
*
|
|
296
|
+
* This is a convenience function that provides a standard middleware pattern.
|
|
297
|
+
* Most policies can use this instead of implementing toMiddleware() manually.
|
|
298
|
+
*
|
|
299
|
+
* @param policy - Policy engine instance
|
|
300
|
+
* @param operation - Operation name (list, get, create, update, delete)
|
|
301
|
+
* @returns Fastify preHandler middleware
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```typescript
|
|
305
|
+
* class SimplePolicy implements PolicyEngine {
|
|
306
|
+
* can(user, operation) {
|
|
307
|
+
* return { allowed: user.isActive };
|
|
308
|
+
* }
|
|
309
|
+
*
|
|
310
|
+
* toMiddleware(operation) {
|
|
311
|
+
* return createPolicyMiddleware(this, operation);
|
|
312
|
+
* }
|
|
313
|
+
* }
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
declare function createPolicyMiddleware(policy: PolicyEngine, operation: string): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
317
|
+
/**
|
|
318
|
+
* Combine multiple policies with AND logic
|
|
319
|
+
*
|
|
320
|
+
* All policies must allow the operation for it to succeed.
|
|
321
|
+
* First denial stops evaluation and returns the denial reason.
|
|
322
|
+
*
|
|
323
|
+
* @param policies - Array of policy engines to combine
|
|
324
|
+
* @returns Combined policy engine
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```typescript
|
|
328
|
+
* const combinedPolicy = combinePolicies(
|
|
329
|
+
* rbacPolicy, // Must have correct role
|
|
330
|
+
* ownershipPolicy, // Must own the resource
|
|
331
|
+
* auditPolicy, // Logs access for compliance
|
|
332
|
+
* );
|
|
333
|
+
*
|
|
334
|
+
* // All three policies must pass for the operation to succeed
|
|
335
|
+
* const result = await combinedPolicy.can(user, 'update', context);
|
|
336
|
+
* ```
|
|
337
|
+
*
|
|
338
|
+
* @example Multi-tenant + RBAC
|
|
339
|
+
* ```typescript
|
|
340
|
+
* const policy = combinePolicies(
|
|
341
|
+
* definePolicy({ tenant: { field: 'organizationId' } }),
|
|
342
|
+
* definePolicy({ roles: { update: ['admin', 'editor'] } }),
|
|
343
|
+
* );
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
declare function combinePolicies(...policies: PolicyEngine[]): PolicyEngine;
|
|
347
|
+
/**
|
|
348
|
+
* Combine multiple policies with OR logic
|
|
349
|
+
*
|
|
350
|
+
* At least one policy must allow the operation for it to succeed.
|
|
351
|
+
* If all policies deny, returns the first denial reason.
|
|
352
|
+
*
|
|
353
|
+
* @param policies - Array of policy engines to combine
|
|
354
|
+
* @returns Combined policy engine
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* ```typescript
|
|
358
|
+
* const policy = anyPolicy(
|
|
359
|
+
* ownerPolicy, // User owns the resource
|
|
360
|
+
* adminPolicy, // OR user is admin
|
|
361
|
+
* publicPolicy, // OR resource is public
|
|
362
|
+
* );
|
|
363
|
+
*
|
|
364
|
+
* // Any one of these policies passing allows the operation
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
declare function anyPolicy(...policies: PolicyEngine[]): PolicyEngine;
|
|
368
|
+
/**
|
|
369
|
+
* Create a pass-through policy that always allows
|
|
370
|
+
*
|
|
371
|
+
* Useful for testing or for routes that don't need authorization.
|
|
372
|
+
*
|
|
373
|
+
* @example
|
|
374
|
+
* ```typescript
|
|
375
|
+
* const policy = allowAll();
|
|
376
|
+
* const result = await policy.can(user, 'any-operation');
|
|
377
|
+
* // result.allowed === true
|
|
378
|
+
* ```
|
|
379
|
+
*/
|
|
380
|
+
declare function allowAll(): PolicyEngine;
|
|
381
|
+
/**
|
|
382
|
+
* Create a policy that always denies
|
|
383
|
+
*
|
|
384
|
+
* Useful for explicitly blocking operations or for testing.
|
|
385
|
+
*
|
|
386
|
+
* @param reason - Denial reason
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* ```typescript
|
|
390
|
+
* const policy = denyAll('This resource is deprecated');
|
|
391
|
+
* const result = await policy.can(user, 'any-operation');
|
|
392
|
+
* // result.allowed === false
|
|
393
|
+
* // result.reason === 'This resource is deprecated'
|
|
394
|
+
* ```
|
|
395
|
+
*/
|
|
396
|
+
declare function denyAll(reason?: string): PolicyEngine;
|
|
397
|
+
|
|
398
|
+
export { type PolicyContext, type PolicyEngine, type PolicyFactory, type PolicyResult, allowAll, anyPolicy, combinePolicies, createPolicyMiddleware, denyAll };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// src/policies/helpers.ts
|
|
2
|
+
function createPolicyMiddleware(policy, operation) {
|
|
3
|
+
return async function policyMiddleware(request, reply) {
|
|
4
|
+
const context = {
|
|
5
|
+
document: request.document,
|
|
6
|
+
body: request.body,
|
|
7
|
+
params: request.params,
|
|
8
|
+
query: request.query
|
|
9
|
+
};
|
|
10
|
+
const result = await policy.can(request.user, operation, context);
|
|
11
|
+
if (!result.allowed) {
|
|
12
|
+
return reply.code(403).send({
|
|
13
|
+
success: false,
|
|
14
|
+
error: "Access denied",
|
|
15
|
+
message: result.reason || "You do not have permission to perform this action"
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
request.policyResult = result;
|
|
19
|
+
if (result.filters && Object.keys(result.filters).length > 0) {
|
|
20
|
+
request.query = request.query || {};
|
|
21
|
+
request.query._policyFilters = result.filters;
|
|
22
|
+
}
|
|
23
|
+
if (result.fieldMask) {
|
|
24
|
+
request.fieldMask = result.fieldMask;
|
|
25
|
+
}
|
|
26
|
+
if (result.metadata) {
|
|
27
|
+
request.policyMetadata = result.metadata;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function combinePolicies(...policies) {
|
|
32
|
+
if (policies.length === 0) {
|
|
33
|
+
throw new Error("combinePolicies requires at least one policy");
|
|
34
|
+
}
|
|
35
|
+
if (policies.length === 1) {
|
|
36
|
+
return policies[0];
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
async can(user, operation, context) {
|
|
40
|
+
const results = [];
|
|
41
|
+
for (const policy of policies) {
|
|
42
|
+
const result = await policy.can(user, operation, context);
|
|
43
|
+
if (!result.allowed) {
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
results.push(result);
|
|
47
|
+
}
|
|
48
|
+
const mergedResult = {
|
|
49
|
+
allowed: true,
|
|
50
|
+
filters: {},
|
|
51
|
+
metadata: {}
|
|
52
|
+
};
|
|
53
|
+
for (const result of results) {
|
|
54
|
+
if (result.filters) {
|
|
55
|
+
Object.assign(mergedResult.filters, result.filters);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const allExcludes = /* @__PURE__ */ new Set();
|
|
59
|
+
const allIncludes = [];
|
|
60
|
+
for (const result of results) {
|
|
61
|
+
if (result.fieldMask?.exclude) {
|
|
62
|
+
result.fieldMask.exclude.forEach((field) => allExcludes.add(field));
|
|
63
|
+
}
|
|
64
|
+
if (result.fieldMask?.include) {
|
|
65
|
+
allIncludes.push(new Set(result.fieldMask.include));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (allExcludes.size > 0 || allIncludes.length > 0) {
|
|
69
|
+
mergedResult.fieldMask = {};
|
|
70
|
+
if (allExcludes.size > 0) {
|
|
71
|
+
mergedResult.fieldMask.exclude = Array.from(allExcludes);
|
|
72
|
+
}
|
|
73
|
+
if (allIncludes.length > 0) {
|
|
74
|
+
const intersection = allIncludes.reduce((acc, set) => {
|
|
75
|
+
return new Set([...acc].filter((x) => set.has(x)));
|
|
76
|
+
});
|
|
77
|
+
if (intersection.size > 0) {
|
|
78
|
+
mergedResult.fieldMask.include = Array.from(intersection);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const result of results) {
|
|
83
|
+
if (result.metadata) {
|
|
84
|
+
Object.assign(mergedResult.metadata, result.metadata);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (Object.keys(mergedResult.filters).length === 0) {
|
|
88
|
+
delete mergedResult.filters;
|
|
89
|
+
}
|
|
90
|
+
if (Object.keys(mergedResult.metadata).length === 0) {
|
|
91
|
+
delete mergedResult.metadata;
|
|
92
|
+
}
|
|
93
|
+
return mergedResult;
|
|
94
|
+
},
|
|
95
|
+
toMiddleware(operation) {
|
|
96
|
+
const middlewares = policies.map((p) => p.toMiddleware(operation));
|
|
97
|
+
return async (request, reply) => {
|
|
98
|
+
for (const middleware of middlewares) {
|
|
99
|
+
await middleware(request, reply);
|
|
100
|
+
if (reply.sent) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function anyPolicy(...policies) {
|
|
109
|
+
if (policies.length === 0) {
|
|
110
|
+
throw new Error("anyPolicy requires at least one policy");
|
|
111
|
+
}
|
|
112
|
+
if (policies.length === 1) {
|
|
113
|
+
return policies[0];
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
async can(user, operation, context) {
|
|
117
|
+
let firstDenial = null;
|
|
118
|
+
for (const policy of policies) {
|
|
119
|
+
const result = await policy.can(user, operation, context);
|
|
120
|
+
if (result.allowed) {
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
if (!firstDenial) {
|
|
124
|
+
firstDenial = result;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return firstDenial;
|
|
128
|
+
},
|
|
129
|
+
toMiddleware(operation) {
|
|
130
|
+
return async (request, reply) => {
|
|
131
|
+
const results = [];
|
|
132
|
+
for (const policy of policies) {
|
|
133
|
+
const result = await policy.can(
|
|
134
|
+
request.user,
|
|
135
|
+
operation,
|
|
136
|
+
{
|
|
137
|
+
document: request.document,
|
|
138
|
+
body: request.body,
|
|
139
|
+
params: request.params,
|
|
140
|
+
query: request.query
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
if (result.allowed) {
|
|
144
|
+
request.policyResult = result;
|
|
145
|
+
if (result.filters) {
|
|
146
|
+
request.query = request.query || {};
|
|
147
|
+
request.query._policyFilters = result.filters;
|
|
148
|
+
}
|
|
149
|
+
if (result.fieldMask) {
|
|
150
|
+
request.fieldMask = result.fieldMask;
|
|
151
|
+
}
|
|
152
|
+
if (result.metadata) {
|
|
153
|
+
request.policyMetadata = result.metadata;
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
results.push(result);
|
|
158
|
+
}
|
|
159
|
+
return reply.code(403).send({
|
|
160
|
+
success: false,
|
|
161
|
+
error: "Access denied",
|
|
162
|
+
message: results[0]?.reason || "You do not have permission to perform this action"
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function allowAll() {
|
|
169
|
+
return {
|
|
170
|
+
can() {
|
|
171
|
+
return { allowed: true };
|
|
172
|
+
},
|
|
173
|
+
toMiddleware() {
|
|
174
|
+
return async () => {
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function denyAll(reason = "Operation not allowed") {
|
|
180
|
+
return {
|
|
181
|
+
can() {
|
|
182
|
+
return { allowed: false, reason };
|
|
183
|
+
},
|
|
184
|
+
toMiddleware() {
|
|
185
|
+
return async (request, reply) => {
|
|
186
|
+
return reply.code(403).send({
|
|
187
|
+
success: false,
|
|
188
|
+
error: "Access denied",
|
|
189
|
+
message: reason
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export { allowAll, anyPolicy, combinePolicies, createPolicyMiddleware, denyAll };
|