@feltdb/core 0.4.15 → 0.4.17

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.
Files changed (42) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/collection.d.ts +11 -0
  3. package/dist/collection.d.ts.map +1 -1
  4. package/dist/collection.js +34 -0
  5. package/dist/create/package-versions.js +1 -1
  6. package/dist/create/server-source/crates/feltdb/Cargo.toml +1 -1
  7. package/dist/create/server-source/crates/feltdb/src/application.rs +93 -0
  8. package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +787 -0
  9. package/dist/create/server-source/crates/feltdb/src/lib.rs +139 -0
  10. package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +1669 -0
  11. package/dist/create/server-source/crates/feltdb/src/state_contract.rs +1269 -15
  12. package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +406 -0
  13. package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +908 -0
  14. package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +1028 -0
  15. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +332 -0
  16. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +342 -0
  17. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +313 -0
  18. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +90 -0
  19. package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +571 -0
  20. package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +506 -0
  21. package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +81 -0
  22. package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +434 -0
  23. package/dist/create/server-source/crates/feltdb-server/src/main.rs +130 -23
  24. package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +2 -2
  25. package/dist/db.d.ts +91 -0
  26. package/dist/db.d.ts.map +1 -1
  27. package/dist/db.js +89 -1
  28. package/dist/feltdb.d.ts +14 -0
  29. package/dist/feltdb.d.ts.map +1 -1
  30. package/dist/file-db.d.ts +75 -0
  31. package/dist/file-db.d.ts.map +1 -0
  32. package/dist/file-db.js +437 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -0
  36. package/dist/studio-app/assets/{feltdb_wasm-CJEJryDx.js → feltdb_wasm-CBGD0zRu.js} +1 -1
  37. package/dist/studio-app/assets/feltdb_wasm_bg-C6ATF9mJ.wasm +0 -0
  38. package/dist/studio-app/assets/{index-D_p8T7nO.js → index-5siPkRSN.js} +9 -9
  39. package/dist/studio-app/index.html +1 -1
  40. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  41. package/package.json +9 -4
  42. package/dist/studio-app/assets/feltdb_wasm_bg-BJxQXtoo.wasm +0 -0
