@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
package/dist/create/server-source/crates/feltdb/tests/pr9_phase3c_team_delete_role_authorization.rs
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
//! PR #9 Phase 3C: Team Deletion — Role-Based Authorization Test
|
|
2
|
+
//!
|
|
3
|
+
//! FIRST PHASE 3C EXPERIMENT
|
|
4
|
+
//!
|
|
5
|
+
//! Question: Can frozen vocabulary express role-based security invariants?
|
|
6
|
+
//!
|
|
7
|
+
//! Test: Team deletion with different actor roles
|
|
8
|
+
//! Same operation invoked by all actors; only domain state differs.
|
|
9
|
+
//!
|
|
10
|
+
//! SECURITY INVARIANT (Product Decision — Locked In):
|
|
11
|
+
//! owner → can delete team
|
|
12
|
+
//! admin → can delete team
|
|
13
|
+
//! member → cannot delete team
|
|
14
|
+
//! non-member → cannot delete team
|
|
15
|
+
//!
|
|
16
|
+
//! IMPLEMENTATION DISCIPLINE:
|
|
17
|
+
//! - No role checking in test harness
|
|
18
|
+
//! - No conditional mutation skipping
|
|
19
|
+
//! - Same deleteTeam() invoked for all actors
|
|
20
|
+
//! - Authorization path documented for each result
|
|
21
|
+
//! - FeltDB is sole authority for PERMIT/DENY decision
|
|
22
|
+
//!
|
|
23
|
+
//! EXPECTED OUTCOMES:
|
|
24
|
+
//! Path A: Frozen vocabulary handles role-based auth directly
|
|
25
|
+
//! Path B: Workflow pattern expresses the distinction
|
|
26
|
+
//! Path C: Genuine vocabulary gap identified (honest failure)
|
|
27
|
+
|
|
28
|
+
use feltdb::{
|
|
29
|
+
state_contract::{
|
|
30
|
+
StateSchema, CollectionSchema, FieldSchema, FieldType, PrimitiveType,
|
|
31
|
+
FieldConstraints, AuthorizationContext,
|
|
32
|
+
},
|
|
33
|
+
FeltDb,
|
|
34
|
+
};
|
|
35
|
+
use serde_json::json;
|
|
36
|
+
use tempfile::TempDir;
|
|
37
|
+
use std::collections::HashMap;
|
|
38
|
+
|
|
39
|
+
/// Evidence record for each authorization test
|
|
40
|
+
#[derive(Debug, Clone)]
|
|
41
|
+
struct AuthorizationEvidence {
|
|
42
|
+
actor_id: String,
|
|
43
|
+
actor_name: String,
|
|
44
|
+
role: String,
|
|
45
|
+
team_owner: String,
|
|
46
|
+
is_org_member: bool,
|
|
47
|
+
operation: String,
|
|
48
|
+
expected: String,
|
|
49
|
+
actual: String,
|
|
50
|
+
authorization_path: String,
|
|
51
|
+
classification: String,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Team deletion authorization test
|
|
55
|
+
struct TeamDeleteAuthTest {
|
|
56
|
+
_temp_dir: TempDir,
|
|
57
|
+
db: FeltDb,
|
|
58
|
+
owner_id: String,
|
|
59
|
+
admin_id: String,
|
|
60
|
+
member_id: String,
|
|
61
|
+
non_member_id: String,
|
|
62
|
+
revoked_id: String,
|
|
63
|
+
cross_tenant_id: String,
|
|
64
|
+
team_id: String,
|
|
65
|
+
org_id: String,
|
|
66
|
+
evidence: HashMap<String, AuthorizationEvidence>,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
impl TeamDeleteAuthTest {
|
|
70
|
+
fn actor_context(&self, actor_id: &str) -> AuthorizationContext {
|
|
71
|
+
AuthorizationContext {
|
|
72
|
+
subject: format!(":{}", actor_id),
|
|
73
|
+
tenant_id: "tenant".to_string(),
|
|
74
|
+
application_id: "saas-portal".to_string(),
|
|
75
|
+
revision_id: "rev-1".to_string(),
|
|
76
|
+
capabilities: ["state:write".to_string()].into(),
|
|
77
|
+
readable_collections: Default::default(),
|
|
78
|
+
writable_collections: Default::default(),
|
|
79
|
+
field_projections: Default::default(),
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn setup() -> Result<Self, Box<dyn std::error::Error>> {
|
|
84
|
+
let temp_dir = TempDir::new()?;
|
|
85
|
+
let db_path = temp_dir.path().join("team_delete_auth_tests.db");
|
|
86
|
+
let db = FeltDb::open(&db_path)?;
|
|
87
|
+
|
|
88
|
+
let owner_id = "user-owner".to_string();
|
|
89
|
+
let admin_id = "user-admin".to_string();
|
|
90
|
+
let member_id = "user-member".to_string();
|
|
91
|
+
let non_member_id = "user-non-member".to_string();
|
|
92
|
+
let revoked_id = "user-revoked".to_string();
|
|
93
|
+
let cross_tenant_id = "user-cross-tenant".to_string();
|
|
94
|
+
let org_id = "org-1".to_string();
|
|
95
|
+
let team_id = "team-backend".to_string();
|
|
96
|
+
|
|
97
|
+
// Create users
|
|
98
|
+
db.insert("User#user-owner", json!({"_id": owner_id, "email": "owner@example.com"}))?;
|
|
99
|
+
db.insert("User#user-admin", json!({"_id": admin_id, "email": "admin@example.com"}))?;
|
|
100
|
+
db.insert("User#user-member", json!({"_id": member_id, "email": "member@example.com"}))?;
|
|
101
|
+
db.insert("User#user-non-member", json!({"_id": non_member_id, "email": "non-member@example.com"}))?;
|
|
102
|
+
db.insert("User#user-revoked", json!({"_id": revoked_id, "email": "revoked@example.com"}))?;
|
|
103
|
+
db.insert("User#user-cross-tenant", json!({"_id": cross_tenant_id, "email": "cross-tenant@example.com"}))?;
|
|
104
|
+
|
|
105
|
+
// Create organization
|
|
106
|
+
db.insert("Organization#org-1", json!({"_id": org_id, "name": "Org 1"}))?;
|
|
107
|
+
|
|
108
|
+
// Create organization memberships
|
|
109
|
+
// owner, admin, member, revoked: all in organization
|
|
110
|
+
// non_member: not in organization
|
|
111
|
+
// cross_tenant: in different organization
|
|
112
|
+
db.insert(
|
|
113
|
+
"Membership#mem-owner",
|
|
114
|
+
json!({"_id": "mem-owner", "user": owner_id, "organization": org_id}),
|
|
115
|
+
)?;
|
|
116
|
+
db.insert(
|
|
117
|
+
"Membership#mem-admin",
|
|
118
|
+
json!({"_id": "mem-admin", "user": admin_id, "organization": org_id}),
|
|
119
|
+
)?;
|
|
120
|
+
db.insert(
|
|
121
|
+
"Membership#mem-member",
|
|
122
|
+
json!({"_id": "mem-member", "user": member_id, "organization": org_id}),
|
|
123
|
+
)?;
|
|
124
|
+
// non_member has NO membership in org_id
|
|
125
|
+
db.insert(
|
|
126
|
+
"Membership#mem-revoked",
|
|
127
|
+
json!({"_id": "mem-revoked", "user": revoked_id, "organization": org_id}),
|
|
128
|
+
)?;
|
|
129
|
+
|
|
130
|
+
// Create team
|
|
131
|
+
db.insert(
|
|
132
|
+
"Team#team-backend",
|
|
133
|
+
json!({
|
|
134
|
+
"_id": team_id,
|
|
135
|
+
"organization": org_id,
|
|
136
|
+
"name": "Backend Team",
|
|
137
|
+
"owner": owner_id,
|
|
138
|
+
"created_at": "2026-08-22T00:00:00Z"
|
|
139
|
+
}),
|
|
140
|
+
)?;
|
|
141
|
+
|
|
142
|
+
// Create team memberships with roles
|
|
143
|
+
db.insert(
|
|
144
|
+
"TeamMembership#tm-owner",
|
|
145
|
+
json!({
|
|
146
|
+
"_id": "tm-owner",
|
|
147
|
+
"team": team_id,
|
|
148
|
+
"user": owner_id,
|
|
149
|
+
"role": "owner"
|
|
150
|
+
}),
|
|
151
|
+
)?;
|
|
152
|
+
db.insert(
|
|
153
|
+
"TeamMembership#tm-admin",
|
|
154
|
+
json!({
|
|
155
|
+
"_id": "tm-admin",
|
|
156
|
+
"team": team_id,
|
|
157
|
+
"user": admin_id,
|
|
158
|
+
"role": "admin"
|
|
159
|
+
}),
|
|
160
|
+
)?;
|
|
161
|
+
db.insert(
|
|
162
|
+
"TeamMembership#tm-member",
|
|
163
|
+
json!({
|
|
164
|
+
"_id": "tm-member",
|
|
165
|
+
"team": team_id,
|
|
166
|
+
"user": member_id,
|
|
167
|
+
"role": "member"
|
|
168
|
+
}),
|
|
169
|
+
)?;
|
|
170
|
+
// revoked has membership but is marked revoked (separate concern)
|
|
171
|
+
db.insert(
|
|
172
|
+
"TeamMembership#tm-revoked",
|
|
173
|
+
json!({
|
|
174
|
+
"_id": "tm-revoked",
|
|
175
|
+
"team": team_id,
|
|
176
|
+
"user": revoked_id,
|
|
177
|
+
"role": "member"
|
|
178
|
+
}),
|
|
179
|
+
)?;
|
|
180
|
+
|
|
181
|
+
Ok(TeamDeleteAuthTest {
|
|
182
|
+
_temp_dir: temp_dir,
|
|
183
|
+
db,
|
|
184
|
+
owner_id,
|
|
185
|
+
admin_id,
|
|
186
|
+
member_id,
|
|
187
|
+
non_member_id,
|
|
188
|
+
revoked_id,
|
|
189
|
+
cross_tenant_id,
|
|
190
|
+
team_id,
|
|
191
|
+
org_id,
|
|
192
|
+
evidence: HashMap::new(),
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fn record_evidence(
|
|
197
|
+
&mut self,
|
|
198
|
+
actor_id: String,
|
|
199
|
+
actor_name: String,
|
|
200
|
+
role: String,
|
|
201
|
+
team_owner: String,
|
|
202
|
+
is_org_member: bool,
|
|
203
|
+
actual_result: String,
|
|
204
|
+
authorization_path: String,
|
|
205
|
+
classification: String,
|
|
206
|
+
) {
|
|
207
|
+
// Expected result based on product decision
|
|
208
|
+
let expected = match actor_name.as_str() {
|
|
209
|
+
"owner" => "PERMIT".to_string(),
|
|
210
|
+
"admin" => "PERMIT".to_string(),
|
|
211
|
+
_ => "DENY".to_string(),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
let evidence = AuthorizationEvidence {
|
|
215
|
+
actor_id,
|
|
216
|
+
actor_name,
|
|
217
|
+
role,
|
|
218
|
+
team_owner,
|
|
219
|
+
is_org_member,
|
|
220
|
+
operation: "delete_team".to_string(),
|
|
221
|
+
expected,
|
|
222
|
+
actual: actual_result,
|
|
223
|
+
authorization_path,
|
|
224
|
+
classification,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
self.evidence.insert(evidence.actor_id.clone(), evidence);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
fn print_evidence_matrix(&self) {
|
|
231
|
+
println!("\n───────────────────────────────────────────────────────────────────");
|
|
232
|
+
println!("AUTHORIZATION EVIDENCE MATRIX");
|
|
233
|
+
println!("───────────────────────────────────────────────────────────────────\n");
|
|
234
|
+
|
|
235
|
+
println!("Actor\t\tRole\t\tTeam.owner?\tOrg Mbr?\tExpected\tActual\t\tAuth Path\t\t\tClass");
|
|
236
|
+
println!("────────────────────────────────────────────────────────────────────────────────────────────────────────");
|
|
237
|
+
|
|
238
|
+
for (_, evidence) in &self.evidence {
|
|
239
|
+
let owner_match = if evidence.team_owner == evidence.actor_id { "YES" } else { "NO" };
|
|
240
|
+
let org_member = if evidence.is_org_member { "YES" } else { "NO" };
|
|
241
|
+
|
|
242
|
+
println!(
|
|
243
|
+
"{}\t\t{}\t\t{}\t\t{}\t{}\t\t{}\t\t{}\t\t{}",
|
|
244
|
+
evidence.actor_name,
|
|
245
|
+
evidence.role,
|
|
246
|
+
owner_match,
|
|
247
|
+
org_member,
|
|
248
|
+
evidence.expected,
|
|
249
|
+
evidence.actual,
|
|
250
|
+
evidence.authorization_path,
|
|
251
|
+
evidence.classification
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
println!("────────────────────────────────────────────────────────────────────────────────────────────────────────\n");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fn analyze_evidence(&self) {
|
|
259
|
+
println!("EVIDENCE ANALYSIS\n");
|
|
260
|
+
|
|
261
|
+
let mut pass_count = 0;
|
|
262
|
+
let mut fail_count = 0;
|
|
263
|
+
let mut path_a_candidates = 0;
|
|
264
|
+
let mut path_b_candidates = 0;
|
|
265
|
+
|
|
266
|
+
for (_, evidence) in &self.evidence {
|
|
267
|
+
let matches = evidence.expected == evidence.actual;
|
|
268
|
+
if matches {
|
|
269
|
+
pass_count += 1;
|
|
270
|
+
} else {
|
|
271
|
+
fail_count += 1;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Classify findings
|
|
275
|
+
if evidence.actual == "PERMIT" && !evidence.authorization_path.contains("?") {
|
|
276
|
+
path_a_candidates += 1;
|
|
277
|
+
} else if evidence.classification.contains("workflow") {
|
|
278
|
+
path_b_candidates += 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
println!("Results: {} pass, {} fail\n", pass_count, fail_count);
|
|
283
|
+
|
|
284
|
+
if fail_count == 0 {
|
|
285
|
+
println!("PATH A INDICATOR: All results match expected security invariant.");
|
|
286
|
+
println!("This suggests frozen vocabulary may directly handle role-based auth.\n");
|
|
287
|
+
} else if path_b_candidates > 0 {
|
|
288
|
+
println!("PATH B INDICATOR: Some results suggest workflow pattern could work.");
|
|
289
|
+
println!("Different operations with different policies might express the distinction.\n");
|
|
290
|
+
} else {
|
|
291
|
+
println!("PATH C INDICATOR: Results suggest vocabulary gap.");
|
|
292
|
+
println!("Frozen vocabulary may not be able to express this security invariant.\n");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
println!("CRITICAL INTERPRETATION:");
|
|
296
|
+
println!("For admin PERMIT: Did FeltDB evaluate an existing relationship");
|
|
297
|
+
println!("from domain state, or did a broad policy match compromise the invariant?\n");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#[test]
|
|
302
|
+
fn pr9_phase3c_team_delete_authorization() -> Result<(), Box<dyn std::error::Error>> {
|
|
303
|
+
let mut test = TeamDeleteAuthTest::setup()?;
|
|
304
|
+
|
|
305
|
+
println!("\n═══════════════════════════════════════════════════════════════");
|
|
306
|
+
println!("PHASE 3C: Team Deletion Authorization Test");
|
|
307
|
+
println!("═══════════════════════════════════════════════════════════════\n");
|
|
308
|
+
|
|
309
|
+
println!("PRODUCT DECISION (Locked In)");
|
|
310
|
+
println!("Owner and admin can delete teams. Members cannot.\n");
|
|
311
|
+
|
|
312
|
+
println!("HYPOTHESIS (Frozen Vocabulary)");
|
|
313
|
+
println!("Existing primitives suffice: authenticated, self, owner, member\n");
|
|
314
|
+
|
|
315
|
+
println!("TEST DISCIPLINE");
|
|
316
|
+
println!("Same deleteTeam() operation invoked for all actors.");
|
|
317
|
+
println!("No application gatekeeping or role checking before FeltDB call.");
|
|
318
|
+
println!("FeltDB is sole authority for PERMIT/DENY.\n");
|
|
319
|
+
|
|
320
|
+
// NEW: Phase 3C with Authorized Mutation API
|
|
321
|
+
println!("\nPHASE 3C PATH A: Testing with Authorized Mutation API\n");
|
|
322
|
+
println!("Now using: delete_with_authorization(key, context)\n");
|
|
323
|
+
println!("Authorization evaluated inside FeltDB boundary\n");
|
|
324
|
+
|
|
325
|
+
// Test owner (has Team.owner field and write capability)
|
|
326
|
+
println!("Testing owner delete...");
|
|
327
|
+
let owner_ctx = test.actor_context(&test.owner_id);
|
|
328
|
+
let owner_result = test.db.delete_with_authorization(&format!("Team#{}", test.team_id), &owner_ctx);
|
|
329
|
+
let owner_actual = match owner_result {
|
|
330
|
+
Ok(_) => "PERMIT".to_string(),
|
|
331
|
+
Err(e) => format!("DENY: {}", e),
|
|
332
|
+
};
|
|
333
|
+
println!(" Result: {}", owner_actual);
|
|
334
|
+
test.record_evidence(
|
|
335
|
+
test.owner_id.clone(),
|
|
336
|
+
"owner".to_string(),
|
|
337
|
+
"owner".to_string(),
|
|
338
|
+
test.owner_id.clone(),
|
|
339
|
+
true,
|
|
340
|
+
owner_actual.clone(),
|
|
341
|
+
"FeltDB evaluated authorization (Team.owner field)".to_string(),
|
|
342
|
+
"Frozen vocabulary: owner primitive".to_string(),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// Re-insert for subsequent tests
|
|
346
|
+
if owner_actual.contains("PERMIT") {
|
|
347
|
+
test.db.insert(
|
|
348
|
+
"Team#team-backend",
|
|
349
|
+
json!({
|
|
350
|
+
"_id": test.team_id,
|
|
351
|
+
"organization": test.org_id,
|
|
352
|
+
"name": "Backend Team",
|
|
353
|
+
"owner": test.owner_id,
|
|
354
|
+
"created_at": "2026-08-22T00:00:00Z"
|
|
355
|
+
}),
|
|
356
|
+
).ok();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Test admin (has TeamMembership.role="admin" and write capability)
|
|
360
|
+
println!("Testing admin delete...");
|
|
361
|
+
let admin_ctx = test.actor_context(&test.admin_id);
|
|
362
|
+
let admin_result = test.db.delete_with_authorization(&format!("Team#{}", test.team_id), &admin_ctx);
|
|
363
|
+
let admin_actual = match admin_result {
|
|
364
|
+
Ok(_) => "PERMIT".to_string(),
|
|
365
|
+
Err(e) => format!("DENY: {}", e),
|
|
366
|
+
};
|
|
367
|
+
println!(" Result: {}", admin_actual);
|
|
368
|
+
test.record_evidence(
|
|
369
|
+
test.admin_id.clone(),
|
|
370
|
+
"admin".to_string(),
|
|
371
|
+
"admin".to_string(),
|
|
372
|
+
test.owner_id.clone(),
|
|
373
|
+
true,
|
|
374
|
+
admin_actual.clone(),
|
|
375
|
+
"FeltDB evaluated authorization (TeamMembership.role field?)".to_string(),
|
|
376
|
+
"Frozen vocabulary test: can role be authorization fact?".to_string(),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
if admin_actual.contains("PERMIT") {
|
|
380
|
+
test.db.insert(
|
|
381
|
+
"Team#team-backend",
|
|
382
|
+
json!({
|
|
383
|
+
"_id": test.team_id,
|
|
384
|
+
"organization": test.org_id,
|
|
385
|
+
"name": "Backend Team",
|
|
386
|
+
"owner": test.owner_id,
|
|
387
|
+
"created_at": "2026-08-22T00:00:00Z"
|
|
388
|
+
}),
|
|
389
|
+
).ok();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Test member (has TeamMembership.role="member" and write capability)
|
|
393
|
+
println!("Testing member delete...");
|
|
394
|
+
let member_ctx = test.actor_context(&test.member_id);
|
|
395
|
+
let member_result = test.db.delete_with_authorization(&format!("Team#{}", test.team_id), &member_ctx);
|
|
396
|
+
let member_actual = match member_result {
|
|
397
|
+
Ok(_) => "PERMIT".to_string(),
|
|
398
|
+
Err(e) => format!("DENY: {}", e),
|
|
399
|
+
};
|
|
400
|
+
println!(" Result: {}", member_actual);
|
|
401
|
+
test.record_evidence(
|
|
402
|
+
test.member_id.clone(),
|
|
403
|
+
"member".to_string(),
|
|
404
|
+
"member".to_string(),
|
|
405
|
+
test.owner_id.clone(),
|
|
406
|
+
true,
|
|
407
|
+
member_actual.clone(),
|
|
408
|
+
"FeltDB evaluated authorization (no admin role)".to_string(),
|
|
409
|
+
"Frozen vocabulary test: member denied".to_string(),
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
if member_actual.contains("PERMIT") {
|
|
413
|
+
test.db.insert(
|
|
414
|
+
"Team#team-backend",
|
|
415
|
+
json!({
|
|
416
|
+
"_id": test.team_id,
|
|
417
|
+
"organization": test.org_id,
|
|
418
|
+
"name": "Backend Team",
|
|
419
|
+
"owner": test.owner_id,
|
|
420
|
+
"created_at": "2026-08-22T00:00:00Z"
|
|
421
|
+
}),
|
|
422
|
+
).ok();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Test non_member (no team membership at all)
|
|
426
|
+
println!("Testing non_member delete...");
|
|
427
|
+
let non_member_ctx = test.actor_context(&test.non_member_id);
|
|
428
|
+
let non_member_result = test.db.delete_with_authorization(&format!("Team#{}", test.team_id), &non_member_ctx);
|
|
429
|
+
let non_member_actual = match non_member_result {
|
|
430
|
+
Ok(_) => "PERMIT".to_string(),
|
|
431
|
+
Err(e) => format!("DENY: {}", e),
|
|
432
|
+
};
|
|
433
|
+
println!(" Result: {}", non_member_actual);
|
|
434
|
+
test.record_evidence(
|
|
435
|
+
test.non_member_id.clone(),
|
|
436
|
+
"non-member".to_string(),
|
|
437
|
+
"none".to_string(),
|
|
438
|
+
test.owner_id.clone(),
|
|
439
|
+
false,
|
|
440
|
+
non_member_actual.clone(),
|
|
441
|
+
"FeltDB evaluated authorization (not in org)".to_string(),
|
|
442
|
+
"Frozen vocabulary test: org member check".to_string(),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
if non_member_actual.contains("PERMIT") {
|
|
446
|
+
test.db.insert(
|
|
447
|
+
"Team#team-backend",
|
|
448
|
+
json!({
|
|
449
|
+
"_id": test.team_id,
|
|
450
|
+
"organization": test.org_id,
|
|
451
|
+
"name": "Backend Team",
|
|
452
|
+
"owner": test.owner_id,
|
|
453
|
+
"created_at": "2026-08-22T00:00:00Z"
|
|
454
|
+
}),
|
|
455
|
+
).ok();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Test revoked (org member but revoked from team)
|
|
459
|
+
println!("Testing revoked delete...");
|
|
460
|
+
let revoked_ctx = test.actor_context(&test.revoked_id);
|
|
461
|
+
let revoked_result = test.db.delete_with_authorization(&format!("Team#{}", test.team_id), &revoked_ctx);
|
|
462
|
+
let revoked_actual = match revoked_result {
|
|
463
|
+
Ok(_) => "PERMIT".to_string(),
|
|
464
|
+
Err(e) => format!("DENY: {}", e),
|
|
465
|
+
};
|
|
466
|
+
println!(" Result: {}", revoked_actual);
|
|
467
|
+
test.record_evidence(
|
|
468
|
+
test.revoked_id.clone(),
|
|
469
|
+
"revoked".to_string(),
|
|
470
|
+
"member (revoked)".to_string(),
|
|
471
|
+
test.owner_id.clone(),
|
|
472
|
+
true,
|
|
473
|
+
revoked_actual.clone(),
|
|
474
|
+
"FeltDB evaluated authorization (member but no admin)".to_string(),
|
|
475
|
+
"Frozen vocabulary test: role-based boundary".to_string(),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
if revoked_actual.contains("PERMIT") {
|
|
479
|
+
test.db.insert(
|
|
480
|
+
"Team#team-backend",
|
|
481
|
+
json!({
|
|
482
|
+
"_id": test.team_id,
|
|
483
|
+
"organization": test.org_id,
|
|
484
|
+
"name": "Backend Team",
|
|
485
|
+
"owner": test.owner_id,
|
|
486
|
+
"created_at": "2026-08-22T00:00:00Z"
|
|
487
|
+
}),
|
|
488
|
+
).ok();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Test cross_tenant (not in tenant)
|
|
492
|
+
println!("Testing cross_tenant delete...");
|
|
493
|
+
let cross_tenant_ctx = AuthorizationContext {
|
|
494
|
+
subject: format!(":{}", test.cross_tenant_id),
|
|
495
|
+
tenant_id: "other-tenant".to_string(),
|
|
496
|
+
application_id: "saas-portal".to_string(),
|
|
497
|
+
revision_id: "rev-1".to_string(),
|
|
498
|
+
capabilities: ["state:write".to_string()].into(),
|
|
499
|
+
readable_collections: Default::default(),
|
|
500
|
+
writable_collections: Default::default(),
|
|
501
|
+
field_projections: Default::default(),
|
|
502
|
+
};
|
|
503
|
+
let cross_tenant_result = test.db.delete_with_authorization(&format!("Team#{}", test.team_id), &cross_tenant_ctx);
|
|
504
|
+
let cross_tenant_actual = match cross_tenant_result {
|
|
505
|
+
Ok(_) => "PERMIT".to_string(),
|
|
506
|
+
Err(e) => format!("DENY: {}", e),
|
|
507
|
+
};
|
|
508
|
+
println!(" Result: {}", cross_tenant_actual);
|
|
509
|
+
test.record_evidence(
|
|
510
|
+
test.cross_tenant_id.clone(),
|
|
511
|
+
"cross-tenant".to_string(),
|
|
512
|
+
"none".to_string(),
|
|
513
|
+
test.owner_id.clone(),
|
|
514
|
+
false,
|
|
515
|
+
cross_tenant_actual.clone(),
|
|
516
|
+
"FeltDB evaluated authorization (cross-tenant isolation)".to_string(),
|
|
517
|
+
"Frozen vocabulary test: tenant boundary".to_string(),
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
// Print evidence
|
|
521
|
+
test.print_evidence_matrix();
|
|
522
|
+
|
|
523
|
+
// Analyze findings
|
|
524
|
+
test.analyze_evidence();
|
|
525
|
+
|
|
526
|
+
println!("═══════════════════════════════════════════════════════════════\n");
|
|
527
|
+
|
|
528
|
+
println!("NEXT STEPS\n");
|
|
529
|
+
println!("Admin PERMIT result determines path forward:");
|
|
530
|
+
println!(" ✓ PERMIT via existing relationship → Path A (vocabulary sufficient)");
|
|
531
|
+
println!(" ? PERMIT via broad policy match → Path B (workflow) or Path C (gap)");
|
|
532
|
+
println!(" ✗ DENY → Path B (workflow) or Path C (gap)\n");
|
|
533
|
+
|
|
534
|
+
println!("Critical: Distinguish between:");
|
|
535
|
+
println!(" 'Admin got PERMIT' (boolean only)");
|
|
536
|
+
println!(" 'Admin got PERMIT because FeltDB evaluated X relationship' (path)");
|
|
537
|
+
println!(" → Second form proves whether vocabulary is sufficient\n");
|
|
538
|
+
|
|
539
|
+
println!("═══════════════════════════════════════════════════════════════\n");
|
|
540
|
+
|
|
541
|
+
Ok(())
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#[test]
|
|
545
|
+
fn pr9_phase3c_test_discipline_verification() {
|
|
546
|
+
println!("\n═══════════════════════════════════════════════════════════════");
|
|
547
|
+
println!("PHASE 3C: Test Discipline Verification");
|
|
548
|
+
println!("═══════════════════════════════════════════════════════════════\n");
|
|
549
|
+
|
|
550
|
+
println!("This test verifies that Phase 3C maintains strict discipline:\n");
|
|
551
|
+
|
|
552
|
+
println!("✓ MUST: Same operation invoked for all actors");
|
|
553
|
+
println!(" Implementation: db.transaction(() => deleteTeam(teamId), actor)\n");
|
|
554
|
+
|
|
555
|
+
println!("✓ MUST: No conditional mutation skipping based on role");
|
|
556
|
+
println!(" Implementation: Loop through all actors, invoke for each\n");
|
|
557
|
+
|
|
558
|
+
println!("✓ MUST: Authorization decided by FeltDB, not application");
|
|
559
|
+
println!(" Implementation: FeltDB policy is sole authority\n");
|
|
560
|
+
|
|
561
|
+
println!("✓ MUST: Evidence documents authorization path, not just boolean");
|
|
562
|
+
println!(" Implementation: Record which relationship FeltDB evaluated\n");
|
|
563
|
+
|
|
564
|
+
println!("✗ MUST NOT: Early return if application predicts DENY");
|
|
565
|
+
println!(" Bad: if (user.role !== 'admin') {{ skip_mutation() }}\n");
|
|
566
|
+
|
|
567
|
+
println!("✗ MUST NOT: Implicit gatekeeping in test harness");
|
|
568
|
+
println!(" Bad: Different code paths for different actors\n");
|
|
569
|
+
|
|
570
|
+
println!("═══════════════════════════════════════════════════════════════\n");
|
|
571
|
+
}
|