@feltdb/core 0.4.15 → 0.4.16
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/dist/cli/index.js +1 -1
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/Cargo.toml +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +93 -0
- package/dist/create/server-source/crates/feltdb/src/authorization_security_tests.rs +787 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +139 -0
- package/dist/create/server-source/crates/feltdb/src/policy_evaluation.rs +1669 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +1269 -15
- package/dist/create/server-source/crates/feltdb/tests/pr7_self_authorization_proof.rs +406 -0
- package/dist/create/server-source/crates/feltdb/tests/pr8_vocabulary_assessment.rs +908 -0
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase2_boundary_tests.rs +1028 -0
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3a_path_a_tests.rs +332 -0
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_authorized_mutations.rs +342 -0
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_role_based_authorization.rs +313 -0
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_simple_auth_delete.rs +90 -0
- package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs +571 -0
- package/dist/create/server-source/crates/feltdb/tests/pr9_teams_role_based_access.rs +506 -0
- package/dist/create/server-source/crates/feltdb/tests/saas_authorization_integration.rs +81 -0
- package/dist/create/server-source/crates/feltdb/tests/saas_invitation_lifecycle.rs +434 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +130 -23
- package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +2 -2
- package/dist/db.d.ts +10 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +3 -1
- package/dist/file-db.d.ts +69 -0
- package/dist/file-db.d.ts.map +1 -0
- package/dist/file-db.js +355 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/studio-app/assets/{feltdb_wasm-CJEJryDx.js → feltdb_wasm-CBGD0zRu.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-C6ATF9mJ.wasm +0 -0
- package/dist/studio-app/assets/{index-D_p8T7nO.js → index-D74dfBgZ.js} +9 -9
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/package.json +7 -2
- 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
|
+
}
|