@@ -0,0 +1,787 @@
1
+ //! Phase 5: Authorization Security & Adversarial Verification
2
+ //!
3
+ //! Comprehensive tests proving the authorization system cannot be bypassed through:
4
+ //! - Caller attribute spoofing
5
+ //! - Cross-organization access
6
+ //! - Query/mutation isolation violations
7
+ //! - Transaction atomicity breaks
8
+ //! - Reference integrity violations
9
+ //! - Membership revocation handling
10
+ //! - Policy interaction vulnerabilities
11
+
12
+ #[cfg(test)]
13
+ mod security_tests {
14
+ use crate::*;
15
+ use serde_json::json;
16
+ use std::sync::Arc;
17
+
18
+ // ============================================================================
19
+ // Helpers
20
+ // ============================================================================
21
+
22
+ fn make_stored_row(collection: &str, key: &str, value: serde_json::Value) -> StoredRow {
23
+ StoredRow {
24
+ capability: format!("tenant:test:{}", collection),
25
+ key: key.into(),
26
+ rust_type: "json".into(),
27
+ value,
28
+ unix_ms: 0,
29
+ content_hash: None,
30
+ flow_ref: None,
31
+ deleted: false,
32
+ operation: None,
33
+ }
34
+ }
35
+
36
+ fn make_schema_with_collections(
37
+ collections: Vec<state_contract::CollectionSchema>,
38
+ ) -> Arc<state_contract::StateSchema> {
39
+ Arc::new(state_contract::StateSchema {
40
+ contract_version: 1,
41
+ schema_version: 1,
42
+ application_id: "app".into(),
43
+ revision_id: "rev".into(),
44
+ collections,
45
+ })
46
+ }
47
+
48
+ fn make_collection(
49
+ name: &str,
50
+ fields: Vec<state_contract::FieldSchema>,
51
+ ) -> state_contract::CollectionSchema {
52
+ state_contract::CollectionSchema {
53
+ name: name.into(),
54
+ version: 1,
55
+ fields,
56
+ indexes: vec![],
57
+ }
58
+ }
59
+
60
+ fn make_field(
61
+ name: &str,
62
+ field_type: state_contract::FieldType,
63
+ ) -> state_contract::FieldSchema {
64
+ state_contract::FieldSchema {
65
+ name: name.into(),
66
+ field_type,
67
+ nullable: false,
68
+ required: true,
69
+ default: None,
70
+ constraints: state_contract::FieldConstraints::default(),
71
+ computed: None,
72
+ }
73
+ }
74
+
75
+ fn make_auth_state(
76
+ schema: Arc<state_contract::StateSchema>,
77
+ snapshot: Vec<StoredRow>,
78
+ ) -> policy_evaluation::AuthorizationState {
79
+ policy_evaluation::AuthorizationState::new(schema, "tenant:test", Arc::new(snapshot))
80
+ }
81
+
82
+ // ============================================================================
83
+ // Invariant A: No Caller Authorization Facts
84
+ // ============================================================================
85
+
86
+ #[test]
87
+ fn invariant_a_caller_organization_id_ignored() {
88
+ use state_contract::FieldType;
89
+
90
+ let schema = make_schema_with_collections(vec![
91
+ make_collection("Project", vec![
92
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
93
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
94
+ ]),
95
+ make_collection("Membership", vec![
96
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
97
+ make_field("user", FieldType::Reference { collection: "User".into() }),
98
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
99
+ ]),
100
+ ]);
101
+
102
+ let project = json!({"_id": "proj1", "organization": "org-A"});
103
+ let membership = json!({"_id": "mem1", "user": "alice", "organization": "org-A"});
104
+
105
+ let snapshot = vec![
106
+ make_stored_row("Project", "proj1", project),
107
+ make_stored_row("Membership", "mem1", membership),
108
+ ];
109
+
110
+ let auth_state = make_auth_state(schema, snapshot);
111
+
112
+ // Test that correct record from snapshot authorizes
113
+ let correct_context = policy_evaluation::RecordAuthorizationContext::new(
114
+ Some(policy_evaluation::Actor::new("alice")),
115
+ "Project",
116
+ "proj1",
117
+ json!({"_id": "proj1", "organization": "org-A"}), // Correct org from snapshot
118
+ )
119
+ .with_state(auth_state.clone());
120
+
121
+ // This should succeed
122
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
123
+ policy_evaluation::PolicySubject::Member,
124
+ &correct_context,
125
+ );
126
+ assert!(result.is_ok());
127
+
128
+ // Now test that even if we spoof the field, it doesn't matter because
129
+ // the record value should come from authoritative snapshot
130
+ let spoofed_context = policy_evaluation::RecordAuthorizationContext::new(
131
+ Some(policy_evaluation::Actor::new("alice")),
132
+ "Project",
133
+ "proj1",
134
+ json!({"_id": "proj1", "organization": "org-B"}), // Spoofed
135
+ )
136
+ .with_state(auth_state);
137
+
138
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
139
+ policy_evaluation::PolicySubject::Member,
140
+ &spoofed_context,
141
+ );
142
+ // This fails because org-B != org-A (the membership org)
143
+ assert!(result.is_err());
144
+ assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
145
+ }
146
+
147
+ #[test]
148
+ fn invariant_a_caller_is_member_attribute_ignored() {
149
+ use state_contract::FieldType;
150
+
151
+ let schema = make_schema_with_collections(vec![
152
+ make_collection("Project", vec![
153
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
154
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
155
+ ]),
156
+ make_collection("Membership", vec![
157
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
158
+ make_field("user", FieldType::Reference { collection: "User".into() }),
159
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
160
+ ]),
161
+ ]);
162
+
163
+ let project = json!({"_id": "proj1", "organization": "org-A"});
164
+ let snapshot = vec![
165
+ make_stored_row("Project", "proj1", project),
166
+ // No membership record
167
+ ];
168
+
169
+ let auth_state = make_auth_state(schema, snapshot);
170
+
171
+ // Caller claims is_member=true but no actual membership record exists
172
+ let context = policy_evaluation::RecordAuthorizationContext::new(
173
+ Some(policy_evaluation::Actor::new("alice")),
174
+ "Project",
175
+ "proj1",
176
+ json!({"_id": "proj1", "organization": "org-A", "is_member": "true"}), // Spoofed attribute
177
+ )
178
+ .with_state(auth_state);
179
+
180
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
181
+ policy_evaluation::PolicySubject::Member,
182
+ &context,
183
+ );
184
+
185
+ // Must deny because no actual membership record exists
186
+ assert!(result.is_err());
187
+ assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
188
+ }
189
+
190
+ #[test]
191
+ fn invariant_a_caller_role_attribute_ignored() {
192
+ use state_contract::FieldType;
193
+
194
+ let schema = make_schema_with_collections(vec![
195
+ make_collection("Project", vec![
196
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
197
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
198
+ ]),
199
+ make_collection("Membership", vec![
200
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
201
+ make_field("user", FieldType::Reference { collection: "User".into() }),
202
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
203
+ ]),
204
+ ]);
205
+
206
+ let project = json!({"_id": "proj1", "organization": "org-A"});
207
+ let snapshot = vec![
208
+ make_stored_row("Project", "proj1", project),
209
+ ];
210
+
211
+ let auth_state = make_auth_state(schema, snapshot);
212
+
213
+ // Caller claims role=admin but no membership exists
214
+ let context = policy_evaluation::RecordAuthorizationContext::new(
215
+ Some(policy_evaluation::Actor::new("alice")),
216
+ "Project",
217
+ "proj1",
218
+ json!({"_id": "proj1", "organization": "org-A", "role": "admin"}), // Spoofed
219
+ )
220
+ .with_state(auth_state);
221
+
222
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
223
+ policy_evaluation::PolicySubject::Member,
224
+ &context,
225
+ );
226
+
227
+ assert!(result.is_err());
228
+ assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
229
+ }
230
+
231
+ // ============================================================================
232
+ // Invariant B: Query Isolation
233
+ // ============================================================================
234
+
235
+ #[test]
236
+ fn invariant_b_query_cannot_bypass_record_auth() {
237
+ use state_contract::FieldType;
238
+
239
+ let schema = make_schema_with_collections(vec![
240
+ make_collection("Project", vec![
241
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
242
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
243
+ ]),
244
+ make_collection("Membership", vec![
245
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
246
+ make_field("user", FieldType::Reference { collection: "User".into() }),
247
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
248
+ ]),
249
+ ]);
250
+
251
+ // Three projects: alice can access proj1 and proj3 (org-A),
252
+ // cannot access proj2 (org-B)
253
+ let proj1 = json!({"_id": "proj1", "organization": "org-A"});
254
+ let proj2 = json!({"_id": "proj2", "organization": "org-B"});
255
+ let proj3 = json!({"_id": "proj3", "organization": "org-A"});
256
+
257
+ let mem_a = json!({"_id": "mem1", "user": "alice", "organization": "org-A"});
258
+
259
+ let snapshot = vec![
260
+ make_stored_row("Project", "proj1", proj1.clone()),
261
+ make_stored_row("Project", "proj2", proj2.clone()),
262
+ make_stored_row("Project", "proj3", proj3.clone()),
263
+ make_stored_row("Membership", "mem1", mem_a),
264
+ ];
265
+
266
+ let auth_state = make_auth_state(schema, snapshot);
267
+
268
+ // Test authorization for each project
269
+ let projects = vec![(proj1, "proj1"), (proj2, "proj2"), (proj3, "proj3")];
270
+
271
+ for (proj, proj_id) in projects.iter() {
272
+ let context = policy_evaluation::RecordAuthorizationContext::new(
273
+ Some(policy_evaluation::Actor::new("alice")),
274
+ "Project",
275
+ *proj_id,
276
+ proj.clone(),
277
+ )
278
+ .with_state(auth_state.clone());
279
+
280
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
281
+ policy_evaluation::PolicySubject::Member,
282
+ &context,
283
+ );
284
+
285
+ if *proj_id == "proj2" {
286
+ // proj2 is in org-B, alice is only member of org-A
287
+ assert!(result.is_err());
288
+ assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
289
+ } else {
290
+ // proj1 and proj3 are in org-A
291
+ assert!(result.is_ok());
292
+ }
293
+ }
294
+ }
295
+
296
+ // ============================================================================
297
+ // Invariant D: Update Authorization Uses Pre-Update State
298
+ // ============================================================================
299
+
300
+ #[test]
301
+ fn invariant_d_update_auth_uses_existing_state() {
302
+ use state_contract::FieldType;
303
+
304
+ let schema = make_schema_with_collections(vec![
305
+ make_collection("Project", vec![
306
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
307
+ make_field("owner_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
308
+ ]),
309
+ ]);
310
+
311
+ // Existing project owned by alice
312
+ let existing = json!({"_id": "proj1", "owner_id": "alice", "title": "Secret"});
313
+
314
+ let snapshot = vec![
315
+ make_stored_row("Project", "proj1", existing.clone()),
316
+ ];
317
+
318
+ let auth_state = make_auth_state(schema, snapshot);
319
+
320
+ let context = policy_evaluation::RecordAuthorizationContext::new(
321
+ Some(policy_evaluation::Actor::new("bob")),
322
+ "Project",
323
+ "proj1",
324
+ existing, // Authorization uses EXISTING state, not proposed
325
+ )
326
+ .with_state(auth_state);
327
+
328
+ // Owner policy checks if bob == owner_id
329
+ // Existing owner_id is alice, so bob is not owner
330
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
331
+ policy_evaluation::PolicySubject::Owner,
332
+ &context,
333
+ );
334
+
335
+ assert!(result.is_err());
336
+ assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
337
+ }
338
+
339
+ // ============================================================================
340
+ // Invariant J: Cross-Org Isolation
341
+ // ============================================================================
342
+
343
+ #[test]
344
+ fn invariant_j_member_cannot_access_other_org_resource() {
345
+ use state_contract::FieldType;
346
+
347
+ let schema = make_schema_with_collections(vec![
348
+ make_collection("Project", vec![
349
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
350
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
351
+ ]),
352
+ make_collection("Membership", vec![
353
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
354
+ make_field("user", FieldType::Reference { collection: "User".into() }),
355
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
356
+ ]),
357
+ ]);
358
+
359
+ // Alice is member of org-A only
360
+ let mem_a = json!({"_id": "mem1", "user": "alice", "organization": "org-A"});
361
+
362
+ // Project is in org-B
363
+ let proj_b = json!({"_id": "proj2", "organization": "org-B"});
364
+
365
+ let snapshot = vec![
366
+ make_stored_row("Membership", "mem1", mem_a),
367
+ make_stored_row("Project", "proj2", proj_b),
368
+ ];
369
+
370
+ let auth_state = make_auth_state(schema, snapshot);
371
+
372
+ let context = policy_evaluation::RecordAuthorizationContext::new(
373
+ Some(policy_evaluation::Actor::new("alice")),
374
+ "Project",
375
+ "proj2",
376
+ json!({"_id": "proj2", "organization": "org-B"}),
377
+ )
378
+ .with_state(auth_state);
379
+
380
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
381
+ policy_evaluation::PolicySubject::Member,
382
+ &context,
383
+ );
384
+
385
+ // Alice is not member of org-B
386
+ assert!(result.is_err());
387
+ assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
388
+ }
389
+
390
+ #[test]
391
+ fn invariant_j_cannot_become_member_through_update() {
392
+ use state_contract::FieldType;
393
+
394
+ let schema = make_schema_with_collections(vec![
395
+ make_collection("Project", vec![
396
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
397
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
398
+ ]),
399
+ make_collection("Membership", vec![
400
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
401
+ make_field("user", FieldType::Reference { collection: "User".into() }),
402
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
403
+ ]),
404
+ ]);
405
+
406
+ // Alice is member of org-A
407
+ let mem_a = json!({"_id": "mem1", "user": "alice", "organization": "org-A"});
408
+
409
+ // Existing project is in org-A
410
+ let proj_a = json!({"_id": "proj1", "organization": "org-A"});
411
+
412
+ let snapshot = vec![
413
+ make_stored_row("Membership", "mem1", mem_a),
414
+ make_stored_row("Project", "proj1", proj_a.clone()),
415
+ ];
416
+
417
+ let auth_state = make_auth_state(schema, snapshot);
418
+
419
+ // Alice tries to update the project to org-B (where she's not a member)
420
+ // But authorization uses the EXISTING state (org-A)
421
+ let context = policy_evaluation::RecordAuthorizationContext::new(
422
+ Some(policy_evaluation::Actor::new("alice")),
423
+ "Project",
424
+ "proj1",
425
+ proj_a, // Authorization uses existing org-A
426
+ )
427
+ .with_state(auth_state);
428
+
429
+ // Alice is member of org-A, so this should authorize
430
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
431
+ policy_evaluation::PolicySubject::Member,
432
+ &context,
433
+ );
434
+
435
+ // This will ALLOW because authorization uses existing (org-A)
436
+ // The update to org-B would happen only if auth passes
437
+ // Then downstream checks would ensure the new org is valid
438
+ assert!(result.is_ok());
439
+ }
440
+
441
+ // ============================================================================
442
+ // Reference Integrity Tests
443
+ // ============================================================================
444
+
445
+ #[test]
446
+ fn reference_missing_organization_reference_in_schema() {
447
+ use state_contract::FieldType;
448
+
449
+ let schema = make_schema_with_collections(vec![
450
+ make_collection("Project", vec![
451
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
452
+ // No organization reference
453
+ ]),
454
+ ]);
455
+
456
+ let proj = json!({"_id": "proj1"});
457
+ let snapshot = vec![
458
+ make_stored_row("Project", "proj1", proj),
459
+ ];
460
+
461
+ let auth_state = make_auth_state(schema, snapshot);
462
+
463
+ let context = policy_evaluation::RecordAuthorizationContext::new(
464
+ Some(policy_evaluation::Actor::new("alice")),
465
+ "Project",
466
+ "proj1",
467
+ json!({"_id": "proj1"}),
468
+ )
469
+ .with_state(auth_state);
470
+
471
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
472
+ policy_evaluation::PolicySubject::Member,
473
+ &context,
474
+ );
475
+
476
+ // Must fail because schema doesn't define organization reference
477
+ assert!(result.is_err());
478
+ assert_eq!(result.unwrap_err().code, "ORGANIZATION_NOT_DEFINED");
479
+ }
480
+
481
+ #[test]
482
+ fn reference_missing_organization_value_in_record() {
483
+ use state_contract::FieldType;
484
+
485
+ let schema = make_schema_with_collections(vec![
486
+ make_collection("Project", vec![
487
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
488
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
489
+ ]),
490
+ ]);
491
+
492
+ let proj = json!({"_id": "proj1"}); // Missing organization field
493
+ let snapshot = vec![
494
+ make_stored_row("Project", "proj1", proj),
495
+ ];
496
+
497
+ let auth_state = make_auth_state(schema, snapshot);
498
+
499
+ let context = policy_evaluation::RecordAuthorizationContext::new(
500
+ Some(policy_evaluation::Actor::new("alice")),
501
+ "Project",
502
+ "proj1",
503
+ json!({"_id": "proj1"}),
504
+ )
505
+ .with_state(auth_state);
506
+
507
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
508
+ policy_evaluation::PolicySubject::Member,
509
+ &context,
510
+ );
511
+
512
+ assert!(result.is_err());
513
+ assert_eq!(result.unwrap_err().code, "ORGANIZATION_NOT_DEFINED");
514
+ }
515
+
516
+ #[test]
517
+ fn reference_null_organization_value() {
518
+ use state_contract::FieldType;
519
+
520
+ let schema = make_schema_with_collections(vec![
521
+ make_collection("Project", vec![
522
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
523
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
524
+ ]),
525
+ ]);
526
+
527
+ let proj = json!({"_id": "proj1", "organization": null});
528
+ let snapshot = vec![
529
+ make_stored_row("Project", "proj1", proj),
530
+ ];
531
+
532
+ let auth_state = make_auth_state(schema, snapshot);
533
+
534
+ let context = policy_evaluation::RecordAuthorizationContext::new(
535
+ Some(policy_evaluation::Actor::new("alice")),
536
+ "Project",
537
+ "proj1",
538
+ json!({"_id": "proj1", "organization": null}),
539
+ )
540
+ .with_state(auth_state);
541
+
542
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
543
+ policy_evaluation::PolicySubject::Member,
544
+ &context,
545
+ );
546
+
547
+ assert!(result.is_err());
548
+ assert_eq!(result.unwrap_err().code, "ORGANIZATION_NOT_DEFINED");
549
+ }
550
+
551
+ // ============================================================================
552
+ // Membership Tests
553
+ // ============================================================================
554
+
555
+ #[test]
556
+ fn membership_deleted_record_not_honored() {
557
+ use state_contract::FieldType;
558
+
559
+ let schema = make_schema_with_collections(vec![
560
+ make_collection("Project", vec![
561
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
562
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
563
+ ]),
564
+ make_collection("Membership", vec![
565
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
566
+ make_field("user", FieldType::Reference { collection: "User".into() }),
567
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
568
+ ]),
569
+ ]);
570
+
571
+ let proj = json!({"_id": "proj1", "organization": "org-A"});
572
+ let mut mem = make_stored_row("Membership", "mem1",
573
+ json!({"_id": "mem1", "user": "alice", "organization": "org-A"}));
574
+ mem.deleted = true; // Mark as deleted
575
+
576
+ let snapshot = vec![
577
+ make_stored_row("Project", "proj1", proj),
578
+ mem,
579
+ ];
580
+
581
+ let auth_state = make_auth_state(schema, snapshot);
582
+
583
+ let context = policy_evaluation::RecordAuthorizationContext::new(
584
+ Some(policy_evaluation::Actor::new("alice")),
585
+ "Project",
586
+ "proj1",
587
+ json!({"_id": "proj1", "organization": "org-A"}),
588
+ )
589
+ .with_state(auth_state);
590
+
591
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
592
+ policy_evaluation::PolicySubject::Member,
593
+ &context,
594
+ );
595
+
596
+ // Deleted membership must not grant access
597
+ assert!(result.is_err());
598
+ assert_eq!(result.unwrap_err().code, "UNAUTHORIZED");
599
+ }
600
+
601
+ #[test]
602
+ fn membership_multiple_memberships_found_correctly() {
603
+ use state_contract::FieldType;
604
+
605
+ let schema = make_schema_with_collections(vec![
606
+ make_collection("Project", vec![
607
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
608
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
609
+ ]),
610
+ make_collection("Membership", vec![
611
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
612
+ make_field("user", FieldType::Reference { collection: "User".into() }),
613
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
614
+ ]),
615
+ ]);
616
+
617
+ let proj = json!({"_id": "proj1", "organization": "org-B"});
618
+
619
+ // Alice has memberships in both org-A and org-B
620
+ let mem_a = json!({"_id": "mem1", "user": "alice", "organization": "org-A"});
621
+ let mem_b = json!({"_id": "mem2", "user": "alice", "organization": "org-B"});
622
+
623
+ let snapshot = vec![
624
+ make_stored_row("Project", "proj1", proj),
625
+ make_stored_row("Membership", "mem1", mem_a),
626
+ make_stored_row("Membership", "mem2", mem_b),
627
+ ];
628
+
629
+ let auth_state = make_auth_state(schema, snapshot);
630
+
631
+ let context = policy_evaluation::RecordAuthorizationContext::new(
632
+ Some(policy_evaluation::Actor::new("alice")),
633
+ "Project",
634
+ "proj1",
635
+ json!({"_id": "proj1", "organization": "org-B"}),
636
+ )
637
+ .with_state(auth_state);
638
+
639
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
640
+ policy_evaluation::PolicySubject::Member,
641
+ &context,
642
+ );
643
+
644
+ // Alice has membership in org-B, so should allow
645
+ assert!(result.is_ok());
646
+ }
647
+
648
+ // ============================================================================
649
+ // Anonymous/Unauthenticated Tests
650
+ // ============================================================================
651
+
652
+ #[test]
653
+ fn anonymous_member_policy_denied() {
654
+ use state_contract::FieldType;
655
+
656
+ let schema = make_schema_with_collections(vec![
657
+ make_collection("Project", vec![
658
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
659
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
660
+ ]),
661
+ make_collection("Membership", vec![
662
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
663
+ make_field("user", FieldType::Reference { collection: "User".into() }),
664
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
665
+ ]),
666
+ ]);
667
+
668
+ let proj = json!({"_id": "proj1", "organization": "org-A"});
669
+ let mem = json!({"_id": "mem1", "user": "alice", "organization": "org-A"});
670
+
671
+ let snapshot = vec![
672
+ make_stored_row("Project", "proj1", proj),
673
+ make_stored_row("Membership", "mem1", mem),
674
+ ];
675
+
676
+ let auth_state = make_auth_state(schema, snapshot);
677
+
678
+ // No actor (anonymous)
679
+ let context = policy_evaluation::RecordAuthorizationContext::new(
680
+ None,
681
+ "Project",
682
+ "proj1",
683
+ json!({"_id": "proj1", "organization": "org-A"}),
684
+ )
685
+ .with_state(auth_state);
686
+
687
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
688
+ policy_evaluation::PolicySubject::Member,
689
+ &context,
690
+ );
691
+
692
+ assert!(result.is_err());
693
+ assert_eq!(result.unwrap_err().code, "UNAUTHENTICATED");
694
+ }
695
+
696
+ #[test]
697
+ fn anonymous_owner_policy_denied() {
698
+ let proj = json!({"_id": "proj1", "owner": "alice"});
699
+
700
+ let context = policy_evaluation::RecordAuthorizationContext::new(
701
+ None,
702
+ "Project",
703
+ "proj1",
704
+ proj,
705
+ );
706
+
707
+ let result = policy_evaluation::PolicyEvaluator::evaluate_record(
708
+ policy_evaluation::PolicySubject::Owner,
709
+ &context,
710
+ );
711
+
712
+ assert!(result.is_err());
713
+ assert_eq!(result.unwrap_err().code, "UNAUTHENTICATED");
714
+ }
715
+
716
+ // ============================================================================
717
+ // Comprehensive Property Tests
718
+ // ============================================================================
719
+
720
+ #[test]
721
+ fn property_caller_attribute_changes_dont_change_authorization() {
722
+ use state_contract::FieldType;
723
+
724
+ let schema = make_schema_with_collections(vec![
725
+ make_collection("Project", vec![
726
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
727
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
728
+ ]),
729
+ make_collection("Membership", vec![
730
+ make_field("_id", FieldType::Primitive { primitive: state_contract::PrimitiveType::String }),
731
+ make_field("user", FieldType::Reference { collection: "User".into() }),
732
+ make_field("organization", FieldType::Reference { collection: "Organization".into() }),
733
+ ]),
734
+ ]);
735
+
736
+ let proj = json!({"_id": "proj1", "organization": "org-A"});
737
+ let mem = json!({"_id": "mem1", "user": "alice", "organization": "org-A"});
738
+
739
+ let snapshot = vec![
740
+ make_stored_row("Project", "proj1", proj),
741
+ make_stored_row("Membership", "mem1", mem),
742
+ ];
743
+
744
+ let auth_state = make_auth_state(schema, snapshot);
745
+
746
+ // Base case: correct state
747
+ let base_context = policy_evaluation::RecordAuthorizationContext::new(
748
+ Some(policy_evaluation::Actor::new("alice")),
749
+ "Project",
750
+ "proj1",
751
+ json!({"_id": "proj1", "organization": "org-A"}),
752
+ )
753
+ .with_state(auth_state.clone());
754
+
755
+ let base_result = policy_evaluation::PolicyEvaluator::evaluate_record(
756
+ policy_evaluation::PolicySubject::Member,
757
+ &base_context,
758
+ );
759
+
760
+ // Test with various caller-supplied attributes
761
+ let test_cases = vec![
762
+ json!({"_id": "proj1", "organization": "org-A", "is_member": "true"}),
763
+ json!({"_id": "proj1", "organization": "org-A", "is_member": "false"}),
764
+ json!({"_id": "proj1", "organization": "org-A", "role": "admin"}),
765
+ json!({"_id": "proj1", "organization": "org-A", "role": "member"}),
766
+ json!({"_id": "proj1", "organization": "org-A", "permissions": ["read", "write"]}),
767
+ ];
768
+
769
+ for test_record in test_cases {
770
+ let test_context = policy_evaluation::RecordAuthorizationContext::new(
771
+ Some(policy_evaluation::Actor::new("alice")),
772
+ "Project",
773
+ "proj1",
774
+ test_record,
775
+ )
776
+ .with_state(auth_state.clone());
777
+
778
+ let test_result = policy_evaluation::PolicyEvaluator::evaluate_record(
779
+ policy_evaluation::PolicySubject::Member,
780
+ &test_context,
781
+ );
782
+
783
+ // All should have the same result as base case
784
+ assert_eq!(base_result.is_ok(), test_result.is_ok());
785
+ }
786
+ }
787
+ }