@feltdb/core 0.5.7 → 0.6.1
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/commands.js +110 -5
- package/dist/cli/index.js +1 -1
- package/dist/cli/workspace-integration.js +127 -0
- package/dist/create/cli.js +1 -1
- package/dist/create/create.js +21 -0
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +10 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +5 -1
- package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +273 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +528 -0
- package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +472 -0
- package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +992 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +9 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +157 -9
- package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +413 -0
- package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +488 -0
- package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +378 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +49 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +525 -0
- package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +461 -0
- package/dist/create/template/dot-feltdb-README.md +58 -0
- package/dist/create/workspace-initialization.js +77 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/studio-app/assets/{feltdb_wasm-h9mxesnH.js → feltdb_wasm-B4wq4mqp.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
- package/dist/studio-app/assets/{index-B_EMnTaE.js → index-LQmvJSq6.js} +2 -2
- package/dist/studio-app/index.html +1 -1
- package/dist/telemetry.js +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/development-node.d.ts +42 -0
- package/dist/workspace/development-node.d.ts.map +1 -0
- package/dist/workspace/development-node.js +208 -0
- package/dist/workspace/index.d.ts +20 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +16 -0
- package/dist/workspace/workspace-connection.d.ts +174 -0
- package/dist/workspace/workspace-connection.d.ts.map +1 -0
- package/dist/workspace/workspace-connection.js +290 -0
- package/dist/workspace/workspace-identity.d.ts +13 -0
- package/dist/workspace/workspace-identity.d.ts.map +1 -0
- package/dist/workspace/workspace-identity.js +70 -0
- package/dist/workspace/workspace-types.d.ts +82 -0
- package/dist/workspace/workspace-types.d.ts.map +1 -0
- package/dist/workspace/workspace-types.js +7 -0
- package/package.json +5 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-B8U4A1n1.wasm +0 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use serde_json::Value;
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
|
|
5
|
+
/// Explicit membership policy configuration.
|
|
6
|
+
/// Replaces name discovery with manifest configuration.
|
|
7
|
+
/// No magic User/Organization/Membership relationships.
|
|
8
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
9
|
+
pub struct MembershipPolicy {
|
|
10
|
+
/// Name of this membership relationship (e.g., "organization_members", "team_members").
|
|
11
|
+
pub name: String,
|
|
12
|
+
|
|
13
|
+
/// Collection where membership records are stored.
|
|
14
|
+
pub collection: String,
|
|
15
|
+
|
|
16
|
+
/// Field in membership record that identifies the actor (e.g., "user_id").
|
|
17
|
+
pub actor_field: String,
|
|
18
|
+
|
|
19
|
+
/// Field in membership record that identifies the tenant (e.g., "organization_id").
|
|
20
|
+
pub tenant_field: String,
|
|
21
|
+
|
|
22
|
+
/// Optional role field in membership record (e.g., "role").
|
|
23
|
+
pub role_field: Option<String>,
|
|
24
|
+
|
|
25
|
+
/// Optional status field (e.g., "status" with values "active", "suspended").
|
|
26
|
+
pub status_field: Option<String>,
|
|
27
|
+
|
|
28
|
+
/// Statuses considered "active" for membership verification.
|
|
29
|
+
#[serde(default = "default_active_statuses")]
|
|
30
|
+
pub active_statuses: Vec<String>,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn default_active_statuses() -> Vec<String> {
|
|
34
|
+
vec!["active".into(), "verified".into()]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Store for membership policies.
|
|
38
|
+
#[derive(Debug, Clone)]
|
|
39
|
+
pub struct MembershipPolicyStore {
|
|
40
|
+
policies: HashMap<String, MembershipPolicy>,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl MembershipPolicyStore {
|
|
44
|
+
/// Creates a new membership policy store.
|
|
45
|
+
pub fn new() -> Self {
|
|
46
|
+
Self {
|
|
47
|
+
policies: HashMap::new(),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Registers a new membership policy.
|
|
52
|
+
pub fn register(&mut self, policy: MembershipPolicy) {
|
|
53
|
+
self.policies.insert(policy.name.clone(), policy);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Gets a membership policy by name.
|
|
57
|
+
pub fn get(&self, name: &str) -> Option<&MembershipPolicy> {
|
|
58
|
+
self.policies.get(name)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Gets a policy by collection name.
|
|
62
|
+
pub fn get_by_collection(&self, collection: &str) -> Option<&MembershipPolicy> {
|
|
63
|
+
self.policies.values().find(|p| p.collection == collection)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Loads default policies.
|
|
67
|
+
pub fn with_defaults() -> Self {
|
|
68
|
+
let mut store = Self::new();
|
|
69
|
+
|
|
70
|
+
// Organization members policy
|
|
71
|
+
store.register(MembershipPolicy {
|
|
72
|
+
name: "organization_members".into(),
|
|
73
|
+
collection: "organization_members".into(),
|
|
74
|
+
actor_field: "user_id".into(),
|
|
75
|
+
tenant_field: "organization_id".into(),
|
|
76
|
+
role_field: Some("role".into()),
|
|
77
|
+
status_field: Some("status".into()),
|
|
78
|
+
active_statuses: vec!["active".into(), "verified".into()],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Team members policy
|
|
82
|
+
store.register(MembershipPolicy {
|
|
83
|
+
name: "team_members".into(),
|
|
84
|
+
collection: "team_members".into(),
|
|
85
|
+
actor_field: "user_id".into(),
|
|
86
|
+
tenant_field: "team_id".into(),
|
|
87
|
+
role_field: Some("role".into()),
|
|
88
|
+
status_field: Some("status".into()),
|
|
89
|
+
active_statuses: vec!["active".into()],
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
store
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
impl Default for MembershipPolicyStore {
|
|
97
|
+
fn default() -> Self {
|
|
98
|
+
Self::new()
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Membership verification context.
|
|
103
|
+
#[derive(Debug, Clone)]
|
|
104
|
+
pub struct MembershipVerificationContext {
|
|
105
|
+
pub actor_id: String,
|
|
106
|
+
pub tenant_id: String,
|
|
107
|
+
pub required_role: Option<String>,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Membership verification result.
|
|
111
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
112
|
+
pub enum MembershipDecision {
|
|
113
|
+
Verified { role: Option<String> },
|
|
114
|
+
Denied(String),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
impl MembershipDecision {
|
|
118
|
+
pub fn is_verified(&self) -> bool {
|
|
119
|
+
matches!(self, MembershipDecision::Verified { .. })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pub fn role(&self) -> Option<&str> {
|
|
123
|
+
match self {
|
|
124
|
+
MembershipDecision::Verified { role } => role.as_deref(),
|
|
125
|
+
MembershipDecision::Denied(_) => None,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pub fn error_message(&self) -> Option<&str> {
|
|
130
|
+
match self {
|
|
131
|
+
MembershipDecision::Verified { .. } => None,
|
|
132
|
+
MembershipDecision::Denied(msg) => Some(msg),
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Verifies membership relationships.
|
|
138
|
+
pub struct MembershipVerifier {
|
|
139
|
+
store: MembershipPolicyStore,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
impl MembershipVerifier {
|
|
143
|
+
pub fn new(store: MembershipPolicyStore) -> Self {
|
|
144
|
+
Self { store }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Verifies membership using a policy.
|
|
148
|
+
pub fn verify(
|
|
149
|
+
&self,
|
|
150
|
+
context: &MembershipVerificationContext,
|
|
151
|
+
policy_name: &str,
|
|
152
|
+
membership_record: &Value,
|
|
153
|
+
) -> MembershipDecision {
|
|
154
|
+
let policy = match self.store.get(policy_name) {
|
|
155
|
+
Some(p) => p,
|
|
156
|
+
None => return MembershipDecision::Denied(format!("policy not found: {}", policy_name)),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Verify actor field
|
|
160
|
+
let record_actor = self.extract_string(&policy.actor_field, membership_record);
|
|
161
|
+
if record_actor != Some(context.actor_id.clone()) {
|
|
162
|
+
return MembershipDecision::Denied(format!(
|
|
163
|
+
"actor mismatch: expected {} but got {:?}",
|
|
164
|
+
context.actor_id, record_actor
|
|
165
|
+
));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Verify tenant field
|
|
169
|
+
let record_tenant = self.extract_string(&policy.tenant_field, membership_record);
|
|
170
|
+
if record_tenant != Some(context.tenant_id.clone()) {
|
|
171
|
+
return MembershipDecision::Denied(format!(
|
|
172
|
+
"tenant mismatch: expected {} but got {:?}",
|
|
173
|
+
context.tenant_id, record_tenant
|
|
174
|
+
));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check status if configured
|
|
178
|
+
if let Some(status_field) = &policy.status_field {
|
|
179
|
+
let status = self.extract_string(status_field, membership_record);
|
|
180
|
+
if let Some(status_val) = status {
|
|
181
|
+
if !policy.active_statuses.contains(&status_val) {
|
|
182
|
+
return MembershipDecision::Denied(format!(
|
|
183
|
+
"membership status not active: {}",
|
|
184
|
+
status_val
|
|
185
|
+
));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Extract role
|
|
191
|
+
let role = policy
|
|
192
|
+
.role_field
|
|
193
|
+
.as_ref()
|
|
194
|
+
.and_then(|f| self.extract_string(f, membership_record));
|
|
195
|
+
|
|
196
|
+
// Verify required role if specified
|
|
197
|
+
if let Some(required) = &context.required_role {
|
|
198
|
+
if let Some(actual) = &role {
|
|
199
|
+
if actual != required {
|
|
200
|
+
return MembershipDecision::Denied(format!(
|
|
201
|
+
"role mismatch: required {} but got {}",
|
|
202
|
+
required, actual
|
|
203
|
+
));
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
return MembershipDecision::Denied(format!("required role not found: {}", required));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
MembershipDecision::Verified { role }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Verifies membership from a collection of records.
|
|
214
|
+
/// Returns Some(role) if verified, None if not found or denied.
|
|
215
|
+
pub fn verify_from_records(
|
|
216
|
+
&self,
|
|
217
|
+
context: &MembershipVerificationContext,
|
|
218
|
+
policy_name: &str,
|
|
219
|
+
records: &[Value],
|
|
220
|
+
) -> MembershipDecision {
|
|
221
|
+
// Find matching membership record
|
|
222
|
+
let policy = match self.store.get(policy_name) {
|
|
223
|
+
Some(p) => p,
|
|
224
|
+
None => return MembershipDecision::Denied(format!("policy not found: {}", policy_name)),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
for record in records {
|
|
228
|
+
let record_actor = self.extract_string(&policy.actor_field, record);
|
|
229
|
+
let record_tenant = self.extract_string(&policy.tenant_field, record);
|
|
230
|
+
|
|
231
|
+
if record_actor == Some(context.actor_id.clone())
|
|
232
|
+
&& record_tenant == Some(context.tenant_id.clone())
|
|
233
|
+
{
|
|
234
|
+
return self.verify(context, policy_name, record);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
MembershipDecision::Denied(format!(
|
|
239
|
+
"no membership record found for actor {} in tenant {}",
|
|
240
|
+
context.actor_id, context.tenant_id
|
|
241
|
+
))
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Gets all roles an actor has in a tenant.
|
|
245
|
+
pub fn get_roles(
|
|
246
|
+
&self,
|
|
247
|
+
actor_id: &str,
|
|
248
|
+
tenant_id: &str,
|
|
249
|
+
policy_name: &str,
|
|
250
|
+
records: &[Value],
|
|
251
|
+
) -> Vec<String> {
|
|
252
|
+
let policy = match self.store.get(policy_name) {
|
|
253
|
+
Some(p) => p,
|
|
254
|
+
None => return vec![],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
let mut roles = vec![];
|
|
258
|
+
|
|
259
|
+
for record in records {
|
|
260
|
+
let record_actor = self.extract_string(&policy.actor_field, record);
|
|
261
|
+
let record_tenant = self.extract_string(&policy.tenant_field, record);
|
|
262
|
+
|
|
263
|
+
if record_actor.as_deref() == Some(actor_id) && record_tenant.as_deref() == Some(tenant_id)
|
|
264
|
+
{
|
|
265
|
+
// Check status
|
|
266
|
+
if let Some(status_field) = &policy.status_field {
|
|
267
|
+
let status = self.extract_string(status_field, record);
|
|
268
|
+
if let Some(status_val) = status {
|
|
269
|
+
if !policy.active_statuses.contains(&status_val) {
|
|
270
|
+
continue; // Skip inactive records
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Extract role if configured
|
|
276
|
+
if let Some(role_field) = &policy.role_field {
|
|
277
|
+
if let Some(role) = self.extract_string(role_field, record) {
|
|
278
|
+
if !roles.contains(&role) {
|
|
279
|
+
roles.push(role);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
roles
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Extracts a string value from a record.
|
|
290
|
+
fn extract_string(&self, field: &str, record: &Value) -> Option<String> {
|
|
291
|
+
record
|
|
292
|
+
.get(field)
|
|
293
|
+
.and_then(|v| v.as_str())
|
|
294
|
+
.map(|s| s.to_string())
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#[cfg(test)]
|
|
299
|
+
mod tests {
|
|
300
|
+
use super::*;
|
|
301
|
+
use serde_json::json;
|
|
302
|
+
|
|
303
|
+
fn test_context() -> MembershipVerificationContext {
|
|
304
|
+
MembershipVerificationContext {
|
|
305
|
+
actor_id: "user_123".into(),
|
|
306
|
+
tenant_id: "org_acme".into(),
|
|
307
|
+
required_role: None,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
fn test_membership(user_id: &str, org_id: &str, role: &str, status: &str) -> Value {
|
|
312
|
+
json!({
|
|
313
|
+
"user_id": user_id,
|
|
314
|
+
"organization_id": org_id,
|
|
315
|
+
"role": role,
|
|
316
|
+
"status": status
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
fn test_verifier() -> MembershipVerifier {
|
|
321
|
+
MembershipVerifier::new(MembershipPolicyStore::with_defaults())
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
#[test]
|
|
325
|
+
fn verify_active_membership() {
|
|
326
|
+
let verifier = test_verifier();
|
|
327
|
+
let context = test_context();
|
|
328
|
+
let record = test_membership("user_123", "org_acme", "admin", "active");
|
|
329
|
+
|
|
330
|
+
let decision = verifier.verify(&context, "organization_members", &record);
|
|
331
|
+
assert_eq!(
|
|
332
|
+
decision,
|
|
333
|
+
MembershipDecision::Verified {
|
|
334
|
+
role: Some("admin".into())
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#[test]
|
|
340
|
+
fn deny_actor_mismatch() {
|
|
341
|
+
let verifier = test_verifier();
|
|
342
|
+
let context = test_context();
|
|
343
|
+
let record = test_membership("user_other", "org_acme", "admin", "active");
|
|
344
|
+
|
|
345
|
+
let decision = verifier.verify(&context, "organization_members", &record);
|
|
346
|
+
assert!(matches!(decision, MembershipDecision::Denied(_)));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#[test]
|
|
350
|
+
fn deny_tenant_mismatch() {
|
|
351
|
+
let verifier = test_verifier();
|
|
352
|
+
let context = test_context();
|
|
353
|
+
let record = test_membership("user_123", "org_other", "admin", "active");
|
|
354
|
+
|
|
355
|
+
let decision = verifier.verify(&context, "organization_members", &record);
|
|
356
|
+
assert!(matches!(decision, MembershipDecision::Denied(_)));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#[test]
|
|
360
|
+
fn deny_inactive_status() {
|
|
361
|
+
let verifier = test_verifier();
|
|
362
|
+
let context = test_context();
|
|
363
|
+
let record = test_membership("user_123", "org_acme", "admin", "suspended");
|
|
364
|
+
|
|
365
|
+
let decision = verifier.verify(&context, "organization_members", &record);
|
|
366
|
+
assert!(matches!(decision, MembershipDecision::Denied(_)));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
#[test]
|
|
370
|
+
fn verify_with_required_role() {
|
|
371
|
+
let verifier = test_verifier();
|
|
372
|
+
let mut context = test_context();
|
|
373
|
+
context.required_role = Some("admin".into());
|
|
374
|
+
let record = test_membership("user_123", "org_acme", "admin", "active");
|
|
375
|
+
|
|
376
|
+
let decision = verifier.verify(&context, "organization_members", &record);
|
|
377
|
+
assert_eq!(
|
|
378
|
+
decision,
|
|
379
|
+
MembershipDecision::Verified {
|
|
380
|
+
role: Some("admin".into())
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#[test]
|
|
386
|
+
fn deny_wrong_required_role() {
|
|
387
|
+
let verifier = test_verifier();
|
|
388
|
+
let mut context = test_context();
|
|
389
|
+
context.required_role = Some("owner".into());
|
|
390
|
+
let record = test_membership("user_123", "org_acme", "admin", "active");
|
|
391
|
+
|
|
392
|
+
let decision = verifier.verify(&context, "organization_members", &record);
|
|
393
|
+
assert!(matches!(decision, MembershipDecision::Denied(_)));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[test]
|
|
397
|
+
fn get_all_roles() {
|
|
398
|
+
let verifier = test_verifier();
|
|
399
|
+
let records = vec![
|
|
400
|
+
test_membership("user_123", "org_acme", "admin", "active"),
|
|
401
|
+
test_membership("user_123", "org_acme", "reviewer", "active"),
|
|
402
|
+
test_membership("user_123", "org_other", "admin", "active"),
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
let roles = verifier.get_roles("user_123", "org_acme", "organization_members", &records);
|
|
406
|
+
assert_eq!(roles.len(), 2);
|
|
407
|
+
assert!(roles.contains(&"admin".into()));
|
|
408
|
+
assert!(roles.contains(&"reviewer".into()));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
#[test]
|
|
412
|
+
fn verify_from_records_finds_matching() {
|
|
413
|
+
let verifier = test_verifier();
|
|
414
|
+
let context = test_context();
|
|
415
|
+
let records = vec![
|
|
416
|
+
test_membership("user_other", "org_acme", "admin", "active"),
|
|
417
|
+
test_membership("user_123", "org_acme", "viewer", "active"),
|
|
418
|
+
];
|
|
419
|
+
|
|
420
|
+
let decision = verifier.verify_from_records(&context, "organization_members", &records);
|
|
421
|
+
assert_eq!(
|
|
422
|
+
decision,
|
|
423
|
+
MembershipDecision::Verified {
|
|
424
|
+
role: Some("viewer".into())
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#[test]
|
|
430
|
+
fn deny_when_no_matching_record() {
|
|
431
|
+
let verifier = test_verifier();
|
|
432
|
+
let context = test_context();
|
|
433
|
+
let records = vec![test_membership("user_other", "org_acme", "admin", "active")];
|
|
434
|
+
|
|
435
|
+
let decision = verifier.verify_from_records(&context, "organization_members", &records);
|
|
436
|
+
assert!(matches!(decision, MembershipDecision::Denied(_)));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
#[test]
|
|
440
|
+
fn skip_inactive_when_getting_roles() {
|
|
441
|
+
let verifier = test_verifier();
|
|
442
|
+
let records = vec![
|
|
443
|
+
test_membership("user_123", "org_acme", "admin", "active"),
|
|
444
|
+
test_membership("user_123", "org_acme", "suspended_role", "suspended"),
|
|
445
|
+
];
|
|
446
|
+
|
|
447
|
+
let roles = verifier.get_roles("user_123", "org_acme", "organization_members", &records);
|
|
448
|
+
assert_eq!(roles.len(), 1);
|
|
449
|
+
assert_eq!(roles[0], "admin");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
#[test]
|
|
453
|
+
fn handle_missing_optional_fields() {
|
|
454
|
+
let verifier = test_verifier();
|
|
455
|
+
let context = test_context();
|
|
456
|
+
let record = json!({
|
|
457
|
+
"user_id": "user_123",
|
|
458
|
+
"organization_id": "org_acme"
|
|
459
|
+
// No role or status fields
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
let decision = verifier.verify(&context, "organization_members", &record);
|
|
463
|
+
// Should still verify even without optional fields
|
|
464
|
+
assert!(matches!(decision, MembershipDecision::Verified { .. }));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#[test]
|
|
468
|
+
fn no_policy_denied() {
|
|
469
|
+
let verifier = test_verifier();
|
|
470
|
+
let context = test_context();
|
|
471
|
+
let record = test_membership("user_123", "org_acme", "admin", "active");
|
|
472
|
+
|
|
473
|
+
let decision = verifier.verify(&context, "nonexistent_policy", &record);
|
|
474
|
+
assert!(matches!(decision, MembershipDecision::Denied(_)));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
#[test]
|
|
478
|
+
fn duplicate_roles_not_added() {
|
|
479
|
+
let verifier = test_verifier();
|
|
480
|
+
let records = vec![
|
|
481
|
+
test_membership("user_123", "org_acme", "admin", "active"),
|
|
482
|
+
test_membership("user_123", "org_acme", "admin", "active"),
|
|
483
|
+
];
|
|
484
|
+
|
|
485
|
+
let roles = verifier.get_roles("user_123", "org_acme", "organization_members", &records);
|
|
486
|
+
assert_eq!(roles.len(), 1);
|
|
487
|
+
}
|
|
488
|
+
}
|